UNPKG

snake-ai-game

Version:

A terminal-based snake game powered by AI.

225 lines (174 loc) 7.43 kB
import { Position, Direction, GameState, SpatialAnalysis } from './types.js'; import { getPositionKey } from './utils.js'; export const calculateNewPosition = (position: Position, direction: Direction, gridSize: number): Position => { const directions = { UP: { x: 0, y: -1 }, DOWN: { x: 0, y: 1 }, LEFT: { x: -1, y: 0 }, RIGHT: { x: 1, y: 0 } }; const delta = directions[direction]; return { x: (position.x + delta.x + gridSize) % gridSize, y: (position.y + delta.y + gridSize) % gridSize }; }; export const isValidMove = (direction: Direction, state: GameState): boolean => { const newHeadPos = calculateNewPosition(state.snake[0], direction, state.gridSize); if (state.obstacles?.some(obs => obs.x === newHeadPos.x && obs.y === newHeadPos.y)) return false; return !state.snake.slice(1).some(segment => segment.x === newHeadPos.x && segment.y === newHeadPos.y); }; export const getValidMoves = (state: GameState): Direction[] => (['UP', 'DOWN', 'LEFT', 'RIGHT'] as const) .filter(direction => isValidMove(direction, state)); export const findShortestPathToFood = (state: GameState): Direction[] => { const { snake, food, gridSize, obstacles = [] } = state; const head = snake[0]; const obstacleSet = new Set(obstacles.map(getPositionKey)); const visited = new Set([...snake.slice(1).map(getPositionKey), ...obstacleSet]); const queue: { pos: Position; path: Direction[] }[] = []; ['UP', 'DOWN', 'LEFT', 'RIGHT'].forEach(direction => { const nextPos = calculateNewPosition(head, direction as Direction, gridSize); const posKey = getPositionKey(nextPos); if (!visited.has(posKey)) { queue.push({ pos: nextPos, path: [direction as Direction] }); visited.add(posKey); } }); while (queue.length > 0) { const { pos, path } = queue.shift()!; if (pos.x === food.x && pos.y === food.y) return path; if (state.visibilityRadius && Math.max(Math.abs(pos.x - head.x), Math.abs(pos.y - head.y)) > state.visibilityRadius) { continue; } ['UP', 'DOWN', 'LEFT', 'RIGHT'].forEach(direction => { const nextPos = calculateNewPosition(pos, direction as Direction, gridSize); const posKey = getPositionKey(nextPos); if (!visited.has(posKey)) { queue.push({ pos: nextPos, path: [...path, direction as Direction] }); visited.add(posKey); } }); } return []; }; const calculateSafety = (pos: Position, state: GameState): number => { const { gridSize, snake, obstacles = [] } = state; let safeExits = 0; ['UP', 'DOWN', 'LEFT', 'RIGHT'].forEach(dir => { const nextPos = calculateNewPosition(pos, dir as Direction, gridSize); const isSafe = !snake.some(segment => segment.x === nextPos.x && segment.y === nextPos.y) && !obstacles.some(obs => obs.x === nextPos.x && obs.y === nextPos.y); if (isSafe) safeExits++; }); return safeExits; }; export const analyzeSpatialState = (state: GameState): SpatialAnalysis => { const { snake, food, gridSize, obstacles = [] } = state; const head = snake[0]; const snakeSet = new Set(snake.map(getPositionKey)); const obstacleSet = new Set(obstacles.map(getPositionKey)); const floodFill = (startPos: Position): Set<string> => { const visited = new Set<string>(); const queue: Position[] = [startPos]; while (queue.length > 0) { const pos = queue.shift()!; const posKey = getPositionKey(pos); if (visited.has(posKey) || snakeSet.has(posKey) || obstacleSet.has(posKey)) continue; visited.add(posKey); ['UP', 'DOWN', 'LEFT', 'RIGHT'].forEach(dir => { const nextPos = calculateNewPosition(pos, dir as Direction, gridSize); queue.push(nextPos); }); } return visited; }; const connectedRegions: Set<string>[] = []; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const posKey = getPositionKey({x, y}); if (snakeSet.has(posKey) || obstacleSet.has(posKey)) continue; const alreadyInRegion = connectedRegions.some(region => region.has(posKey)); if (!alreadyInRegion) connectedRegions.push(floodFill({x, y})); } } const distanceToFood = Math.abs(head.x - food.x) + Math.abs(head.y - food.y); const pathToFood = findShortestPathToFood(state); const openSpaces = gridSize * gridSize - snake.length - (obstacles?.length || 0); const potentialTraps: Position[] = []; const safetyScores: Record<Direction, number> = {} as Record<Direction, number>; const directions = ['UP', 'DOWN', 'LEFT', 'RIGHT'] as const; directions.forEach(dir => { const nextPos = calculateNewPosition(head, dir, gridSize); if (!snake.slice(1).some(s => s.x === nextPos.x && s.y === nextPos.y) && !obstacleSet.has(getPositionKey(nextPos))) { const nextState = { ...state, snake: [nextPos, ...snake.slice(0, snake.length - 1)] }; const validMoves = getValidMoves(nextState); safetyScores[dir] = calculateSafety(nextPos, state); if (validMoves.length <= 1) potentialTraps.push(nextPos); } }); const obstaclesInPath = pathToFood.reduce((count, dir) => { const nextPos = calculateNewPosition(head, dir, gridSize); return count + (obstacleSet.has(getPositionKey(nextPos)) ? 1 : 0); }, 0); return { openSpaces, connectedRegions: connectedRegions.length, distanceToFood, potentialTraps, safePathExists: pathToFood.length > 0, optimalPathLength: pathToFood.length || undefined, obstaclesInPath, safetyScores }; }; export const suggestOptimalMove = (state: GameState): { direction: Direction, confidence: number, plannedPath?: Direction[] } => { const analysis = analyzeSpatialState(state); const pathToFood = findShortestPathToFood(state); const validMoves = getValidMoves(state); if (validMoves.length === 0) { return { direction: state.direction, confidence: 0.1 }; } if (validMoves.length === 1) { return { direction: validMoves[0], confidence: 0.95 }; } // If there's a clear path to food, follow it if (pathToFood.length > 0) { const nextMove = pathToFood[0]; // Check if this move leads to a trap const willTrap = analysis.potentialTraps.some(trap => { const nextPos = calculateNewPosition(state.snake[0], nextMove, state.gridSize); return trap.x === nextPos.x && trap.y === nextPos.y; }); if (!willTrap) { return { direction: nextMove, confidence: 0.9 - (0.05 * Math.min(5, pathToFood.length)), plannedPath: pathToFood }; } } // Otherwise, choose the move with the highest safety score let bestMove = validMoves[0]; let bestScore = -1; validMoves.forEach(move => { const safetyScore = analysis.safetyScores?.[move] || 0; const newPos = calculateNewPosition(state.snake[0], move, state.gridSize); const distanceToFood = Math.abs(newPos.x - state.food.x) + Math.abs(newPos.y - state.food.y); const adjustedScore = safetyScore * 3 - distanceToFood * 0.5; if (adjustedScore > bestScore) { bestScore = adjustedScore; bestMove = move; } }); return { direction: bestMove, confidence: 0.5 + (0.1 * (bestScore / 10)) }; };