98 lines
2.7 KiB
TypeScript
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);
|
|
}
|