```html Snake Game
Score: 0
``` ```typescript // snake.ts const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement | null; if (!canvas) throw new Error('Canvas element not found'); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('2D context not available'); const scoreElement = document.getElementById('score'); if (!scoreElement) throw new Error('Score element not found'); const GRID_SIZE = 20; const CELL_SIZE = canvas.width / GRID_SIZE; // 20 interface Point { x: number; y: number; } let snake: Point[] = [ { x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }, ]; let direction: Point = { x: 1, y: 0 }; let nextDirection: Point = { x: 1, y: 0 }; let food: Point = { x: 15, y: 15 }; let score = 0; let gameOver = false; let gameInterval: number | undefined; function randomFoodPosition(): Point { while (true) { const x = Math.floor(Math.random() * GRID_SIZE); const y = Math.floor(Math.random() * GRID_SIZE); if (!snake.some(segment => segment.x === x && segment.y === y)) { return { x, y }; } } } function draw(): void { if (!ctx) return; // Clear canvas ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw snake ctx.fillStyle = '#0f0'; for (const segment of snake) { ctx.fillRect(segment.x * CELL_SIZE, segment.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); } // Draw food ctx.fillStyle = '#f00'; ctx.fillRect(food.x * CELL_SIZE, food.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); // Draw game over text if (gameOver) { ctx.fillStyle = '#fff'; ctx.font = '30px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2); } } function update(): void { if (gameOver) return; // Apply the queued direction direction = nextDirection; // Calculate new head position const head = snake[0]; const newHead: Point = { x: head.x + direction.x, y: head.y + direction.y, }; // Wall collision if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) { gameOver = true; stopGame(); draw(); return; } // Insert new head snake.unshift(newHead); // Self collision (check against body, i.e., everything after the new head) const collidesWithBody = snake.slice(1).some(segment => segment.x === newHead.x && segment.y === newHead.y); if (collidesWithBody) { gameOver = true; stopGame(); draw(); return; } // Food consumption if (newHead.x === food.x && newHead.y === food.y) { score++; scoreElement.textContent = `Score: ${score}`; food = randomFoodPosition(); // Do not remove tail → snake grows } else { snake.pop(); // Remove tail to keep length constant } draw(); } function stopGame(): void { if (gameInterval !== undefined) { clearInterval(gameInterval); gameInterval = undefined; } } function startGame(): void { if (gameInterval) clearInterval(gameInterval); gameInterval = window.setInterval(update, 150); } // Keyboard controls document.addEventListener('keydown', (event: KeyboardEvent) => { if (gameOver) return; switch (event.key) { case 'ArrowUp': if (direction.y !== 1) { // Prevent reversing nextDirection = { x: 0, y: -1 }; } event.preventDefault(); break; case 'ArrowDown': if (direction.y !== -1) { nextDirection = { x: 0, y: 1 }; } event.preventDefault(); break; case 'ArrowLeft': if (direction.x !== 1) { nextDirection = { x: -1, y: 0 }; } event.preventDefault(); break; case 'ArrowRight': if (direction.x !== -1) { nextDirection = { x: 1, y: 0 }; } event.preventDefault(); break; } }); // Initial render and game start draw(); startGame(); ```