```html
Snake Game
Score: 0
```
```typescript
// snake.ts
// Declare browser globals to satisfy tsc without --lib DOM
declare var document: any;
declare var window: any;
// Constants
const GRID_SIZE = 20;
const CANVAS_WIDTH = 400;
const CANVAS_HEIGHT = 400;
const TILE_COUNT = CANVAS_WIDTH / GRID_SIZE;
const GAME_SPEED = 100; // Milliseconds per frame
// Types
interface Position {
x: number;
y: number;
}
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
// Game State
let snake: Position[] = [];
let food: Position = { x: 0, y: 0 };
let direction: Direction = 'RIGHT';
let nextDirection: Direction = 'RIGHT';
let score: number = 0;
let isGameOver: boolean = false;
let gameInterval: number = 0;
// DOM Elements
const canvas: any = document.getElementById('gameCanvas');
const ctx: any = canvas.getContext('2d');
const scoreElement: any = document.getElementById('score');
/**
* Initializes the game state and starts the game loop.
*/
function initGame(): void {
snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
];
direction = 'RIGHT';
nextDirection = 'RIGHT';
score = 0;
isGameOver = false;
updateScoreDisplay();
placeFood();
if (gameInterval) {
window.clearInterval(gameInterval);
}
gameInterval = window.setInterval(gameLoop, GAME_SPEED);
}
/**
* The main game loop called at regular intervals.
*/
function gameLoop(): void {
if (isGameOver) {
return;
}
update();
draw();
}
/**
* Updates the game state (movement, collisions).
*/
function update(): void {
direction = nextDirection;
const head: Position = { ...snake[0] };
switch (direction) {
case 'UP':
head.y -= 1;
break;
case 'DOWN':
head.y += 1;
break;
case 'LEFT':
head.x -= 1;
break;
case 'RIGHT':
head.x += 1;
break;
}
// Wall Collision
if (head.x < 0 || head.x >= TILE_COUNT || head.y < 0 || head.y >= TILE_COUNT) {
endGame();
return;
}
// Self Collision
for (const segment of snake) {
if (head.x === segment.x && head.y === segment.y) {
endGame();
return;
}
}
snake.unshift(head);
// Food Collision
if (head.x === food.x && head.y === food.y) {
score += 10;
updateScoreDisplay();
placeFood();
} else {
snake.pop();
}
}
/**
* Renders the game to the canvas.
*/
function draw(): void {
// Clear Canvas
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Draw Food
ctx.fillStyle = 'red';
ctx.fillRect(food.x * GRID_SIZE, food.y * GRID_SIZE, GRID_SIZE - 2, GRID_SIZE - 2);
// Draw Snake
ctx.fillStyle = '#00FF00';
for (const segment of snake) {
ctx.fillRect(segment.x * GRID_SIZE, segment.y * GRID_SIZE, GRID_SIZE - 2, GRID_SIZE - 2);
}
// Draw Game Over text
if (isGameOver) {
ctx.fillStyle = 'white';
ctx.font = '30px Arial';
ctx.textAlign = 'center';
ctx.fillText('Game Over', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
}
}
/**
* Places food at a random position not occupied by the snake.
*/
function placeFood(): void {
while (true) {
const x = Math.floor(Math.random() * TILE_COUNT);
const y = Math.floor(Math.random() * TILE_COUNT);
let onSnake = false;
for (const segment of snake) {
if (segment.x === x && segment.y === y) {
onSnake = true;
break;
}
}
if (!onSnake) {
food = { x, y };
break;
}
}
}
/**
* Updates the score display in the DOM.
*/
function updateScoreDisplay(): void {
if (scoreElement) {
scoreElement.innerText = score.toString();
}
}
/**
* Handles the Game Over state.
*/
function endGame(): void {
isGameOver = true;
draw(); // Draw one last time to show the collision state and Game Over text
}
/**
* Handles keyboard input.
*/
function handleKeyDown(e: any): void {
const key: string = e.key;
if (key === 'ArrowUp' && direction !== 'DOWN') {
nextDirection = 'UP';
} else if (key === 'ArrowDown' && direction !== 'UP') {
nextDirection = 'DOWN';
} else if (key === 'ArrowLeft' && direction !== 'RIGHT') {
nextDirection = 'LEFT';
} else if (key === 'ArrowRight' && direction !== 'LEFT') {
nextDirection = 'RIGHT';
}
}
// Setup input listener
window.addEventListener('keydown', handleKeyDown);
// Start the game
initGame();
```