Files
website/src/lib/games/snake.ts
T

98 lines
2.7 KiB
TypeScript

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<Direction, Point> = {
up: { x: 0, y: -1 },
down: { x: 0, y: 1 },
left: { x: -1, y: 0 },
right: { x: 1, y: 0 }
};
const oppositeDirections: Record<Direction, Direction> = {
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);
}