export type Direction = 'up' | 'down' | 'left' | 'right'; export type SnakeStatus = 'ready' | 'running' | 'paused' | 'over' | 'won'; export interface Point { x: number; y: number; } export interface SnakeGame { snake: Point[]; direction: Direction; queuedDirections: Direction[]; food: Point | null; score: number; status: SnakeStatus; } export const boardSize = 20; const maxQueuedDirections = 2; const startingDelay = 130; const fastestDelay = 60; const delayDropPerFood = 3; const directionVectors: Record = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } }; const oppositeDirections: Record = { up: 'down', down: 'up', left: 'right', right: 'left' }; export function samePoint(first: Point, second: Point): boolean { return first.x === second.x && first.y === second.y; } function isOutsideBoard(point: Point): boolean { return point.x < 0 || point.y < 0 || point.x >= boardSize || point.y >= boardSize; } function randomFreeCell(snake: Point[]): Point | null { const freeCells: Point[] = []; for (let y = 0; y < boardSize; y++) { for (let x = 0; x < boardSize; x++) { if (!snake.some((part) => part.x === x && part.y === y)) freeCells.push({ x, y }); } } return freeCells[Math.floor(Math.random() * freeCells.length)] ?? null; } export function createSnakeGame(): SnakeGame { const middle = Math.floor(boardSize / 2); const snake = [ { x: middle, y: middle }, { x: middle - 1, y: middle }, { x: middle - 2, y: middle } ]; return { snake, direction: 'right', queuedDirections: [], food: randomFreeCell(snake), score: 0, status: 'ready' }; } export function queueDirection(game: SnakeGame, direction: Direction) { const lastDirection = game.queuedDirections.at(-1) ?? game.direction; if (direction === lastDirection || direction === oppositeDirections[lastDirection]) return; if (game.queuedDirections.length < maxQueuedDirections) game.queuedDirections.push(direction); } export function advanceSnake(game: SnakeGame) { game.direction = game.queuedDirections.shift() ?? game.direction; const head = game.snake[0]; const vector = directionVectors[game.direction]; const nextHead = { x: head.x + vector.x, y: head.y + vector.y }; const ateFood = game.food !== null && samePoint(nextHead, game.food); const body = ateFood ? game.snake : game.snake.slice(0, -1); if (isOutsideBoard(nextHead) || body.some((part) => samePoint(part, nextHead))) { game.status = 'over'; return; } game.snake = [nextHead, ...body]; if (!ateFood) return; game.score += 1; game.food = randomFreeCell(game.snake); if (!game.food) game.status = 'won'; } export function tickDelay(score: number): number { return Math.max(fastestDelay, startingDelay - score * delayDropPerFood); }