UNPKG

snake-ai-game

Version:

A terminal-based snake game powered by AI.

110 lines (85 loc) 3.58 kB
import { GameState, Position, Direction } from './types.js'; import { getRandomInt, getPositionKey, arePositionsEqual } from './utils.js'; import { calculateNewPosition } from './gameLogic.js'; import { GAME_CONSTANTS } from './constants/index.js'; class SnakeStateManager { private static instance: SnakeStateManager; private currentState: GameState = { snake: [...GAME_CONSTANTS.DEFAULT_SNAKE_START], food: { ...GAME_CONSTANTS.DEFAULT_FOOD_POSITION }, direction: GAME_CONSTANTS.DEFAULT_DIRECTION, gridSize: GAME_CONSTANTS.DEFAULT_GRID_SIZE, score: 0, isGameOver: false, timeLimit: GAME_CONSTANTS.DEFAULT_TIME_LIMIT, obstacles: [], visibilityRadius: undefined }; private constructor() {} public static getInstance = (): SnakeStateManager => SnakeStateManager.instance ??= new SnakeStateManager(); public getState = (): GameState => ({ ...this.currentState }); public setState = (newState: Partial<GameState>): void => { this.currentState = { ...this.currentState, ...newState }; }; public generateNewFood = (): Position => { const { snake, gridSize, obstacles = [] } = this.currentState; const occupiedPositions = new Set([ ...snake.map(getPositionKey), ...(obstacles?.map(getPositionKey) || []) ]); let newFood: Position; do { newFood = { x: getRandomInt(0, gridSize), y: getRandomInt(0, gridSize) }; } while (occupiedPositions.has(getPositionKey(newFood))); this.setState({ food: newFood }); return newFood; }; public moveSnake = (direction: Direction): { success: boolean; foodCollected: boolean } => { const { snake, food, gridSize, isGameOver } = this.currentState; if (isGameOver) return { success: false, foodCollected: false }; const headPos = snake[0]; const newHeadPos = calculateNewPosition(headPos, direction, gridSize); const isValidMove = this.isValidMove(newHeadPos); if (!isValidMove) { this.setState({ isGameOver: true }); return { success: false, foodCollected: false }; } const foodCollected = arePositionsEqual(newHeadPos, food); const newSnake = [newHeadPos, ...snake.slice(0, foodCollected ? snake.length : snake.length - 1)]; this.setState({ snake: newSnake, direction, score: foodCollected ? this.currentState.score + 1 : this.currentState.score }); if (foodCollected) this.generateNewFood(); return { success: true, foodCollected }; }; private isValidMove = (position: Position): boolean => { const { snake, obstacles = [] } = this.currentState; const hitObstacle = obstacles.some(obs => arePositionsEqual(obs, position)); if (hitObstacle) return false; return !snake.slice(1).some(segment => arePositionsEqual(segment, position)); }; public resetState = (preserveTimeLimit: boolean = true): void => { const currentTimeLimit = preserveTimeLimit ? this.currentState.timeLimit : GAME_CONSTANTS.DEFAULT_TIME_LIMIT; this.currentState = { snake: [...GAME_CONSTANTS.DEFAULT_SNAKE_START], food: { ...GAME_CONSTANTS.DEFAULT_FOOD_POSITION }, direction: GAME_CONSTANTS.DEFAULT_DIRECTION, gridSize: GAME_CONSTANTS.DEFAULT_GRID_SIZE, score: 0, isGameOver: false, timeLimit: currentTimeLimit, obstacles: [], visibilityRadius: undefined }; }; public giveUp = (): void => { this.setState({ isGameOver: true }); }; } export const gameState = SnakeStateManager.getInstance();