Save as game.html. Use any sprite sheet as images/hero.png with 4 frames per row (64 by 64). If you have no image yet, the game draws a coloured box until the image loads.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Coin Collector</title>
<style>
body { text-align: center; font-family: Arial, sans-serif; background: #222; color: #fff; }
canvas { background: #87ceeb; border: 3px solid #fff; }
</style>
</head>
<body>
<h1>Coin Collector</h1>
<p>Arrow keys or WASD to move. Collect coins, avoid the red enemies.</p>
<canvas id='game' width='640' height='360'></canvas>
<script>
(function () {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// ---------- Setup ----------
const sheet = new Image();
sheet.src = 'images/hero.png';
let sheetReady = false;
sheet.onload = () => { sheetReady = true; };
const FW = 64, FH = 64, FRAMES = 4, DELAY = 8;
let frame = 0, tick = 0;
const player = { x: 300, y: 160, w: 40, h: 40, speed: 4, row: 0 };
let coins = [], enemies = [];
let score = 0, lives = 3, over = false;
const keys = {};
// ---------- Input ----------
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code.startsWith('Arrow') || e.code === 'Space') e.preventDefault();
if (over && e.code === 'Enter') restart();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// ---------- Helpers ----------
const rand = (min, max) => Math.random() * (max - min) + min;
const hit = (a, b) =>
a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
function spawnCoin() {
coins.push({ x: rand(0, canvas.width - 20), y: rand(0, canvas.height - 20), w: 20, h: 20 });
}
function spawnEnemy() {
enemies.push({ x: canvas.width, y: rand(0, canvas.height - 30), w: 30, h: 30, speed: rand(1.5, 3.5) });
}
// ---------- Update ----------
function update() {
if (over) return;
let moving = false;
if (keys['ArrowLeft'] || keys['KeyA']) { player.x -= player.speed; player.row = 1; moving = true; }
if (keys['ArrowRight'] || keys['KeyD']) { player.x += player.speed; player.row = 0; moving = true; }
if (keys['ArrowUp'] || keys['KeyW']) { player.y -= player.speed; moving = true; }
if (keys['ArrowDown'] || keys['KeyS']) { player.y += player.speed; moving = true; }
player.x = Math.max(0, Math.min(canvas.width - player.w, player.x));
player.y = Math.max(0, Math.min(canvas.height - player.h, player.y));
// sprite animation
if (moving) {
if (++tick >= DELAY) { tick = 0; frame = (frame + 1) % FRAMES; }
} else { frame = 0; }
// coins: collect on collision
coins = coins.filter(c => {
if (hit(player, c)) { score += 10; return false; } // remove collected coin
return true;
});
// enemies: move left, hurt on collision, remove if off-screen
enemies.forEach(en => { en.x -= en.speed; });
enemies = enemies.filter(en => {
if (hit(player, en)) { lives--; if (lives <= 0) over = true; return false; }
return en.x + en.w > 0;
});
}
// ---------- Draw ----------
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'gold';
coins.forEach(c => {
ctx.beginPath();
ctx.arc(c.x + 10, c.y + 10, 10, 0, Math.PI * 2);
ctx.fill();
});
ctx.fillStyle = 'crimson';
enemies.forEach(en => ctx.fillRect(en.x, en.y, en.w, en.h));
if (sheetReady) {
ctx.drawImage(sheet, frame * FW, player.row * FH, FW, FH, player.x, player.y, player.w, player.h);
} else {
ctx.fillStyle = 'navy';
ctx.fillRect(player.x, player.y, player.w, player.h);
}
ctx.fillStyle = '#000';
ctx.font = '18px Arial';
ctx.fillText('Score: ' + score + ' Lives: ' + lives, 10, 24);
if (over) {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = '36px Arial';
ctx.fillText('GAME OVER Score ' + score, 150, 170);
ctx.font = '18px Arial';
ctx.fillText('Press Enter to play again', 220, 205);
}
}
// ---------- Loop and timers ----------
function loop() { update(); draw(); requestAnimationFrame(loop); }
setInterval(function () { if (!over && coins.length < 6) spawnCoin(); }, 1000);
setInterval(function () { if (!over) spawnEnemy(); }, 1500);
function restart() {
score = 0; lives = 3; over = false; coins = []; enemies = [];
player.x = 300; player.y = 160;
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Understand the code: setup (variables and image), input (keys object), helpers (rand, hit), update (movement, animation, collisions), draw, the loop, and timers. Notice how the whole game is wrapped in an anonymous IIFE and uses anonymous callbacks in filter, forEach and setInterval.
Game practical: assessment criteria
| Feature | Marks guide |
|---|---|
| Scenario implemented as specified (theme, goal, win/lose) | 20% |
| Sprite sheet animation (at least walk cycle with direction) | 20% |
| Keyboard control (smooth, multiple keys) | 15% |
| Collision detection for at least two object types | 20% |
| Score, lives or levels, game-over and restart | 10% |
| Code quality (functions, comments, naming) | 10% |
| Polish (sound, background, difficulty increase, high score) | 5% |
Extensions: shooting with a bullets array, background scrolling, levels with increasing speed, sound with new Audio('coin.wav').play(), saving a high score in localStorage (localStorage.setItem('best', score)), a start screen, and touch controls.
Check yourself. The questions for this part are on the 7.11 Review Questions page, with answers to check your work.