diff --git a/src/app.service.ts b/src/app.service.ts index ef75225..053f8a8 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -11,6 +11,11 @@ export class AppService { this.history = []; } + isSnakeDead(): boolean { + const [previous, current] = this.history.slice(-2) + return previous && current && previous.board.snakes.length > current.board.snakes.length + } + public async getSnake(): Promise { Logger.log(`Getting snake...`, 'AppService'); return { @@ -24,11 +29,14 @@ export class AppService { public async start(state: GameState): Promise { Logger.log(`Starting [${state.game.id}]...`, 'AppService'); + this.history = [] } public async move(state: GameState): Promise { this.history.push(state); Logger.log(`Staring turn [${state.turn}]...`, 'AppService'); + let shout: string + if (this.isSnakeDead()) shout = 'f' const { move } = await this.moveService.findMove(state, 1); Logger.log(`Moving [${move}] on turn ${state.turn}`, 'AppService'); return { diff --git a/src/movement/movement.service.ts b/src/movement/movement.service.ts index 45324ac..7db624c 100644 --- a/src/movement/movement.service.ts +++ b/src/movement/movement.service.ts @@ -8,11 +8,11 @@ import { MoveRank } from '../dtos/move-rank'; export class MovementService { random: boolean = true; - private foodWeight = 0.8 - private conflictWeight = 0.4 - private defaultWeight = 0.5 + private foodWeight = 0.8; + private conflictWeight = 0.4; + private defaultWeight = 0.5; - constructor(private boundaryService: BoundaryService) {} + constructor(private boundaryService: BoundaryService) { } async calculateWeight( state: GameState, @@ -22,59 +22,86 @@ export class MovementService { const options: Direction[] = this.random ? Object.values(DirectionEnum).sort(() => Math.random() - 0.5) : Object.values(DirectionEnum); - const newState: GameState = this.boundaryService.moveAsState(move, state) - if (weight < (1 / (state.board.height * 20))) { + const newState: GameState = this.boundaryService.moveAsState(move, state); + + // Prioritize open spaces by checking the number of available neighboring spaces + const openSpaceWeight = 1 / newState.you.body.length; + + // Penalize moves that lead to narrow paths or dead ends + const narrowPathPenalty = 0.2; + + if (weight < openSpaceWeight) { return weight; } + let moves: MoveRank[]; const isOnFoodSource = this.boundaryService.withinSet( state.board.food, newState.you.head, ); - const hasConflictPotential = (await Promise.all(newState.board.snakes.map((snake: Snake): boolean => { - if (snake.id === newState.you.id) return false - const snakeMoves: Coordinate[] = options.map((m: Direction) => this.boundaryService.moveAsCoord(m, snake.head)) - const isSmallerSnake = snake.body.length > newState.you.body.length - return isSmallerSnake && this.boundaryService.withinSet( - snakeMoves, - newState.you.head, + let conflictAvoidanceWeight = 0; + const hasConflictPotential = ( + await Promise.all( + newState.board.snakes.map(async (snake: Snake): Promise => { + if (snake.id === newState.you.id) return false; + + const snakeMoves: Coordinate[] = options.map((m: Direction) => + this.boundaryService.moveAsCoord(m, snake.head), + ); + + // Dynamic conflict avoidance: Increase weight for conflict avoidance if a smaller snake is close + const isSmallerSnake = snake.body.length > newState.you.body.length; + conflictAvoidanceWeight = isSmallerSnake + ? this.conflictWeight + : 0; + + return ( + isSmallerSnake && + this.boundaryService.withinSet(snakeMoves, newState.you.head) + ); + }), ) - }))).some(m => m) - const isHungry = state.you.health < 70 + ).some((m) => m); + + const isHungry = state.you.health < 70; + if (hasConflictPotential && !isHungry) { moves = await Promise.all( - options.map(m => + options.map((m) => this.calculateMove( newState, m, - weight * this.conflictWeight, + weight * (this.conflictWeight + openSpaceWeight), ), ), ); } else if (isOnFoodSource) { moves = await Promise.all( - options.map(m => + options.map((m) => this.calculateMove( newState, m, - weight * this.foodWeight, + weight * (this.foodWeight + openSpaceWeight), ), ), ); } else { moves = await Promise.all( - options.map(m => + options.map((m) => this.calculateMove( newState, m, - weight * this.defaultWeight, + weight * (this.defaultWeight + openSpaceWeight - narrowPathPenalty), ), ), ); } + const projectedWeight = moves.reduce((a, b) => a + b.weight, 0) / moves.length; - return projectedWeight + (weight * this.defaultWeight); + + // Adjust weight by incorporating open space weight and conflict avoidance weight + return projectedWeight + (weight * openSpaceWeight - conflictAvoidanceWeight); } async calculateMove( @@ -82,17 +109,28 @@ export class MovementService { move: Direction, weight: number, ): Promise { - const newState: GameState = this.boundaryService.moveAsState(move, state) - const isOffTheBoard = this.boundaryService.offBoard(state, newState.you.head); + const newState: GameState = this.boundaryService.moveAsState( + move, + state, + ); + + const isOffTheBoard = this.boundaryService.offBoard( + state, + newState.you.head, + ); const isWithinOwnBody = this.boundaryService.withinSet( newState.you.body.slice(1, newState.you.body.length), newState.you.head, ); - const isWithinAnotherSnake = state.board.snakes.some(s => this.boundaryService.withinSet(s.body, newState.you.head)) + const isWithinAnotherSnake = state.board.snakes.some((s) => + this.boundaryService.withinSet(s.body, newState.you.head), + ); + if (!isOffTheBoard && !isWithinOwnBody && !isWithinAnotherSnake) { const _weight = await this.calculateWeight(state, move, weight); return { move, weight: _weight }; } + return { move, weight: 0 }; } @@ -101,7 +139,7 @@ export class MovementService { ? Object.values(DirectionEnum).sort(() => Math.random() - 0.5) : Object.values(DirectionEnum); const moves: MoveRank[] = await Promise.all( - options.map(move => this.calculateMove(state, move, weight)), + options.map((move) => this.calculateMove(state, move, weight)), ); return moves.reduce((a, b) => (a.weight >= b.weight ? a : b), { move: 'right',