snake-ai-game
Version:
A terminal-based snake game powered by AI.
110 lines (85 loc) • 3.58 kB
text/typescript
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();