```html
Snake Game
Score: 0
```
```typescript
// snake.ts
type Nullable = T | null;
enum Direction {
Up,
Down,
Left,
Right,
}
interface Point {
x: number;
y: number;
}
class SnakeGame {
private readonly canvas: HTMLCanvasElement;
private readonly ctx: CanvasRenderingContext2D;
private readonly cellSize: number = 20;
private readonly cols: number;
private readonly rows: number;
private snake: Point[] = [];
private direction: Direction = Direction.Right;
private nextDirection: Direction = Direction.Right;
private food: Point = { x: 0, y: 0 };
private score: number = 0;
private intervalId: Nullable = null;
private readonly speed: number = 100; // ms per step
private readonly scoreElement: HTMLElement;
constructor(canvasId: string, scoreId: string) {
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
if (!canvas || !ctx) {
throw new Error('Canvas not supported');
}
this.canvas = canvas;
this.ctx = ctx;
this.cols = Math.floor(this.canvas.width / this.cellSize);
this.rows = Math.floor(this.canvas.height / this.cellSize);
const scoreEl = document.getElementById(scoreId);
if (!scoreEl) {
throw new Error('Score element not found');
}
this.scoreElement = scoreEl;
this.reset();
this.attachKeyboard();
this.start();
}
private reset(): void {
this.snake = [
{ x: Math.floor(this.cols / 2), y: Math.floor(this.rows / 2) },
];
this.direction = Direction.Right;
this.nextDirection = Direction.Right;
this.placeFood();
this.score = 0;
this.updateScore();
}
private attachKeyboard(): void {
window.addEventListener('keydown', (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp':
if (this.direction !== Direction.Down) this.nextDirection = Direction.Up;
break;
case 'ArrowDown':
if (this.direction !== Direction.Up) this.nextDirection = Direction.Down;
break;
case 'ArrowLeft':
if (this.direction !== Direction.Right) this.nextDirection = Direction.Left;
break;
case 'ArrowRight':
if (this.direction !== Direction.Left) this.nextDirection = Direction.Right;
break;
}
});
}
private start(): void {
this.intervalId = window.setInterval(() => this.gameLoop(), this.speed);
}
private stop(): void {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
private gameLoop(): void {
this.updateDirection();
this.moveSnake();
this.checkCollisions();
this.draw();
}
private updateDirection(): void {
this.direction = this.nextDirection;
}
private moveSnake(): void {
const head = this.snake[0];
const newHead: Point = { x: head.x, y: head.y };
switch (this.direction) {
case Direction.Up:
newHead.y -= 1;
break;
case Direction.Down:
newHead.y += 1;
break;
case Direction.Left:
newHead.x -= 1;
break;
case Direction.Right:
newHead.x += 1;
break;
}
this.snake.unshift(newHead);
if (newHead.x === this.food.x && newHead.y === this.food.y) {
this.score += 1;
this.updateScore();
this.placeFood();
} else {
this.snake.pop();
}
}
private checkCollisions(): void {
const head = this.snake[0];
// Wall collision
if (head.x < 0 || head.x >= this.cols || head.y < 0 || head.y >= this.rows) {
this.gameOver();
return;
}
// Self collision
for (let i = 1; i < this.snake.length; i++) {
const segment = this.snake[i];
if (head.x === segment.x && head.y === segment.y) {
this.gameOver();
return;
}
}
}
private gameOver(): void {
this.stop();
alert(`Game Over! Your score: ${this.score}`);
this.reset();
this.start();
}
private placeFood(): void {
let newFood: Point;
do {
newFood = {
x: Math.floor(Math.random() * this.cols),
y: Math.floor(Math.random() * this.rows),
};
} while (this.snake.some(seg => seg.x === newFood.x && seg.y === newFood.y));
this.food = newFood;
}
private updateScore(): void {
this.scoreElement.textContent = `Score: ${this.score}`;
}
private drawCell(p: Point, color: string): void {
this.ctx.fillStyle = color;
this.ctx.fillRect(
p.x * this.cellSize,
p.y * this.cellSize,
this.cellSize,
this.cellSize
);
}
private draw(): void {
// Clear board
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw food
this.drawCell(this.food, 'red');
// Draw snake
this.snake.forEach((segment, index) => {
const color = index === 0 ? 'lime' : 'green';
this.drawCell(segment, color);
});
}
}
// Initialise the game when the page is ready
window.addEventListener('load', () => {
new SnakeGame('gameCanvas', 'score');
});
```