Snake
Score: 0
High: 0
Arrow keys or WASD to move · Space to start/pause · R to restart
```
```typescript
type Cell = { x: number; y: number };
type Direction = { x: number; y: number };
const GRID = 20;
const CELL = 20;
const INITIAL_TICK_MS = 150;
const MIN_TICK_MS = 60;
const SPEEDUP_EVERY = 5; // apples
const SPEEDUP_DELTA = 10; // ms
class Game {
// Public interface
constructor(canvas: HTMLCanvasElement, scoreEl: HTMLElement, highEl: HTMLElement) {
this.canvas = canvas;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas not supported');
this.ctx = ctx;
this.scoreEl = scoreEl;
this.highEl = highEl;
// Load high from provided highEl text if already loaded externally
this.high = Game.loadHigh();
// Initialize state
this.restart();
// Initial render (paused before first start)
this.draw();
}
start(): void {
if (this.started) {
// If already started but paused, resume
if (!this.alive) return;
if (!this.paused) return;
this.paused = false;
this.startTick();
this.draw();
return;
}
// First start
this.started = true;
this.paused = false;
this.startTick();
this.draw();
}
togglePause(): void {
if (!this.started || !this.alive) return;
this.paused = !this.paused;
if (this.paused) {
this.stopTick();
this.draw();
} else {
this.startTick();
}
}
restart(): void {
// stop any running tick
this.stopTick();
// initial snake: head at (10,10), body at (9,10),(8,10), moving right
this.snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 },
];
this.dir = { x: 1, y: 0 };
this.pendingDir = null;
this.score = 0;
this.tickMs = INITIAL_TICK_MS;
this.alive = true;
this.started = false;
this.paused = true;
this.intervalId = null;
// spawn initial food
this.food = this.spawnFood();
// update DOM
this.updateScore();
this.updateHigh();
// draw initial paused-before-start screen
this.draw();
}
// Private / internal
// state fields (public shape required in spec comment)
snake!: Cell[];
dir!: Direction;
pendingDir!: Direction | null;
food!: Cell;
score!: number;
high!: number;
tickMs!: number;
alive!: boolean;
started!: boolean;
paused!: boolean;
// internal helpers
private canvas!: HTMLCanvasElement;
private ctx!: CanvasRenderingContext2D;
private scoreEl!: HTMLElement;
private highEl!: HTMLElement;
private intervalId: number | null = null;
private static loadHigh(): number {
const s = localStorage.getItem('snake.high');
const n = s ? parseInt(s, 10) : 0;
return Number.isFinite(n) ? n : 0;
}
private saveHigh(): void {
localStorage.setItem('snake.high', String(this.high));
}
private updateScore(): void {
this.scoreEl.textContent = `Score: ${this.score}`;
}
private updateHigh(): void {
this.highEl.textContent = `High: ${this.high}`;
}
private startTick(): void {
if (this.intervalId != null) return;
// Use arrow to bind 'this'
this.intervalId = window.setInterval(() => this.tick(), this.tickMs);
}
private stopTick(): void {
if (this.intervalId != null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
private restartTickIfNeeded(newMs: number): void {
if (newMs === this.tickMs) return;
this.tickMs = newMs;
if (this.intervalId != null) {
// restart interval with new ms
this.stopTick();
this.startTick();
}
}
private spawnFood(): Cell {
const occupied = new Set