Video Tutorial | 'Snake Game' With AI
Here we develop a snake game from scratch but with a catch. You have to compete against AI to win.
An Important Note
Before we move into the details of this game, kindly read a bit about ‘Manhattan distance’. The logic controlling the snake is built using this.
Also, here is a basic screenshot to help you understand. Do not let it intimidate you.
Since the game is created using a grid, a snake cannot move in a single line, so Euclidean distance cannot be followed, as a snake does not move diagonally, so it follows a Manhattan path.
Both the blue and the green path land at the same time.
The Video
In this 30-minute video, I cover in detail the process of creating a game. You can also view the challenges I face and how I overcome them.
(PS: I am happy with the result, and there is some learning here…)
DO SUBSCRIBE TO THE YOUTUBE CHANNEL
The Recap
So, in the past few posts, we covered how to create games with the below -
Functionality to solve equations and word combinations
Race against a Timer
MP3 files for correct, incorrect, background music, and repeated words
Audio Readout For Rules
Results Showcase
Cookie to save game data on the browser (for 24 hours) in case you refresh
Dynamic background
Dark and Light Modes
The Context
Today, we look at building a ‘Snake Game’. This game is ideated while building the video, so there are no predefined rules to follow. Through the course of the video, I think of what the rules should be and I keep refining them.
What I have tried to do is map a real-world scenario, imitating the problems we face while building something through AI. It might not always respond with what you want, but that is the fun. Once you learn how to prompt it, you also learn why the initial prompt did not make work and it all starts to make sense.
These are the basic hurdles that we need to cross to help make our journey easier.
The Output
While you enjoy the video that I created, below are some of the features covered
The snake game has an AI opponent
There are different levels of difficulty for the opponent
There are a certain set of rules like win, loss and tie, based on collision between the snakes or ending of rounds.
The Prompt
Create a complete HTML file for a Snake Game with the following requirements:
Game Mechanics:
Implement a two-player competitive Snake game where one snake is controlled by the player (using arrow keys) and the other by an AI opponent.
The game is played on a square grid with a grid size of 20 pixels per tile.
Both snakes move continuously, and the player can queue direction changes to prevent immediate reversals.
The snake is controlled using the arrow keys on the keyboard.
A single food item (red triangle) spawns randomly, and when eaten by either snake, it increases the respective snake's score, and a new food spawns.
The length of the snake eating the food item is increases by 1.
The game ends after 30 rounds (food eaten) or upon collision (head-to-head or head-to-body).
Collisions:
Head-to-head collision results in a tie.
If a snake's head hits the other's body, the other snake wins.
Track cumulative scores across games (Player vs. Opponent).
After a game ends, display a "Game Over" message with the winner and restart after a 5-second countdown.
AI Opponent:
The AI has three difficulty levels (easy, medium, difficult), selectable via a dropdown.
Easy: 50% random movement, 50% towards food.
Medium: Moves towards food without reversing.
Difficult: Uses Manhattan distance to move towards food, avoiding the player's snake when possible.
Visuals:
Use two HTML5 canvases: one for the game (foreground) and one for a decorative background.
The game canvas resizes dynamically to 70% of the smaller window dimension, maintaining a square shape.
Player snake is green, opponent snake is blue, both with square heads (with white eyes and black pupils oriented by direction) and circular body segments.
The background canvas fills the entire window with moving, semi-transparent colored circles (representing mini-snakes) that wrap around the edges and change direction randomly (5% chance per frame).
Support light and dark themes, toggled via a button, affecting canvas backgrounds, text, and button styles.
Audio:
Include background music (looped, toggleable with a play/pause button).
Sound effects: "yay" for player eating food, "no" for opponent eating food, "claps" for player winning, and "opponent wins" sound for opponent winning.
Audio files are referenced as background-music-snake.mp3, player-eats.mp3, opponent-eats.mp3, yay.mp3, and no.mp3.
UI Elements:
Display cumulative score at the top (Score - Player: X | Opponent: Y).
Show round count at the bottom (Rounds: X/30).
Include a difficulty dropdown and theme toggle button below the game canvas.
Show a 5-second countdown before the game starts and before restarting after game over.
Center all elements vertically and horizontally using flexbox.
Technical Details:
Use CSS for styling with smooth transitions for theme changes.
Ensure the game loop runs at a fixed interval (120ms).
Handle canvas resizing on window resize.
Use requestAnimationFrame for smooth animation.
Ensure snakes wrap around the grid edges.
Include error handling for audio playback.
Fix the typo in the theme toggle button (themeatoon should be themeToggle).
Output:
Provide a single HTML file with embedded CSS and JavaScript.
Ensure the code is clean, commented, and matches the described functionality exactly.
Do not include external libraries or dependencies.
The Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake Game</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
transition: background-color 0.3s;
}
.light-mode {
background-color: #e0e0e0;
color: #000;
}
.dark-mode {
background-color: #222;
color: #fff;
}
#backgroundCanvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
}
#gameCanvas {
border: 1px solid black;
z-index: 1;
transition: background-color 0.3s;
}
.light-mode #gameCanvas {
background-color: #f0f0f0;
}
.dark-mode #gameCanvas {
background-color: #333;
}
#cumulativeScore {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
font-size: 18px;
z-index: 2;
color: #000;
text-shadow: 0 0 2px #fff;
}
.dark-mode #cumulativeScore {
color: #fff;
text-shadow: 0 0 2px #000;
}
#musicControls {
position: absolute;
top: 40px;
width: 100%;
text-align: center;
z-index: 2;
}
#musicToggle {
font-size: 16px;
padding: 5px 10px;
cursor: pointer;
}
.dark-mode #musicToggle {
background-color: #444;
color: #fff;
border: 1px solid #fff;
}
#rounds {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
font-size: 18px;
z-index: 2;
color: #000;
text-shadow: 0 0 2px #fff;
}
.dark-mode #rounds {
color: #fff;
text-shadow: 0 0 2px #000;
}
#gameOver, #countdown {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 20px;
text-align: center;
font-size: 24px;
border-radius: 10px;
z-index: 2;
text-shadow: 0 0 2px #000;
}
#difficulty, #themeToggle {
margin: 5px;
font-size: 16px;
z-index: 2;
}
.dark-mode #difficulty, .dark-mode #themeToggle {
background-color: #444;
color: #fff;
border: 1px solid #fff;
}
#controls {
display: flex;
gap: 10px;
z-index: 2;
}
</style>
</head>
<body class="light-mode">
<div id="cumulativeScore">Score - Player: 0 | Opponent: 0</div>
<div id="musicControls">
<button id="musicToggle" onclick="toggleMusic()">Play Music</button>
</div>
<canvas id="backgroundCanvas"></canvas>
<canvas id="gameCanvas"></canvas>
<div id="controls">
<select id="difficulty" onchange="setDifficulty(this.value)">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="difficult">Difficult</option>
</select>
<button id="themeToggle" onclick="toggleTheme()">Dark Mode</button>
</div>
<div id="rounds">Rounds: 0/30</div>
<div id="gameOver">Game Over! <br><span id="winner"></span><br>Restarting in <span id="restartTimer">5</span> seconds...</div>
<div id="countdown">Starting in <span id="startTimer">5</span>...</div>
<audio id="backgroundMusic" src="background-music-snake.mp3" loop></audio>
<audio id="yaySound" src="player-eats.mp3"></audio>
<audio id="noSound" src="opponent-eats.mp3"></audio>
<audio id="clapsSound" src="yay.mp3"></audio>
<audio id="opponentWinsSound" src="no.mp3"></audio>
<script>
const gameCanvas = document.getElementById('gameCanvas');
const gameCtx = gameCanvas.getContext('2d');
const bgCanvas = document.getElementById('backgroundCanvas');
const bgCtx = bgCanvas.getContext('2d');
const gridSize = 20;
const tileCount = 20;
let canvasSize = 400;
// Audio elements
const backgroundMusic = document.getElementById('backgroundMusic');
const yaySound = document.getElementById('yaySound');
const noSound = document.getElementById('noSound');
const clapsSound = document.getElementById('clapsSound');
const opponentWinsSound = document.getElementById('opponentWinsSound');
let isMusicPlaying = false;
function resizeCanvases() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
canvasSize = Math.min(window.innerWidth, window.innerHeight) * 0.7;
gameCanvas.width = canvasSize;
gameCanvas.height = canvasSize;
}
resizeCanvases();
window.addEventListener('resize', resizeCanvases);
let playerSnake = [{ x: 5, y: 5 }];
let playerDirection = 'right';
let directionQueue = [];
let playerScore = 0;
let finalPlayerScore = 0;
let opponentSnake = [{ x: 15, y: 15 }];
let opponentDirection = 'left';
let opponentScore = 0;
let finalOpponentScore = 0;
let food = { x: 10, y: 10 };
let roundCount = 0;
const maxRounds = 30;
let gameOver = false;
let isStarting = false;
let difficulty = 'medium';
const gameSpeed = 120;
let lastTime = 0;
const bgSnakes = [
{ segments: [{ x: 100, y: 100 }], direction: 'right', radius: 6, speed: 4, length: 5, colorLight: 'rgba(0, 255, 0, 0.3)', colorDark: 'rgba(0, 200, 0, 0.3)' },
{ segments: [{ x: 150, y: 150 }], direction: 'left', radius: 10, speed: 3, length: 10, colorLight: 'rgba(0, 0, 255, 0.3)', colorDark: 'rgba(0, 0, 200, 0.3)' },
{ segments: [{ x: 200, y: 200 }], direction: 'up', radius: 8, speed: 5, length: 12, colorLight: 'rgba(255, 0, 0, 0.3)', colorDark: 'rgba(200, 0, 0, 0.3)' },
{ segments: [{ x: 250, y: 250 }], direction: 'down', radius: 12, speed: 2, length: 8, colorLight: 'rgba(255, 255, 0, 0.3)', colorDark: 'rgba(200, 200, 0, 0.3)' },
{ segments: [{ x: 300, y: 300 }], direction: 'right', radius: 7, speed: 4.5, length: 14, colorLight: 'rgba(128, 0, 128, 0.3)', colorDark: 'rgba(100, 0, 100, 0.3)' },
{ segments: [{ x: 350, y: 350 }], direction: 'left', radius: 9, speed: 3.5, length: 11, colorLight: 'rgba(0, 255, 255, 0.3)', colorDark: 'rgba(0, 200, 200, 0.3)' },
{ segments: [{ x: 400, y: 400 }], direction: 'up', radius: 11, speed: 2.5, length: 9, colorLight: 'rgba(255, 165, 0, 0.3)', colorDark: 'rgba(200, 130, 0, 0.3)' },
{ segments: [{ x: 450, y: 450 }], direction: 'down', radius: 6, speed: 4, length: 13, colorLight: 'rgba(50, 205, 50, 0.3)', colorDark: 'rgba(40, 160, 40, 0.3)' },
{ segments: [{ x: 500, y: 500 }], direction: 'right', radius: 10, speed: 3, length: 10, colorLight: 'rgba(139, 69, 19, 0.3)', colorDark: 'rgba(110, 55, 15, 0.3)' },
{ segments: [{ x: 550, y: 550 }], direction: 'left', radius: 8, speed: 5, length: 12, colorLight: 'rgba(75, 0, 130, 0.3)', colorDark: 'rgba(60, 0, 100, 0.3)' },
{ segments: [{ x: 600, y: 600 }], direction: 'up', radius: 12, speed: 2, length: 8, colorLight: 'rgba(255, 20, 147, 0.3)', colorDark: 'rgba(200, 15, 110, 0.3)' },
{ segments: [{ x: 650, y: 650 }], direction: 'down', radius: 7, speed: 4.5, length: 14, colorLight: 'rgba(30, 144, 255, 0.3)', colorDark: 'rgba(25, 110, 200, 0.3)' },
{ segments: [{ x: 700, y: 700 }], direction: 'right', radius: 9, speed: 3.5, length: 11, colorLight: 'rgba(173, 255, 47, 0.3)', colorDark: 'rgba(130, 200, 35, 0.3)' },
{ segments: [{ x: 750, y: 750 }], direction: 'left', radius: 11, speed: 2.5, length: 9, colorLight: 'rgba(220, 20, 60, 0.3)', colorDark: 'rgba(170, 15, 45, 0.3)' },
{ segments: [{ x: 800, y: 800 }], direction: 'up', radius: 6, speed: 4, length: 13, colorLight: 'rgba(147, 112, 219, 0.3)', colorDark: 'rgba(110, 85, 165, 0.3)' }
];
function updateBackground() {
bgCtx.fillStyle = document.body.classList.contains('light-mode') ? '#d0d0d0' : '#2a2a2a';
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
bgCtx.filter = 'blur(5px)';
bgSnakes.forEach(snake => {
let head = { ...snake.segments[0] };
if (snake.direction === 'up') head.y -= snake.speed;
else if (snake.direction === 'down') head.y += snake.speed;
else if (snake.direction === 'left') head.x -= snake.speed;
else if (snake.direction === 'right') head.x += snake.speed;
head.x = (head.x + bgCanvas.width) % bgCanvas.width;
head.y = (head.y + bgCanvas.height) % bgCanvas.height;
snake.segments.unshift(head);
if (snake.segments.length > snake.length) snake.segments.pop();
if (Math.random() < 0.05) {
const directions = ['up', 'down', 'left', 'right'].filter(dir =>
(dir === 'up' && snake.direction !== 'down') ||
(dir === 'down' && snake.direction !== 'up') ||
(dir === 'left' && snake.direction !== 'right') ||
(dir === 'right' && snake.direction !== 'left')
);
snake.direction = directions[Math.floor(Math.random() * directions.length)];
}
bgCtx.fillStyle = document.body.classList.contains('light-mode') ? snake.colorLight : snake.colorDark;
snake.segments.forEach(segment => {
bgCtx.beginPath();
bgCtx.arc(segment.x, segment.y, snake.radius, 0, 2 * Math.PI);
bgCtx.fill();
});
});
bgCtx.filter = 'none';
}
function toggleTheme() {
document.body.classList.toggle('light-mode');
document.body.classList.toggle('dark-mode');
document.getElementById('themeatoon').textContent = document.body.classList.contains('light-mode') ? 'Dark Mode' : 'Light Mode';
updateBackground();
}
function toggleMusic() {
if (isMusicPlaying) {
backgroundMusic.pause();
document.getElementById('musicToggle').textContent = 'Play Music';
} else {
backgroundMusic.play().catch(e => console.log('Music playback failed:', e));
document.getElementById('musicToggle').textContent = 'Pause Music';
}
isMusicPlaying = !isMusicPlaying;
}
function setDifficulty(level) {
if (!isStarting && !lastTime) {
difficulty = level;
startCountdown();
} else {
difficulty = level;
}
}
function startCountdown() {
isStarting = true;
document.getElementById('countdown').style.display = 'block';
let countdown = 5;
document.getElementById('startTimer').textContent = countdown;
// Start the animation loop during countdown to warm up rendering
lastTime = performance.now();
requestAnimationFrame(countdownLoop);
const countdownInterval = setInterval(() => {
countdown--;
document.getElementById('startTimer').textContent = countdown;
if (countdown <= 0) {
clearInterval(countdownInterval);
document.getElementById('countdown').style.display = 'none';
isStarting = false;
startGame();
}
}, 1000);
}
function countdownLoop(currentTime) {
if (!isStarting) return; // Exit when countdown ends
updateBackground();
drawGame();
requestAnimationFrame(countdownLoop);
}
function startGame() {
gameOver = false;
// Reset lastTime to current time to ensure smooth start
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function gameLoop(currentTime) {
if (gameOver) return;
// Ensure consistent updates by accumulating time
if (currentTime - lastTime >= gameSpeed) {
updateBackground();
updatePlayerSnake();
updateOpponentSnake();
checkCollisions();
drawGame();
lastTime = currentTime; // Update lastTime to currentTime for next frame
}
requestAnimationFrame(gameLoop);
}
document.addEventListener('keydown', (e) => {
if (gameOver || isStarting) return;
let newDirection;
switch (e.key) {
case 'ArrowUp': newDirection = 'up'; break;
case 'ArrowDown': newDirection = 'down'; break;
case 'ArrowLeft': newDirection = 'left'; break;
case 'ArrowRight': newDirection = 'right'; break;
default: return;
}
if (!directionQueue.length || directionQueue[directionQueue.length - 1] !== newDirection) {
directionQueue.push(newDirection);
}
});
function updatePlayerSnake() {
while (directionQueue.length) {
let nextDir = directionQueue[0];
if (
(nextDir === 'up' && playerDirection !== 'down') ||
(nextDir === 'down' && playerDirection !== 'up') ||
(nextDir === 'left' && playerDirection !== 'right') ||
(nextDir === 'right' && playerDirection !== 'left')
) {
playerDirection = nextDir;
directionQueue.shift();
break;
} else {
directionQueue.shift();
}
}
let head = { ...playerSnake[0] };
if (playerDirection === 'up') head.y--;
else if (playerDirection === 'down') head.y++;
else if (playerDirection === 'left') head.x--;
else if (playerDirection === 'right') head.x++;
head.x = (head.x + tileCount) % tileCount;
head.y = (head.y + tileCount) % tileCount;
playerSnake.unshift(head);
if (head.x === food.x && head.y === food.y) {
playerScore++;
roundCount++;
yaySound.play().catch(e => console.log('Yay sound playback failed:', e));
spawnFood();
} else {
playerSnake.pop();
}
}
function updateOpponentSnake() {
let head = { ...opponentSnake[0] };
let possibleDirections = ['up', 'down', 'left', 'right'];
if (difficulty === 'easy') {
if (Math.random() < 0.5) {
opponentDirection = possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
} else {
if (head.x < food.x && opponentDirection !== 'left') opponentDirection = 'right';
else if (head.x > food.x && opponentDirection !== 'right') opponentDirection = 'left';
else if (head.y < food.y && opponentDirection !== 'up') opponentDirection = 'down';
else if (head.y > food.y && opponentDirection !== 'down') opponentDirection = 'up';
}
} else if (difficulty === 'medium') {
if (head.x < food.x && opponentDirection !== 'left') opponentDirection = 'right';
else if (head.x > food.x && opponentDirection !== 'right') opponentDirection = 'left';
else if (head.y < food.y && opponentDirection !== 'up') opponentDirection = 'down';
else if (head.y > food.y && opponentDirection !== 'down') opponentDirection = 'up';
} else if (difficulty === 'difficult') {
let dx = food.x - head.x;
let dy = food.y - head.y;
let distances = [
{ dir: 'right', dist: Math.abs(dx - 1) + Math.abs(dy), valid: opponentDirection !== 'left' },
{ dir: 'left', dist: Math.abs(dx + 1) + Math.abs(dy), valid: opponentDirection !== 'right' },
{ dir: 'down', dist: Math.abs(dx) + Math.abs(dy - 1), valid: opponentDirection !== 'up' },
{ dir: 'up', dist: Math.abs(dx) + Math.abs(dy + 1), valid: opponentDirection !== 'down' }
];
distances = distances.filter(d => d.valid);
distances.sort((a, b) => a.dist - b.dist);
let nextDir = distances[0].dir;
let nextHead = { ...head };
if (nextDir === 'up') nextHead.y--;
else if (nextDir === 'down') nextHead.y++;
else if (nextDir === 'left') nextHead.x--;
else if (nextDir === 'right') nextHead.x++;
nextHead.x = (nextHead.x + tileCount) % tileCount;
nextHead.y = (nextHead.y + tileCount) % tileCount;
if (!playerSnake.some(segment => segment.x === nextHead.x && segment.y === nextHead.y)) {
opponentDirection = nextDir;
}
}
if (opponentDirection === 'up') head.y--;
else if (opponentDirection === 'down') head.y++;
else if (opponentDirection === 'left') head.x--;
else if (opponentDirection === 'right') head.x++;
head.x = (head.x + tileCount) % tileCount;
head.y = (head.y + tileCount) % tileCount;
opponentSnake.unshift(head);
if (head.x === food.x && head.y === food.y) {
opponentScore++;
roundCount++;
noSound.play().catch(e => console.log('No sound playback failed:', e));
spawnFood();
} else {
opponentSnake.pop();
}
}
function spawnFood() {
food.x = Math.floor(Math.random() * tileCount);
food.y = Math.floor(Math.random() * tileCount);
while (playerSnake.some(segment => segment.x === food.x && segment.y === food.y) ||
opponentSnake.some(segment => segment.x === food.x && segment.y === food.y)) {
food.x = Math.floor(Math.random() * tileCount);
food.y = Math.floor(Math.random() * tileCount);
}
if (roundCount >= maxRounds) {
let winner = playerScore > opponentScore ? 'Player Wins! (Max rounds reached)' :
playerScore < opponentScore ? 'Opponent Wins! (Max rounds reached)' :
'Tie! (Max rounds reached)';
endGame(winner);
}
}
function checkCollisions() {
let playerHead = playerSnake[0];
let opponentHead = opponentSnake[0];
// Check for head-to-head collision first
if (playerHead.x === opponentHead.x && playerHead.y === opponentHead.y) {
endGame('Tie! (Head-to-head)');
return;
}
// Check if player snake hits opponent snake's body
for (let i = 0; i < opponentSnake.length; i++) {
if (playerHead.x === opponentSnake[i].x && playerHead.y === opponentSnake[i].y) {
endGame('Opponent Wins! (Player hit opponent body)');
return;
}
}
// Check if opponent snake hits player snake's body
for (let i = 0; i < playerSnake.length; i++) {
if (opponentHead.x === playerSnake[i].x && opponentHead.y === playerSnake[i].y) {
endGame('Player Wins! (Opponent hit player body)');
return;
}
}
}
function endGame(winnerMessage) {
gameOver = true;
if (winnerMessage.includes('Player Wins')) {
finalPlayerScore += 1;
clapsSound.play().catch(e => console.log('Claps sound playback failed:', e));
} else if (winnerMessage.includes('Opponent Wins')) {
finalOpponentScore += 1;
opponentWinsSound.play().catch(e => console.log('Opponent wins sound playback failed:', e));
}
document.getElementById('cumulativeScore').textContent = `Score - Player: ${finalPlayerScore} | Opponent: ${finalOpponentScore}`;
document.getElementById('winner').textContent = winnerMessage;
document.getElementById('gameOver').style.display = 'block';
let countdown = 5;
document.getElementById('restartTimer').textContent = countdown;
const countdownInterval = setInterval(() => {
countdown--;
document.getElementById('restartTimer').textContent = countdown;
if (countdown <= 0) {
clearInterval(countdownInterval);
resetGame();
}
}, 1000);
}
function resetGame() {
playerSnake = [{ x: 5, y: 5 }];
playerDirection = 'right';
directionQueue = [];
playerScore = 0;
opponentSnake = [{ x: 15, y: 15 }];
opponentDirection = 'left';
opponentScore = 0;
roundCount = 0;
gameOver = false;
document.getElementById('gameOver').style.display = 'none';
startCountdown();
}
function drawGame() {
gameCtx.fillStyle = document.body.classList.contains('light-mode') ? '#f0f0f0' : '#333';
gameCtx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
const scale = canvasSize / (tileCount * gridSize);
// Player snake body (green circles)
gameCtx.fillStyle = 'green';
for (let i = 1; i < playerSnake.length; i++) {
let segment = playerSnake[i];
gameCtx.beginPath();
gameCtx.arc(
segment.x * gridSize * scale + (gridSize * scale) / 2,
segment.y * gridSize * scale + (gridSize * scale) / 2,
((gridSize - 2) * scale) / 2,
0,
2 * Math.PI
);
gameCtx.fill();
}
// Player snake head (square with eyes)
let playerHead = playerSnake[0];
gameCtx.fillStyle = 'green';
gameCtx.fillRect(
playerHead.x * gridSize * scale,
playerHead.y * gridSize * scale,
(gridSize - 2) * scale,
(gridSize - 2) * scale
);
gameCtx.fillStyle = 'white';
let eyeSize = (gridSize / 4) * scale;
let eyeOffset = (gridSize / 4) * scale;
let pupilSize = eyeSize / 2;
if (playerDirection === 'right') {
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, playerHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, playerHead.y * gridSize * scale + eyeOffset + pupilSize / 2, pupilSize, pupilSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, pupilSize, pupilSize);
} else if (playerDirection === 'left') {
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + eyeOffset + pupilSize / 2, pupilSize, pupilSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, pupilSize, pupilSize);
} else if (playerDirection === 'up') {
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, playerHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset + pupilSize / 2, playerHead.y * gridSize * scale + eyeOffset, pupilSize, pupilSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, playerHead.y * gridSize * scale + eyeOffset, pupilSize, pupilSize);
} else if (playerDirection === 'down') {
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(playerHead.x * gridSize * scale + eyeOffset + pupilSize / 2, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, pupilSize, pupilSize);
gameCtx.fillRect(playerHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, playerHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, pupilSize, pupilSize);
}
// Opponent snake body (blue circles)
gameCtx.fillStyle = 'blue';
for (let i = 1; i < opponentSnake.length; i++) {
let segment = opponentSnake[i];
gameCtx.beginPath();
gameCtx.arc(
segment.x * gridSize * scale + (gridSize * scale) / 2,
segment.y * gridSize * scale + (gridSize * scale) / 2,
((gridSize - 2) * scale) / 2,
0,
2 * Math.PI
);
gameCtx.fill();
}
// Opponent snake head (square with eyes)
let opponentHead = opponentSnake[0];
gameCtx.fillStyle = 'blue';
gameCtx.fillRect(
opponentHead.x * gridSize * scale,
opponentHead.y * gridSize * scale,
(gridSize - 2) * scale,
(gridSize - 2) * scale
);
gameCtx.fillStyle = 'white';
if (opponentDirection === 'right') {
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, opponentHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, opponentHead.y * gridSize * scale + eyeOffset + pupilSize / 2, pupilSize, pupilSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, pupilSize, pupilSize);
} else if (opponentDirection === 'left') {
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + eyeOffset + pupilSize / 2, pupilSize, pupilSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, pupilSize, pupilSize);
} else if (opponentDirection === 'up') {
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, opponentHead.y * gridSize * scale + eyeOffset, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset + pupilSize / 2, opponentHead.y * gridSize * scale + eyeOffset, pupilSize, pupilSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, opponentHead.y * gridSize * scale + eyeOffset, pupilSize, pupilSize);
} else if (opponentDirection === 'down') {
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - eyeSize, eyeSize, eyeSize);
gameCtx.fillStyle = 'black';
gameCtx.fillRect(opponentHead.x * gridSize * scale + eyeOffset + pupilSize / 2, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, pupilSize, pupilSize);
gameCtx.fillRect(opponentHead.x * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize / 2, opponentHead.y * gridSize * scale + (gridSize * scale) - eyeOffset - pupilSize, pupilSize, pupilSize);
}
// Food (red triangle)
gameCtx.fillStyle = 'red';
gameCtx.beginPath();
let centerX = food.x * gridSize * scale + (gridSize * scale) / 2;
let centerY = food.y * gridSize * scale + (gridSize * scale) / 2;
let size = (gridSize - 2) * scale;
gameCtx.moveTo(centerX, centerY - size / 2);
gameCtx.lineTo(centerX - size / 2, centerY + size / 2);
gameCtx.lineTo(centerX + size / 2, centerY + size / 2);
gameCtx.closePath();
gameCtx.fill();
document.getElementById('rounds').textContent = `Rounds: ${roundCount}/${maxRounds}`;
}
spawnFood();
startCountdown();
</script>
</body>
</html>