screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
body { display: flex; justify-content: center; align-items: center; flex-direction: column; }
canvas { display: block; border: 2px solid #333; image-rendering: pixelated; }
#ui { color: #fff; font-family: monospace; font-size: 14px; margin-top: 8px; text-align: center; }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="ui">Arrow Keys / WASD to move · Space / Up to jump · R to restart</div>
<script>
(function() {
"use strict";
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 800, H = 450;
// ── Constants ──
const GRAVITY = 0.55;
const PLAYER_SPEED = 3.5;
const JUMP_FORCE = -10.5;
const WORLD_W = 4800;
const GROUND_Y = H - 60;
const TILE = 30;
// ── Input ──
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// ── Level Data ──
// Platforms: {x, y, w, h}
const platforms = [];
// Ground segments (gaps create pits)
const groundSegs = [
{ x: 0, w: 1200 },
{ x: 1320, w: 900 },
{ x: 2340, w: 2460 },
];
// Raised platforms
const raisedPlatforms = [
{ x: 200, y: 300, w: 120, h: 16 },
{ x: 400, y: 240, w: 120, h: 16 },
{ x: 600, y: 200, w: 90, h: 16 },
{ x: 750, y: 260, w: 120, h: 16 },
{ x: 900, y: 310, w: 90, h: 16 },
{ x: 1050, y: 250, w: 120, h: 16 },
// After first pit
{ x: 1400, y: 300, w: 90, h: 16 },
{ x: 1550, y: 230, w: 120, h: 16 },
{ x: 1750, y: 180, w: 90, h: 16 },
{ x: 1900, y: 260, w: 150, h: 16 },
{ x: 2100, y: 300, w: 120, h: 16 },
// After second pit
{ x: 2450, y: 290, w: 120, h: 16 },
{ x: 2650, y: 220, w: 90, h: 16 },
{ x: 2800, y: 170, w: 120, h: 16 },
{ x: 3000, y: 250, w: 150, h: 16 },
{ x: 3200, y: 200, w: 90, h: 16 },
{ x: 3350, y: 280, w: 120, h: 16 },
// Staircase to flag
{ x: 3600, y: 310, w: 60, h: 16 },
{ x: 3660, y: 270, w: 60, h: 16 },
{ x: 3720, y: 230, w: 60, h: 16 },
{ x: 3780, y: 190, w: 60, h: 16 },
];
// Coins: {x, y, collected}
const coins = [
{ x: 240, y: 270 }, { x: 280, y: 270 },
{ x: 440, y: 210 }, { x: 480, y: 210 },
{ x: 630, y: 170 },
{ x: 790, y: 230 }, { x: 830, y: 230 },
{ x: 930, y: 280 },
{ x: 1090, y: 220 }, { x: 1130, y: 220 },
// Above ground before pit
{ x: 300, y: GROUND_Y - 40 }, { x: 340, y: GROUND_Y - 40 },
{ x: 500, y: GROUND_Y - 40 }, { x: 540, y: GROUND_Y - 40 },
{ x: 700, y: GROUND_Y - 40 }, { x: 740, y: GROUND_Y - 40 },
// After first pit
{ x: 1440, y: 270 },
{ x: 1590, y: 200 }, { x: 1630, y: 200 },
{ x: 1780, y: 150 },
{ x: 1940, y: 230 }, { x: 1980, y: 230 },
{ x: 2140, y: 270 },
// Ground coins
{ x: 1500, y: GROUND_Y - 40 }, { x: 1540, y: GROUND_Y - 40 },
{ x: 1700, y: GROUND_Y - 40 }, { x: 1740, y: GROUND_Y - 40 },
// After second pit
{ x: 2490, y: 260 }, { x: 2530, y: 260 },
{ x: 2680, y: 190 },
{ x: 2840, y: 140 }, { x: 2880, y: 140 },
{ x: 3040, y: 220 }, { x: 3080, y: 220 },
{ x: 3230, y: 170 },
{ x: 3390, y: 250 }, { x: 3430, y: 250 },
// Ground coins
{ x: 2500, y: GROUND_Y - 40 }, { x: 2540, y: GROUND_Y - 40 },
{ x: 2700, y: GROUND_Y - 40 }, { x: 2740, y: GROUND_Y - 40 },
{ x: 2900, y: GROUND_Y - 40 }, { x: 2940, y: GROUND_Y - 40 },
{ x: 3100, y: GROUND_Y - 40 }, { x: 3140, y: GROUND_Y - 40 },
// Staircase coins
{ x: 3620, y: 275 }, { x: 3680, y: 235 }, { x: 3740, y: 195 },
];
// Enemies: {x, y, w, h, vx, alive, type}
const enemies = [
{ x: 500, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.2, alive: true, type: 'goomba' },
{ x: 850, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
{ x: 1100, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.5, alive: true, type: 'goomba' },
{ x: 1600, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.3, alive: true, type: 'goomba' },
{ x: 1950, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
{ x: 2600, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.2, alive: true, type: 'goomba' },
{ x: 2850, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.5, alive: true, type: 'goomba' },
{ x: 3100, y: GROUND_Y - 24, w: 24, h: 24, vx: -1.0, alive: true, type: 'goomba' },
{ x: 3400, y: GROUND_Y - 24, w: 24, h: 24, vx: 1.3, alive: true, type: 'goomba' },
// Platform enemies
{ x: 440, y: 216, w: 24, h: 24, vx: -0.8, alive: true, type: 'goomba' },
{ x: 1590, y: 206, w: 24, h: 24, vx: -0.8, alive: true, type: 'goomba' },
{ x: 3040, y: 226, w: 24, h: 24, vx: 0.8, alive: true, type: 'goomba' },
];
// Flag goal
const flag = { x: 4000, y: GROUND_Y - 120, w: 16, h: 120 };
// Particles
let particles = [];
// ── Player ──
let player, camera, score, lives, gameState, winTimer, deathTimer;
function initGame() {
player = {
x: 80, y: GROUND_Y - 36, w: 20, h: 32,
vx: 0, vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
animFrame: 0,
animTimer: 0,
invincible: 0,
};
camera = { x: 0 };
score = 0;
lives = 3;
gameState = 'playing'; // 'playing', 'win', 'dead', 'gameover'
winTimer = 0;
deathTimer = 0;
particles = [];
coins.forEach(c => c.collected = false);
enemies.forEach(e => { e.alive = true; e.deadTimer = 0; });
}
// ── Collision helpers ──
function rectOverlap(a, b) {
return 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 getGroundYAt(x) {
for (const seg of groundSegs) {
if (x >= seg.x && x < seg.x + seg.w) return GROUND_Y;
}
return null; // no ground (pit)
}
function getAllSolids() {
const solids = [];
// Ground segments
for (const seg of groundSegs) {
solids.push({ x: seg.x, y: GROUND_Y, w: seg.w, h: 60, type: 'ground' });
}
// Raised platforms
for (const p of raisedPlatforms) {
solids.push({ x: p.x, y: p.y, w: p.w, h: p.h, type: 'platform' });
}
return solids;
}
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 4 - 1,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3,
});
}
}
// ── Update ──
function update() {
if (gameState === 'win') {
winTimer++;
updateParticles();
return;
}
if (gameState === 'dead') {
deathTimer++;
player.vy += GRAVITY;
player.y += player.vy;
updateParticles();
if (deathTimer > 90) {
if (lives <= 0) {
gameState = 'gameover';
} else {
initGame();
}
}
return;
}
if (gameState === 'gameover') {
if (keys['KeyR']) initGame();
return;
}
// ── Player movement ──
const moveLeft = keys['ArrowLeft'] || keys['KeyA'];
const moveRight = keys['ArrowRight'] || keys['KeyD'];
const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
if (moveLeft) { player.vx = -PLAYER_SPEED; player.facing = -1; }
else if (moveRight) { player.vx = PLAYER_SPEED; player.facing = 1; }
else { player.vx *= 0.75; if (Math.abs(player.vx) < 0.1) player.vx = 0; }
// Jump
if (jumpKey && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
// Variable jump height
if (!jumpKey && player.vy < -3) {
player.vy = -3;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 12) player.vy = 12;
// Animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.animTimer++;
if (player.animTimer > 6) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
} else if (player.onGround) {
player.animFrame = 0;
}
// Invincibility timer
if (player.invincible > 0) player.invincible--;
// ── Horizontal movement & collision ──
player.x += player.vx;
const solids = getAllSolids();
for (const s of solids) {
if (rectOverlap(player, s)) {
if (player.vx > 0) player.x = s.x - player.w;
else if (player.vx < 0) player.x = s.x + s.w;
player.vx = 0;
}
}
// ── Vertical movement & collision ──
player.y += player.vy;
player.onGround = false;
for (const s of solids) {
if (rectOverlap(player, s)) {
if (player.vy > 0) {
player.y = s.y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = s.y + s.h;
player.vy = 0;
}
}
}
// Clamp to world
if (player.x < 0) player.x = 0;
if (player.x + player.w > WORLD_W) player.x = WORLD_W - player.w;
// Fall into pit
if (player.y > H + 50) {
loseLife();
return;
}
// ── Coin collection ──
for (const c of coins) {
if (c.collected) continue;
const coinRect = { x: c.x - 8, y: c.y - 8, w: 16, h: 16 };
if (rectOverlap(player, coinRect)) {
c.collected = true;
score += 100;
spawnParticles(c.x, c.y, '#FFD700', 8);
}
}
// ── Enemy update ──
for (const e of enemies) {
if (!e.alive) {
if (e.deadTimer !== undefined) e.deadTimer++;
continue;
}
e.x += e.vx;
// Simple patrol: reverse at edges or walls
const groundBelow = getGroundYAt(e.x);
if (groundBelow === null && e.y >= GROUND_Y - 30) {
e.vx *= -1; // reverse at pit edge
}
// Reverse at solid walls
for (const s of solids) {
if (rectOverlap(e, s)) {
e.vx *= -1;
e.x += e.vx * 2;
}
}
// Player-enemy collision
if (player.invincible <= 0 && rectOverlap(player, e)) {
// Stomp from above
if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
e.alive = false;
e.deadTimer = 30;
player.vy = -7;
score += 200;
spawnParticles(e.x + e.w/2, e.y + e.h/2, '#8B4513', 6);
} else {
// Hit by enemy
loseLife();
return;
}
}
}
// ── Flag (win) ──
const flagRect = { x: flag.x - 10, y: flag.y, w: flag.w + 20, h: flag.h };
if (rectOverlap(player, flagRect)) {
gameState = 'win';
score += 1000;
spawnParticles(flag.x, flag.y + 20, '#FFD700', 20);
spawnParticles(flag.x, flag.y + 20, '#FF4500', 15);
}
// ── Camera ──
const targetCam = player.x - W / 3;
camera.x += (targetCam - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
if (camera.x > WORLD_W - W) camera.x = WORLD_W - W;
updateParticles();
}
function loseLife() {
lives--;
gameState = 'dead';
deathTimer = 0;
player.vy = -8;
spawnParticles(player.x + player.w/2, player.y + player.h/2, '#FF0000', 10);
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
// ── Drawing ──
function draw() {
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#5C94FC');
skyGrad.addColorStop(0.6, '#87CEEB');
skyGrad.addColorStop(1, '#B0E0FF');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Background clouds (parallax)
drawClouds();
// Background hills (parallax)
drawHills();
ctx.save();
ctx.translate(-camera.x, 0);
// Draw ground
drawGround();
// Draw raised platforms
drawPlatforms();
// Draw coins
drawCoins();
// Draw enemies
drawEnemies();
// Draw flag
drawFlag();
// Draw particles
drawParticles();
// Draw player
if (gameState !== 'dead') {
drawPlayer();
} else if (deathTimer < 60) {
drawPlayer();
}
ctx.restore();
// HUD
drawHUD();
// Win/Game Over overlays
if (gameState === 'win') drawWinScreen();
if (gameState === 'gameover') drawGameOverScreen();
}
function drawClouds() {
const cloudOffset = camera.x * 0.2;
ctx.fillStyle = 'rgba(255,255,255,0.8)';
const cloudPositions = [
{ x: 100, y: 50, s: 1.2 }, { x: 400, y: 80, s: 0.8 },
{ x: 700, y: 40, s: 1.0 }, { x: 1100, y: 65, s: 1.3 },
{ x: 1500, y: 55, s: 0.9 }, { x: 2000, y: 75, s: 1.1 },
{ x: 2500, y: 45, s: 1.0 }, { x: 3000, y: 70, s: 0.8 },
{ x: 3500, y: 50, s: 1.2 }, { x: 4200, y: 60, s: 0.9 },
];
for (const c of cloudPositions) {
const cx = c.x - cloudOffset % 5000;
// Wrap
let wx = cx;
if (wx < -100) wx += 5000;
if (wx > W + 100) wx -= 5000;
drawCloud(wx, c.y, c.s);
}
}
function drawCloud(x, y, scale) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.arc(20, -5, 15, 0, Math.PI * 2);
ctx.arc(35, 0, 18, 0, Math.PI * 2);
ctx.arc(15, 5, 16, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHills() {
const hillOffset = camera.x * 0.35;
ctx.fillStyle = '#4AA52E';
const hillPositions = [
{ x: 50, r: 80 }, { x: 350, r: 50 }, { x: 650, r: 90 },
{ x: 1000, r: 60 }, { x: 1400, r: 75 }, { x: 1800, r: 55 },
{ x: 2200, r: 85 }, { x: 2700, r: 65 }, { x: 3200, r: 70 },
{ x: 3700, r: 80 }, { x: 4300, r: 55 },
];
for (const h of hillPositions) {
let hx = h.x - hillOffset % 5000;
if (hx < -200) hx += 5000;
if (hx > W + 200) hx -= 5000;
ctx.beginPath();
ctx.arc(hx, GROUND_Y, h.r, Math.PI, 0);
ctx.fill();
}
}
function drawGround() {
for (const seg of groundSegs) {
// Top dirt layer
ctx.fillStyle = '#8B5E3C';
ctx.fillRect(seg.x, GROUND_Y, seg.w, 60);
// Grass top
ctx.fillStyle = '#4CAF50';
ctx.fillRect(seg.x, GROUND_Y, seg.w, 8);
ctx.fillStyle = '#66BB6A';
ctx.fillRect(seg.x, GROUND_Y, seg.w, 4);
// Dirt pattern
ctx.fillStyle = '#7A5230';
for (let gx = seg.x; gx < seg.x + seg.w; gx += 20) {
for (let gy = GROUND_Y + 12; gy < GROUND_Y + 56; gy += 14) {
ctx.fillRect(gx + ((gy % 28 === 0) ? 10 : 0), gy, 10, 6);
}
}
}
}
function drawPlatforms() {
for (const p of raisedPlatforms) {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(p.x + 2, p.y + 2, p.w, p.h);
// Block body
ctx.fillStyle = '#C8703A';
ctx.fillRect(p.x, p.y, p.w, p.h);
// Top highlight
ctx.fillStyle = '#E8A060';
ctx.fillRect(p.x, p.y, p.w, 4);
// Brick lines
ctx.strokeStyle = '#8B5E3C';
ctx.lineWidth = 1;
for (let bx = p.x + 15; bx < p.x + p.w; bx += 15) {
ctx.beginPath();
ctx.moveTo(bx, p.y);
ctx.lineTo(bx, p.y + p.h);
ctx.stroke();
}
// Border
ctx.strokeStyle = '#6B3A1F';
ctx.lineWidth = 1;
ctx.strokeRect(p.x, p.y, p.w, p.h);
}
}
function drawCoins() {
const t = Date.now() / 300;
for (const c of coins) {
if (c.collected) continue;
const bob = Math.sin(t + c.x * 0.01) * 3;
const cy = c.y + bob;
// Glow
ctx.fillStyle = 'rgba(255,215,0,0.3)';
ctx.beginPath();
ctx.arc(c.x, cy, 12, 0, Math.PI * 2);
ctx.fill();
// Coin body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(c.x, cy, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#DAA520';
ctx.lineWidth = 1.5;
ctx.stroke();
// Shine
ctx.fillStyle = '#FFF8DC';
ctx.beginPath();
ctx.arc(c.x - 2, cy - 2, 2.5, 0, Math.PI * 2);
ctx.fill();
}
}
function drawEnemies() {
for (const e of enemies) {
if (!e.alive && (e.deadTimer === undefined || e.deadTimer > 30)) continue;
ctx.save();
if (!e.alive) {
// Squished
ctx.translate(e.x + e.w/2, e.y + e.h);
ctx.scale(1, 0.3);
ctx.translate(-(e.x + e.w/2), -(e.y + e.h));
ctx.globalAlpha = 1 - e.deadTimer / 30;
}
// Body
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.ellipse(e.x + e.w/2, e.y + e.h/2 + 2, e.w/2, e.h/2 - 2, 0, 0, Math.PI * 2);
ctx.fill();
// Feet
ctx.fillStyle = '#3E1F0D';
const footWiggle = Math.sin(Date.now() / 150) * 2;
ctx.fillRect(e.x + 2, e.y + e.h - 6 + footWiggle, 7, 6);
ctx.fillRect(e.x + e.w - 9, e.y + e.h - 6 - footWiggle, 7, 6);
// Eyes
ctx.fillStyle = '#FFF';
ctx.beginPath();
ctx.arc(e.x + 7, e.y + 10, 4, 0, Math.PI * 2);
ctx.arc(e.x + e.w - 7, e.y + 10, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(e.x + 8, e.y + 10, 2, 0, Math.PI * 2);
ctx.arc(e.x + e.w - 6, e.y + 10, 2, 0, Math.PI * 2);
ctx.fill();
// Eyebrows (angry)
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(e.x + 4, e.y + 6);
ctx.lineTo(e.x + 10, e.y + 8);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.x + e.w - 4, e.y + 6);
ctx.lineTo(e.x + e.w - 10, e.y + 8);
ctx.stroke();
ctx.restore();
}
}
function drawFlag() {
// Pole
ctx.fillStyle = '#888';
ctx.fillRect(flag.x + 6, flag.y, 4, flag.h);
// Ball on top
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(flag.x + 8, flag.y, 6, 0, Math.PI * 2);
ctx.fill();
// Flag
const wave = Math.sin(Date.now() / 200) * 3;
ctx.fillStyle = '#FF2200';
ctx.beginPath();
ctx.moveTo(flag.x + 10, flag.y + 5);
ctx.lineTo(flag.x + 40 + wave, flag.y + 15);
ctx.lineTo(flag.x + 10, flag.y + 30);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = '#FFD700';
ctx.font = '12px monospace';
ctx.fillText('★', flag.x + 18, flag.y + 23);
// Base
ctx.fillStyle = '#666';
ctx.fillRect(flag.x, flag.y + flag.h - 8, 16, 8);
}
function drawPlayer() {
if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;
const px = player.x;
const py = player.y;
const f = player.facing;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
ctx.scale(f, 1);
ctx.translate(-player.w / 2, -player.h / 2);
// Hat
ctx.fillStyle = '#E01010';
ctx.fillRect(2, 0, 16, 8);
ctx.fillRect(0, 4, 20, 5);
// Face
ctx.fillStyle = '#FFB366';
ctx.fillRect(2, 9, 16, 10);
// Eye
ctx.fillStyle = '#000';
ctx.fillRect(12, 11, 3, 3);
// Mustache
ctx.fillStyle = '#4A2800';
ctx.fillRect(8, 15, 10, 2);
// Body (overalls)
ctx.fillStyle = '#0050D0';
ctx.fillRect(2, 19, 16, 8);
// Shirt
ctx.fillStyle = '#E01010';
ctx.fillRect(0, 19, 4, 6);
ctx.fillRect(16, 19, 4, 6);
// Legs
const legOffset = player.onGround ? Math.sin(player.animFrame * Math.PI / 2) * 3 : 0;
ctx.fillStyle = '#0050D0';
ctx.fillRect(2, 27, 7, 5 + legOffset);
ctx.fillRect(11, 27, 7, 5 - legOffset);
// Shoes
ctx.fillStyle = '#6B3A1F';
ctx.fillRect(0, 30 + legOffset, 9, 3);
ctx.fillRect(11, 30 - legOffset, 9, 3);
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score background
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(10, 8, 180, 32);
ctx.strokeStyle = '#FFF';
ctx.lineWidth = 1;
ctx.strokeRect(10, 8, 180, 32);
// Score
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 16px monospace';
ctx.fillText('SCORE: ' + score.toString().padStart(6, '0'), 20, 28);
// Lives
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(W - 100, 8, 90, 32);
ctx.strokeStyle = '#FFF';
ctx.strokeRect(W - 100, 8, 90, 32);
ctx.fillStyle = '#FF4444';
ctx.font = 'bold 16px monospace';
ctx.fillText('LIVES: ' + lives, W - 90, 28);
// Coins collected
const collected = coins.filter(c => c.collected).length;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(W/2 - 60, 8, 120, 32);
ctx.strokeStyle = '#FFF';
ctx.strokeRect(W/2 - 60, 8, 120, 32);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 16px monospace';
ctx.fillText('COINS: ' + collected + '/' + coins.length, W/2 - 50, 28);
}
function drawWinScreen() {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.fillText('YOU WIN!', W/2, H/2 - 30);
ctx.fillStyle = '#FFF';
ctx.font = '24px monospace';
ctx.fillText('Score: ' + score, W/2, H/2 + 20);
const collected = coins.filter(c => c.collected).length;
ctx.fillText('Coins: ' + collected + '/' + coins.length, W/2, H/2 + 55);
ctx.fillStyle = '#AAA';
ctx.font = '16px monospace';
ctx.fillText('Press R to play again', W/2, H/2 + 95);
ctx.textAlign = 'left';
if (keys['KeyR']) initGame();
}
function drawGameOverScreen() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#FF4444';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', W/2, H/2 - 20);
ctx.fillStyle = '#FFF';
ctx.font = '24px monospace';
ctx.fillText('Final Score: ' + score, W/2, H/2 + 25);
ctx.fillStyle = '#AAA';
ctx.font = '16px monospace';
ctx.fillText('Press R to try again', W/2, H/2 + 65);
ctx.textAlign = 'left';
if (keys['KeyR']) initGame();
}
// ── Game Loop ──
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
initGame();
gameLoop();
})();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Canvas Bros</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: monospace;
overflow: hidden;
}
#game {
border: 3px solid #16213e;
border-radius: 4px;
image-rendering: pixelated;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = 800, H = 450;
// ─── Constants ───────────────────────────────────────────────
const GRAVITY = 0.55;
const PLAYER_SPEED = 3.5;
const PLAYER_RUN_SPEED = 5.0;
const JUMP_FORCE = -11.5;
const ENEMY_SPEED = 1.2;
const WORLD_WIDTH = 4800;
const GROUND_Y = H - 60;
// ─── Input ───────────────────────────────────────────────────
const keys = {};
window.addEventListener("keydown", e => {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
// ─── Game State ──────────────────────────────────────────────
let state = "playing"; // "playing", "won", "dead", "title"
let score = 0;
let coins = 0;
let lives = 3;
let camera = { x: 0 };
let frameCount = 0;
let deathTimer = 0;
let winTimer = 0;
let particles = [];
let floatingTexts = [];
// ─── Level Data ──────────────────────────────────────────────
// Platforms: {x, y, w, h, type}
// type: "ground", "brick", "question", "pipe", "cloud"
let platforms = [];
let coinItems = [];
let enemies = [];
let flagPole = null;
function buildLevel() {
platforms = [];
coinItems = [];
enemies = [];
// Ground segments with gaps
addGround(0, 800);
addGround(900, 1600);
addGround(1750, 3000);
addGround(3150, WORLD_WIDTH);
// ── Section 1: Intro platforms ──
addPlatform(250, GROUND_Y - 80, 48, 48, "question");
addPlatform(350, GROUND_Y - 80, 48, 48, "brick");
addPlatform(400, GROUND_Y - 80, 48, 48, "question");
addPlatform(450, GROUND_Y - 80, 48, 48, "brick");
addPlatform(350, GROUND_Y - 170, 48, 48, "question");
addPlatform(600, GROUND_Y - 60, 64, 64, "pipe");
// Coins in the air
addCoin(270, GROUND_Y - 140);
addCoin(420, GROUND_Y - 140);
addCoin(370, GROUND_Y - 220);
// Enemies
addEnemy(500, GROUND_Y - 24, "goomba");
addEnemy(700, GROUND_Y - 24, "goomba");
// ── Section 2: After first gap ──
addPlatform(950, GROUND_Y - 70, 48, 48, "brick");
addPlatform(1000, GROUND_Y - 70, 48, 48, "question");
addPlatform(1050, GROUND_Y - 70, 48, 48, "brick");
addPlatform(1150, GROUND_Y - 130, 48, 48, "brick");
addPlatform(1200, GROUND_Y - 130, 48, 48, "question");
addPlatform(1250, GROUND_Y - 130, 48, 48, "brick");
addPlatform(1150, GROUND_Y - 210, 48, 48, "question");
addPlatform(1350, GROUND_Y - 60, 64, 64, "pipe");
addCoin(1020, GROUND_Y - 130);
addCoin(1220, GROUND_Y - 190);
addCoin(1170, GROUND_Y - 270);
addEnemy(1050, GROUND_Y - 24, "goomba");
addEnemy(1400, GROUND_Y - 24, "goomba");
// ── Section 3: Staircase ──
for (let i = 0; i < 5; i++) {
addPlatform(1800 + i * 48, GROUND_Y - (i + 1) * 48, 48, 48, "brick");
}
for (let i = 0; i < 4; i++) {
addPlatform(2000 + i * 48, GROUND_Y - (5 - i) * 48, 48, 48, "brick");
}
addCoin(1850, GROUND_Y - 130);
addCoin(1900, GROUND_Y - 180);
addCoin(1950, GROUND_Y - 230);
addCoin(2000, GROUND_Y - 260);
addCoin(2050, GROUND_Y - 210);
addEnemy(1900, GROUND_Y - 24, "goomba");
addEnemy(2200, GROUND_Y - 24, "goomba");
// ── Section 4: Floating platforms over gap ──
addPlatform(2300, GROUND_Y - 60, 80, 20, "ground");
addPlatform(2450, GROUND_Y - 110, 80, 20, "ground");
addPlatform(2600, GROUND_Y - 160, 80, 20, "ground");
addCoin(2340, GROUND_Y - 110);
addCoin(2490, GROUND_Y - 170);
addCoin(2640, GROUND_Y - 220);
// ── Section 5: Pipe gauntlet ──
addPlatform(2800, GROUND_Y - 60, 64, 64, "pipe");
addPlatform(2950, GROUND_Y - 80, 64, 80, "pipe");
addPlatform(3100, GROUND_Y - 60, 64, 64, "pipe");
addCoin(2832, GROUND_Y - 110);
addCoin(2982, GROUND_Y - 140);
addCoin(3132, GROUND_Y - 110);
addEnemy(2700, GROUND_Y - 24, "goomba");
addEnemy(2850, GROUND_Y - 24, "goomba");
addEnemy(3000, GROUND_Y - 24, "goomba");
// ── Section 6: Final staircase to flag ──
for (let i = 0; i < 8; i++) {
addPlatform(3400 + i * 48, GROUND_Y - (i + 1) * 48, 48, 48, "brick");
}
addCoin(3450, GROUND_Y - 130);
addCoin(3500, GROUND_Y - 180);
addCoin(3550, GROUND_Y - 230);
addCoin(3600, GROUND_Y - 280);
addCoin(3650, GROUND_Y - 330);
addCoin(3700, GROUND_Y - 380);
addEnemy(3400, GROUND_Y - 24, "goomba");
addEnemy(3550, GROUND_Y - 24, "goomba");
// Flag pole at end
flagPole = { x: 4000, y: GROUND_Y - 320, w: 8, h: 320 };
}
function addGround(x, x2) {
platforms.push({ x, y: GROUND_Y, w: x2 - x, h: H - GROUND_Y, type: "ground" });
}
function addPlatform(x, y, w, h, type) {
platforms.push({ x, y, w, h, type, hit: false });
}
function addCoin(x, y) {
coinItems.push({ x, y, w: 16, h: 16, collected: false, bobOffset: Math.random() * Math.PI * 2 });
}
function addEnemy(x, y, type) {
enemies.push({
x, y, w: 28, h: 24, type,
vx: -ENEMY_SPEED,
alive: true,
squishTimer: 0,
startX: x,
patrolDist: 120
});
}
// ─── Player ──────────────────────────────────────────────────
let player = {};
function resetPlayer() {
player = {
x: 80, y: GROUND_Y - 40,
w: 24, h: 32,
vx: 0, vy: 0,
onGround: false,
facing: 1,
animFrame: 0,
animTimer: 0,
invincible: 0
};
}
// ─── Collision ───────────────────────────────────────────────
function aabb(a, b) {
return 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 resolveCollisions() {
player.onGround = false;
for (const p of platforms) {
if (!aabb(player, p)) continue;
const overlapLeft = (player.x + player.w) - p.x;
const overlapRight = (p.x + p.w) - player.x;
const overlapTop = (player.y + player.h) - p.y;
const overlapBottom = (p.y + p.h) - player.y;
const minOverlapX = Math.min(overlapLeft, overlapRight);
const minOverlapY = Math.min(overlapTop, overlapBottom);
if (minOverlapY < minOverlapX) {
if (overlapTop < overlapBottom) {
// Landing on top
player.y = p.y - player.h;
player.vy = 0;
player.onGround = true;
} else {
// Hitting from below
player.y = p.y + p.h;
player.vy = 0.5;
if (p.type === "question" && !p.hit) {
p.hit = true;
score += 100;
spawnParticles(p.x + p.w/2, p.y, "#FFD700", 6);
addFloatingText("+100", p.x, p.y - 20, "#FFD700");
}
if (p.type === "brick" && !p.hit) {
p.hit = true;
score += 50;
spawnParticles(p.x + p.w/2, p.y + p.h/2, "#C84C09", 8);
addFloatingText("+50", p.x, p.y - 20, "#FFD700");
}
}
} else {
if (overlapLeft < overlapRight) {
player.x = p.x - player.w;
} else {
player.x = p.x + p.w;
}
player.vx = 0;
}
}
}
// ─── Particles ───────────────────────────────────────────────
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 6,
vy: -Math.random() * 5 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3
});
}
}
function addFloatingText(text, x, y, color) {
floatingTexts.push({ text, x, y, life: 45, color });
}
// ─── Update ──────────────────────────────────────────────────
function update() {
frameCount++;
if (state === "title") {
if (keys["Space"] || keys["Enter"]) {
state = "playing";
keys["Space"] = false;
keys["Enter"] = false;
}
return;
}
if (state === "dead") {
deathTimer--;
if (deathTimer <= 0) {
if (lives <= 0) {
state = "title";
score = 0;
coins = 0;
lives = 3;
} else {
resetPlayer();
camera.x = 0;
state = "playing";
}
}
return;
}
if (state === "won") {
winTimer++;
return;
}
// ── Player movement ──
const moveLeft = keys["ArrowLeft"] || keys["KeyA"];
const moveRight = keys["ArrowRight"] || keys["KeyD"];
const jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
const running = keys["ShiftLeft"] || keys["ShiftRight"];
const speed = running ? PLAYER_RUN_SPEED : PLAYER_SPEED;
if (moveLeft) { player.vx = -speed; player.facing = -1; }
else if (moveRight) { player.vx = speed; player.facing = 1; }
else { player.vx *= 0.7; if (Math.abs(player.vx) < 0.1) player.vx = 0; }
if (jumpKey && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
// Variable jump height
if (!jumpKey && player.vy < -3) {
player.vy *= 0.85;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 14) player.vy = 14;
// Move X
player.x += player.vx;
resolveCollisions();
// Move Y
player.y += player.vy;
resolveCollisions();
// Clamp player
if (player.x < 0) player.x = 0;
if (player.x > WORLD_WIDTH - player.w) player.x = WORLD_WIDTH - player.w;
// Fall death
if (player.y > H + 50) {
playerDie();
return;
}
// Animation
if (Math.abs(player.vx) > 0.5 && player.onGround) {
player.animTimer += Math.abs(player.vx);
if (player.animTimer > 8) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 3;
}
} else {
player.animFrame = 0;
}
// Invincibility timer
if (player.invincible > 0) player.invincible--;
// ── Camera ──
const targetCam = player.x - W * 0.35;
camera.x += (targetCam - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
if (camera.x > WORLD_WIDTH - W) camera.x = WORLD_WIDTH - W;
// ── Coins ──
for (const c of coinItems) {
if (c.collected) continue;
const coinBox = { x: c.x - 8, y: c.y - 8 + Math.sin(frameCount * 0.06 + c.bobOffset) * 4, w: 16, h: 16 };
if (aabb(player, coinBox)) {
c.collected = true;
coins++;
score += 50;
spawnParticles(c.x, c.y, "#FFD700", 8);
addFloatingText("+50", c.x - 10, c.y - 20, "#FFD700");
}
}
// ── Enemies ──
for (const e of enemies) {
if (!e.alive) {
if (e.squishTimer > 0) e.squishTimer--;
continue;
}
e.x += e.vx;
// Patrol
if (e.x < e.startX - e.patrolDist || e.x > e.startX + e.patrolDist) {
e.vx *= -1;
}
// Platform check for enemies
let onPlatform = false;
for (const p of platforms) {
if (e.x + e.w > p.x && e.x < p.x + p.w &&
e.y + e.h >= p.y && e.y + e.h <= p.y + 8) {
onPlatform = true;
break;
}
}
if (!onPlatform && e.y > GROUND_Y - 10) {
e.vx *= -1;
}
// Player collision
if (player.invincible <= 0 && aabb(player, e)) {
// Stomp from above
if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
e.alive = false;
e.squishTimer = 30;
player.vy = -8;
score += 200;
spawnParticles(e.x + e.w/2, e.y, "#8B4513", 6);
addFloatingText("+200", e.x, e.y - 20, "#FFD700");
} else {
playerHit();
}
}
}
// ── Flag pole ──
if (flagPole && aabb(player, { x: flagPole.x - 10, y: flagPole.y, w: flagPole.w + 20, h: flagPole.h })) {
state = "won";
winTimer = 0;
score += 1000;
spawnParticles(flagPole.x, flagPole.y, "#FFD700", 20);
spawnParticles(flagPole.x, flagPole.y + 40, "#FF6347", 15);
}
// ── Update particles ──
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
// ── Update floating texts ──
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y -= 1;
ft.life--;
if (ft.life <= 0) floatingTexts.splice(i, 1);
}
}
function playerHit() {
if (player.invincible > 0) return;
lives--;
player.invincible = 90;
player.vy = -8;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 10);
if (lives <= 0) {
playerDie();
}
}
function playerDie() {
state = "dead";
deathTimer = 60;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 15);
}
// ─── Drawing ─────────────────────────────────────────────────
function draw() {
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, "#5c94fc");
skyGrad.addColorStop(0.7, "#87CEEB");
skyGrad.addColorStop(1, "#b8d8f0");
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
drawClouds();
// Hills (parallax)
drawHills();
ctx.save();
ctx.translate(-camera.x, 0);
// Platforms
for (const p of platforms) {
if (p.x + p.w < camera.x - 50 || p.x > camera.x + W + 50) continue;
drawPlatform(p);
}
// Coins
for (const c of coinItems) {
if (c.collected) continue;
if (c.x < camera.x - 50 || c.x > camera.x + W + 50) continue;
drawCoin(c);
}
// Enemies
for (const e of enemies) {
if (e.x < camera.x - 100 || e.x > camera.x + W + 100) continue;
drawEnemy(e);
}
// Flag pole
if (flagPole) {
drawFlagPole(flagPole);
}
// Player
if (state === "playing" || state === "won") {
if (player.invincible <= 0 || Math.floor(player.invincible / 4) % 2 === 0) {
drawPlayer();
}
}
// Particles
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
// Floating texts
for (const ft of floatingTexts) {
const alpha = ft.life / 45;
ctx.globalAlpha = alpha;
ctx.fillStyle = ft.color;
ctx.font = "bold 14px monospace";
ctx.textAlign = "center";
ctx.fillText(ft.text, ft.x, ft.y);
}
ctx.globalAlpha = 1;
ctx.restore();
// HUD
drawHUD();
// Overlays
if (state === "title") drawTitleScreen();
if (state === "won") drawWinScreen();
if (state === "dead") drawDeathScreen();
}
function drawClouds() {
ctx.fillStyle = "rgba(255,255,255,0.8)";
const cloudPositions = [
{ x: 100, y: 60, s: 1.2 },
{ x: 400, y: 40, s: 0.9 },
{ x: 700, y: 70, s: 1.0 },
{ x: 1100, y: 50, s: 1.3 },
{ x: 1500, y: 35, s: 0.8 },
{ x: 2000, y: 65, s: 1.1 },
{ x: 2500, y: 45, s: 1.0 },
{ x: 3000, y: 55, s: 1.4 },
{ x: 3500, y: 40, s: 0.9 },
{ x: 4000, y: 60, s: 1.2 },
];
for (const c of cloudPositions) {
const cx = c.x - camera.x * 0.3;
// Wrap clouds
const wrappedX = ((cx % (W + 200)) + W + 200) % (W + 200) - 100;
const s = c.s;
ctx.beginPath();
ctx.ellipse(wrappedX, c.y, 40 * s, 18 * s, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(wrappedX - 25 * s, c.y + 5 * s, 28 * s, 14 * s, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(wrappedX + 25 * s, c.y + 3 * s, 30 * s, 15 * s, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawHills() {
ctx.fillStyle = "#4a8c3f";
const hillPositions = [
{ x: 200, w: 200, h: 80 },
{ x: 800, w: 250, h: 100 },
{ x: 1600, w: 180, h: 70 },
{ x: 2400, w: 300, h: 110 },
{ x: 3200, w: 220, h: 90 },
{ x: 4000, w: 260, h: 95 },
];
for (const h2 of hillPositions) {
const hx = h2.x - camera.x * 0.5;
const wrappedX = ((hx % (W + 400)) + W + 400) % (W + 400) - 200;
ctx.beginPath();
ctx.ellipse(wrappedX, GROUND_Y, h2.w / 2, h2.h, 0, Math.PI, 0);
ctx.fill();
}
}
function drawPlatform(p) {
if (p.type === "ground") {
// Ground with grass top
ctx.fillStyle = "#8B6914";
ctx.fillRect(p.x, p.y, p.w, p.h);
// Grass
ctx.fillStyle = "#4CAF50";
ctx.fillRect(p.x, p.y, p.w, 8);
ctx.fillStyle = "#66BB6A";
ctx.fillRect(p.x, p.y, p.w, 4);
// Dirt texture
ctx.fillStyle = "#7A5B10";
for (let dx = p.x; dx < p.x + p.w; dx += 24) {
ctx.fillRect(dx + 4, p.y + 12, 12, 2);
ctx.fillRect(dx + 16, p.y + 24, 8, 2);
}
} else if (p.type === "brick") {
if (p.hit) {
ctx.fillStyle = "#8B6914";
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.strokeStyle = "#5C4010";
ctx.lineWidth = 1;
ctx.strokeRect(p.x, p.y, p.w, p.h);
return;
}
ctx.fillStyle = "#C84C09";
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.strokeStyle = "#8B3000";
ctx.lineWidth = 2;
ctx.strokeRect(p.x, p.y, p.w, p.h);
// Brick lines
ctx.strokeStyle = "#A0400A";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p.x, p.y + p.h / 2);
ctx.lineTo(p.x + p.w, p.y + p.h / 2);
ctx.moveTo(p.x + p.w / 2, p.y);
ctx.lineTo(p.x + p.w / 2, p.y + p.h / 2);
ctx.moveTo(p.x + p.w / 4, p.y + p.h / 2);
ctx.lineTo(p.x + p.w / 4, p.y + p.h);
ctx.moveTo(p.x + p.w * 3 / 4, p.y + p.h / 2);
ctx.lineTo(p.x + p.w * 3 / 4, p.y + p.h);
ctx.stroke();
} else if (p.type === "question") {
const flash = p.hit ? "#8B6914" : (Math.floor(frameCount / 8) % 2 === 0 ? "#FFB800" : "#FFD700");
ctx.fillStyle = flash;
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.strokeStyle = p.hit ? "#5C4010" : "#C89600";
ctx.lineWidth = 2;
ctx.strokeRect(p.x, p.y, p.w, p.h);
if (!p.hit) {
ctx.fillStyle = "#8B6914";
ctx.font = "bold 22px monospace";
ctx.textAlign = "center";
ctx.fillText("?", p.x + p.w / 2, p.y + p.h / 2 + 7);
}
} else if (p.type === "pipe") {
// Pipe body
ctx.fillStyle = "#338A33";
ctx.fillRect(p.x + 4, p.y + 20, p.w - 8, p.h - 20);
// Pipe top
ctx.fillStyle = "#44AA44";
ctx.fillRect(p.x, p.y, p.w, 24);
// Pipe highlight
ctx.fillStyle = "#55CC55";
ctx.fillRect(p.x + 4, p.y + 2, 6, 20);
ctx.fillRect(p.x + 8, p.y + 24, 4, p.h - 24);
// Pipe shadow
ctx.fillStyle = "#226622";
ctx.fillRect(p.x + p.w - 10, p.y + 2, 4, 20);
ctx.fillRect(p.x + p.w - 12, p.y + 24, 4, p.h - 24);
// Pipe rim
ctx.strokeStyle = "#1A551A";
ctx.lineWidth = 2;
ctx.strokeRect(p.x, p.y, p.w, 24);
}
}
function drawCoin(c) {
const bobY = Math.sin(frameCount * 0.06 + c.bobOffset) * 4;
const cx = c.x;
const cy = c.y + bobY;
const stretch = Math.abs(Math.cos(frameCount * 0.08 + c.bobOffset));
ctx.save();
ctx.translate(cx, cy);
ctx.scale(stretch, 1);
// Coin body
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "#DAA520";
ctx.lineWidth = 2;
ctx.stroke();
// Coin shine
ctx.fillStyle = "#FFF8DC";
ctx.beginPath();
ctx.arc(-2, -2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
if (!e.alive && e.squishTimer <= 0) return;
const ex = e.x;
const ey = e.y;
if (!e.alive) {
// Squished
ctx.fillStyle = "#8B4513";
ctx.fillRect(ex, ey + e.h - 6, e.w, 6);
return;
}
// Goomba body
ctx.fillStyle = "#8B4513";
ctx.beginPath();
ctx.ellipse(ex + e.w / 2, ey + e.h / 2 + 2, e.w / 2, e.h / 2 - 2, 0, 0, Math.PI * 2);
ctx.fill();
// Goomba head
ctx.fillStyle = "#A0522D";
ctx.beginPath();
ctx.ellipse(ex + e.w / 2, ey + 6, e.w / 2 + 2, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Eyes
const eyeDir = e.vx > 0 ? 2 : -2;
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.ellipse(ex + 8 + eyeDir, ey + 6, 4, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(ex + 20 + eyeDir, ey + 6, 4, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Pupils
ctx.fillStyle = "#000";
ctx.beginPath();
ctx.arc(ex + 9 + eyeDir, ey + 7, 2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(ex + 21 + eyeDir, ey + 7, 2, 0, Math.PI * 2);
ctx.fill();
// Feet
const footAnim = Math.sin(frameCount * 0.15) * 3;
ctx.fillStyle = "#000";
ctx.fillRect(ex + 2, ey + e.h - 6 + footAnim, 8, 6);
ctx.fillRect(ex + e.w - 10, ey + e.h - 6 - footAnim, 8, 6);
}
function drawFlagPole(fp) {
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(fp.x, fp.y, fp.w, fp.h);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(fp.x + fp.w / 2, fp.y, 8, 0, Math.PI * 2);
ctx.fill();
// Flag
const flagWave = Math.sin(frameCount * 0.08) * 3;
ctx.fillStyle = "#FF4444";
ctx.beginPath();
ctx.moveTo(fp.x + fp.w, fp.y + 5);
ctx.lineTo(fp.x + fp.w + 35 + flagWave, fp.y + 15);
ctx.lineTo(fp.x + fp.w, fp.y + 30);
ctx.closePath();
ctx.fill();
// Flag star
ctx.fillStyle = "#FFD700";
ctx.font = "12px monospace";
ctx.textAlign = "center";
ctx.fillText("★", fp.x + fp.w + 16 + flagWave / 2, fp.y + 22);
}
function drawPlayer() {
const px = player.x;
const py = player.y;
const f = player.facing;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
ctx.scale(f, 1);
ctx.translate(-player.w / 2, -player.h / 2);
// Hat
ctx.fillStyle = "#E00000";
ctx.fillRect(2, 0, 20, 8);
ctx.fillRect(0, 2, 24, 6);
// Face
ctx.fillStyle = "#FFB68E";
ctx.fillRect(4, 8, 18, 12);
// Eye
ctx.fillStyle = "#000";
ctx.fillRect(14, 10, 3, 3);
// Mustache
ctx.fillStyle = "#4A2800";
ctx.fillRect(10, 16, 12, 2);
// Body / shirt
ctx.fillStyle = "#E00000";
ctx.fillRect(2, 20, 20, 6);
// Overalls
ctx.fillStyle = "#0000CC";
ctx.fillRect(2, 22, 20, 6);
// Overall buttons
ctx.fillStyle = "#FFD700";
ctx.fillRect(8, 23, 2, 2);
ctx.fillRect(14, 23, 2, 2);
// Legs
if (!player.onGround) {
// Jumping pose
ctx.fillStyle = "#0000CC";
ctx.fillRect(2, 28, 8, 4);
ctx.fillRect(14, 26, 8, 4);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(0, 28, 8, 4);
ctx.fillRect(16, 26, 8, 4);
} else if (Math.abs(player.vx) > 0.5) {
// Walking animation
const legOffset = player.animFrame === 1 ? 3 : 0;
ctx.fillStyle = "#0000CC";
ctx.fillRect(2, 28, 8, 4 + legOffset);
ctx.fillRect(14, 28, 8, 4 - legOffset);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(0, 28 + legOffset, 10, 4);
ctx.fillRect(14, 28 - legOffset, 10, 4);
} else {
// Standing
ctx.fillStyle = "#0000CC";
ctx.fillRect(2, 28, 8, 4);
ctx.fillRect(14, 28, 8, 4);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(0, 28, 10, 4);
ctx.fillRect(14, 28, 10, 4);
}
ctx.restore();
}
function drawHUD() {
// Background bar
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(0, 0, W, 36);
ctx.fillStyle = "#FFF";
ctx.font = "bold 16px monospace";
ctx.textAlign = "left";
// Score
ctx.fillText("SCORE", 16, 14);
ctx.fillText(String(score).padStart(6, "0"), 16, 30);
// Coins
ctx.fillStyle = "#FFD700";
ctx.fillText("🪙 × " + String(coins).padStart(2, "0"), 160, 24);
// Lives
ctx.fillStyle = "#FF6B6B";
ctx.fillText("♥ × " + lives, 300, 24);
// World
ctx.fillStyle = "#FFF";
ctx.textAlign = "right";
ctx.fillText("WORLD 1-1", W - 16, 24);
}
function drawTitleScreen() {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
// Title
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px monospace";
ctx.textAlign = "center";
ctx.fillText("SUPER CANVAS BROS", W / 2, H / 2 - 60);
ctx.fillStyle = "#FFF";
ctx.font = "20px monospace";
ctx.fillText("Arrow Keys / WASD to move", W / 2, H / 2);
ctx.fillText("Space / Up to jump", W / 2, H / 2 + 30);
ctx.fillText("Shift to run", W / 2, H / 2 + 60);
// Blinking prompt
if (Math.floor(frameCount / 30) % 2 === 0) {
ctx.fillStyle = "#FFD700";
ctx.font = "bold 22px monospace";
ctx.fillText("Press SPACE or ENTER to start", W / 2, H / 2 + 120);
}
ctx.fillStyle = "#AAA";
ctx.font = "12px monospace";
ctx.fillText("Collect coins, stomp enemies, reach the flag!", W / 2, H / 2 + 160);
}
function drawWinScreen() {
const alpha = Math.min(winTimer / 60, 0.7);
ctx.fillStyle = "rgba(0,0,0," + alpha + ")";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 42px monospace";
ctx.textAlign = "center";
ctx.fillText("★ YOU WIN! ★", W / 2, H / 2 - 40);
ctx.fillStyle = "#FFF";
ctx.font = "22px monospace";
ctx.fillText("Score: " + String(score).padStart(6, "0"), W / 2, H / 2 + 10);
ctx.fillText("Coins: " + coins, W / 2, H / 2 + 40);
if (winTimer > 60) {
if (Math.floor(frameCount / 30) % 2 === 0) {
ctx.fillStyle = "#FFD700";
ctx.font = "bold 18px monospace";
ctx.fillText("Press SPACE to play again", W / 2, H / 2 + 90);
}
}
if (winTimer > 60 && keys["Space"]) {
state = "title";
score = 0;
coins = 0;
lives = 3;
keys["Space"] = false;
}
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FF4444";
ctx.font = "bold 36px monospace";
ctx.textAlign = "center";
ctx.fillText("LIFE LOST", W / 2, H / 2);
if (lives > 0) {
ctx.fillStyle = "#FFF";
ctx.font = "18px monospace";
ctx.fillText("Lives remaining: " + lives, W / 2, H / 2 + 35);
}
}
// ─── Game Loop ───────────────────────────────────────────────
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ─── Init ────────────────────────────────────────────────────
buildLevel();
resetPlayer();
state = "title";
gameLoop();
})();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Pixel Adventure</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: monospace;
}
#game {
border: 3px solid #e94560;
border-radius: 4px;
image-rendering: pixelated;
cursor: none;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
// ── Constants ──────────────────────────────────────────────
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_FALL = 12;
const WORLD_W = 4000;
const GROUND_Y = 390;
// ── Input ──────────────────────────────────────────────────
const keys = {};
window.addEventListener("keydown", function(e) {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
e.preventDefault();
}
});
window.addEventListener("keyup", function(e) {
keys[e.code] = false;
});
// ── Camera ─────────────────────────────────────────────────
var camera = { x: 0, y: 0 };
// ── Player ─────────────────────────────────────────────────
var player = {
x: 60, y: GROUND_Y - 36,
w: 24, h: 36,
vx: 0, vy: 0,
speed: 0.55, maxSpeed: 4.5,
jumpForce: -10.5,
grounded: false,
facing: 1,
frame: 0, frameTimer: 0,
alive: true,
win: false,
invincible: 0,
jumpHeld: false
};
// ── Level Data ─────────────────────────────────────────────
// Platforms: {x, y, w, h, type}
// type: 1=ground-block, 2=brick, 3=question-block, 4=pipe
var platforms = [];
var coins = [];
var enemies = [];
var particles = [];
var floatingTexts = [];
var flag = null;
function buildLevel() {
platforms = [];
coins = [];
enemies = [];
// Ground segments (with gaps)
var groundSegs = [
{x:0, w:600}, {x:680, w:500}, {x:1250, w:800},
{x:2150, w:600}, {x:2850, w:1150}
];
for (var i = 0; i < groundSegs.length; i++) {
platforms.push({x: groundSegs[i].x, y: GROUND_Y, w: groundSegs[i].w, h: 60, type: 1});
}
// Raised platforms
var raised = [
{x: 200, y: 300, w: 96, h: 24, type: 2},
{x: 340, y: 250, w: 48, h: 24, type: 3},
{x: 420, y: 250, w: 48, h: 24, type: 2},
{x: 500, y: 200, w: 48, h: 24, type: 3},
{x: 750, y: 310, w: 48, h: 24, type: 2},
{x: 820, y: 260, w: 48, h: 24, type: 2},
{x: 890, y: 210, w: 96, h: 24, type: 2},
{x: 1050, y: 280, w: 144, h: 24, type: 2},
{x: 1080, y: 200, w: 48, h: 24, type: 3},
{x: 1350, y: 300, w: 48, h: 24, type: 3},
{x: 1450, y: 250, w: 48, h: 24, type: 2},
{x: 1550, y: 200, w: 96, h: 24, type: 2},
{x: 1680, y: 280, w: 48, h: 24, type: 3},
{x: 1850, y: 310, w: 144, h: 24, type: 2},
{x: 1950, y: 240, w: 48, h: 24, type: 2},
{x: 2050, y: 180, w: 48, h: 24, type: 3},
{x: 2250, y: 300, w: 96, h: 24, type: 2},
{x: 2400, y: 250, w: 48, h: 24, type: 2},
{x: 2500, y: 200, w: 96, h: 24, type: 2},
{x: 2700, y: 310, w: 48, h: 24, type: 3},
{x: 2800, y: 260, w: 48, h: 24, type: 2},
{x: 2900, y: 210, w: 96, h: 24, type: 2},
{x: 3050, y: 260, w: 48, h: 24, type: 3},
{x: 3150, y: 200, w: 48, h: 24, type: 2},
// Staircase near end
{x: 3300, y: 340, w: 48, h: 24, type: 2},
{x: 3350, y: 300, w: 48, h: 24, type: 2},
{x: 3400, y: 260, w: 48, h: 24, type: 2},
{x: 3450, y: 220, w: 48, h: 24, type: 2},
];
for (var j = 0; j < raised.length; j++) {
platforms.push(raised[j]);
}
// Pipes
var pipes = [
{x: 560, y: GROUND_Y - 48, w: 48, h: 48, type: 4},
{x: 1200, y: GROUND_Y - 64, w: 48, h: 64, type: 4},
{x: 2100, y: GROUND_Y - 56, w: 48, h: 56, type: 4},
{x: 2750, y: GROUND_Y - 48, w: 48, h: 48, type: 4},
];
for (var k = 0; k < pipes.length; k++) {
platforms.push(pipes[k]);
}
// Coins
var coinPositions = [
// On/above platforms
{x:220,y:270},{x:260,y:270},{x:300,y:270},
{x:360,y:220},{x:440,y:220},{x:520,y:170},
{x:770,y:280},{x:840,y:230},{x:910,y:180},{x:950,y:180},
{x:1070,y:250},{x:1110,y:250},{x:1150,y:250},
{x:1090,y:170},
{x:1370,y:270},{x:1470,y:220},{x:1570,y:170},{x:1610,y:170},
{x:1700,y:250},
{x:1870,y:280},{x:1910,y:280},{x:1970,y:210},{x:2070,y:150},
{x:2270,y:270},{x:2310,y:270},{x:2420,y:220},{x:2520,y:170},{x:2560,y:170},
{x:2720,y:280},{x:2820,y:230},{x:2920,y:180},{x:2960,y:180},
{x:3070,y:230},{x:3170,y:170},
{x:3320,y:310},{x:3370,y:270},{x:3420,y:230},{x:3470,y:190},
// Ground level coins
{x:630,y:360},{x:660,y:360},
{x:1220,y:360},
{x:2120,y:360},{x:2140,y:360},
// Arc of coins
{x:1700,y:340},{x:1730,y:310},{x:1760,y:290},{x:1790,y:310},{x:1820,y:340},
{x:2600,y:340},{x:2630,y:310},{x:2660,y:290},{x:2690,y:310},{x:2720,y:340},
];
for (var c = 0; c < coinPositions.length; c++) {
coins.push({x: coinPositions[c].x, y: coinPositions[c].y, w: 16, h: 16, collected: false, bobTimer: Math.random()*Math.PI*2});
}
// Enemies (goombas)
var enemyPositions = [
{x: 350, y: GROUND_Y - 24, minX: 200, maxX: 550},
{x: 800, y: GROUND_Y - 24, minX: 700, maxX: 1150},
{x: 1350, y: GROUND_Y - 24, minX: 1300, maxX: 1900},
{x: 1600, y: GROUND_Y - 24, minX: 1300, maxX: 1900},
{x: 2300, y: GROUND_Y - 24, minX: 2150, maxX: 2700},
{x: 2500, y: GROUND_Y - 24, minX: 2150, maxX: 2700},
{x: 3000, y: GROUND_Y - 24, minX: 2850, maxX: 3400},
{x: 3200, y: GROUND_Y - 24, minX: 2850, maxX: 3400},
];
for (var e = 0; e < enemyPositions.length; e++) {
var ep = enemyPositions[e];
enemies.push({
x: ep.x, y: ep.y, w: 24, h: 24,
vx: 1.2, alive: true,
minX: ep.minX, maxX: ep.maxX,
frame: 0, frameTimer: 0,
squishTimer: 0
});
}
// Flag at the end
flag = {x: 3750, y: GROUND_Y - 160, w: 8, h: 160, reached: false};
}
// ── Score ──────────────────────────────────────────────────
var score = 0;
var coinCount = 0;
var totalCoins = 0;
var gameState = "playing"; // "playing", "won", "dead"
var deathTimer = 0;
var winTimer = 0;
// ── Collision helpers ──────────────────────────────────────
function rectsOverlap(a, b) {
return 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 resolveCollisions(entity) {
entity.grounded = false;
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (!rectsOverlap(entity, p)) continue;
// Determine overlap on each axis
var overlapLeft = (entity.x + entity.w) - p.x;
var overlapRight = (p.x + p.w) - entity.x;
var overlapTop = (entity.y + entity.h) - p.y;
var overlapBottom = (p.y + p.h) - entity.y;
var minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && entity.vy >= 0) {
entity.y = p.y - entity.h;
entity.vy = 0;
entity.grounded = true;
} else if (minOverlap === overlapBottom && entity.vy < 0) {
entity.y = p.y + p.h;
entity.vy = 1;
} else if (minOverlap === overlapLeft) {
entity.x = p.x - entity.w;
entity.vx = 0;
} else if (minOverlap === overlapRight) {
entity.x = p.x + p.w;
entity.vx = 0;
}
}
}
// ── Particles ──────────────────────────────────────────────
function spawnParticles(x, y, color, count) {
for (var i = 0; i < count; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 6,
vy: -Math.random() * 5 - 1,
life: 30 + Math.random() * 20,
maxLife: 30 + Math.random() * 20,
color: color,
size: 2 + Math.random() * 3
});
}
}
function spawnFloatingText(x, y, text, color) {
floatingTexts.push({x: x, y: y, text: text, color: color || "#FFD700", life: 40, vy: -1.5});
}
// ── Update ─────────────────────────────────────────────────
function update() {
if (gameState === "won") {
winTimer++;
updateParticles();
updateFloatingTexts();
return;
}
if (gameState === "dead") {
deathTimer++;
updateParticles();
if (deathTimer > 90) {
resetGame();
}
return;
}
// Player input
var moveLeft = keys["ArrowLeft"] || keys["KeyA"];
var moveRight = keys["ArrowRight"] || keys["KeyD"];
var jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
if (moveLeft) {
player.vx -= player.speed;
player.facing = -1;
}
if (moveRight) {
player.vx += player.speed;
player.facing = 1;
}
// Clamp horizontal speed
if (player.vx > player.maxSpeed) player.vx = player.maxSpeed;
if (player.vx < -player.maxSpeed) player.vx = -player.maxSpeed;
// Jump
if (jumpKey && player.grounded && !player.jumpHeld) {
player.vy = player.jumpForce;
player.grounded = false;
player.jumpHeld = true;
}
if (!jumpKey) {
player.jumpHeld = false;
// Variable jump height
if (player.vy < -4) player.vy = -4;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// Friction
if (!moveLeft && !moveRight) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Move player
player.x += player.vx;
player.y += player.vy;
// Resolve collisions
resolveCollisions(player);
// Clamp to world bounds
if (player.x < 0) player.x = 0;
if (player.x > WORLD_W - player.w) player.x = WORLD_W - player.w;
// Fall death
if (player.y > H + 50) {
gameState = "dead";
deathTimer = 0;
spawnParticles(player.x, H - 20, "#ff6b6b", 15);
}
// Animation
player.frameTimer++;
if (Math.abs(player.vx) > 0.5 && player.grounded) {
if (player.frameTimer % 6 === 0) player.frame = (player.frame + 1) % 4;
} else if (!player.grounded) {
player.frame = 5; // jump frame
} else {
player.frame = 0;
}
// Invincibility timer
if (player.invincible > 0) player.invincible--;
// Coins
for (var ci = 0; ci < coins.length; ci++) {
var coin = coins[ci];
if (coin.collected) continue;
coin.bobTimer += 0.05;
var coinRect = {x: coin.x, y: coin.y + Math.sin(coin.bobTimer) * 3, w: coin.w, h: coin.h};
if (rectsOverlap(player, coinRect)) {
coin.collected = true;
coinCount++;
score += 100;
spawnParticles(coin.x + 8, coin.y, "#FFD700", 8);
spawnFloatingText(coin.x, coin.y - 10, "+100");
}
}
// Enemies
for (var ei = 0; ei < enemies.length; ei++) {
var enemy = enemies[ei];
if (!enemy.alive) {
if (enemy.squishTimer > 0) enemy.squishTimer--;
continue;
}
// Patrol
enemy.x += enemy.vx;
if (enemy.x <= enemy.minX || enemy.x + enemy.w >= enemy.maxX) {
enemy.vx *= -1;
}
// Gravity for enemies
var enemyGrounded = false;
enemy.y += 2;
for (var pi = 0; pi < platforms.length; pi++) {
var p = platforms[pi];
if (enemy.x < p.x + p.w && enemy.x + enemy.w > p.x &&
enemy.y + enemy.h > p.y && enemy.y + enemy.h < p.y + 10) {
enemy.y = p.y - enemy.h;
enemyGrounded = true;
}
}
if (!enemyGrounded) {
enemy.y -= 2;
}
// Animation
enemy.frameTimer++;
if (enemy.frameTimer % 10 === 0) enemy.frame = (enemy.frame + 1) % 2;
// Player collision
if (player.invincible <= 0 && rectsOverlap(player, enemy)) {
// Stomp from above
if (player.vy > 0 && player.y + player.h - 8 < enemy.y + enemy.h / 2) {
enemy.alive = false;
enemy.squishTimer = 30;
player.vy = -7;
score += 200;
spawnParticles(enemy.x + 12, enemy.y + 12, "#8B4513", 10);
spawnFloatingText(enemy.x, enemy.y - 10, "+200", "#ff6b6b");
} else {
// Hurt player
gameState = "dead";
deathTimer = 0;
spawnParticles(player.x + 12, player.y + 18, "#ff6b6b", 15);
}
}
}
// Flag check
if (flag && !flag.reached) {
var flagRect = {x: flag.x - 10, y: flag.y, w: 30, h: flag.h};
if (rectsOverlap(player, flagRect)) {
flag.reached = true;
gameState = "won";
winTimer = 0;
score += 1000;
spawnParticles(flag.x, flag.y, "#FFD700", 30);
spawnParticles(flag.x, flag.y + 40, "#ff6b6b", 20);
spawnParticles(flag.x, flag.y + 80, "#4ecdc4", 20);
}
}
// Camera
var targetX = player.x - W / 2 + player.w / 2;
camera.x += (targetX - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
if (camera.x > WORLD_W - W) camera.x = WORLD_W - W;
updateParticles();
updateFloatingTexts();
}
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
function updateFloatingTexts() {
for (var i = floatingTexts.length - 1; i >= 0; i--) {
var ft = floatingTexts[i];
ft.y += ft.vy;
ft.life--;
if (ft.life <= 0) floatingTexts.splice(i, 1);
}
}
// ── Drawing ────────────────────────────────────────────────
function drawBackground() {
// Sky gradient
var grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#4a90d9");
grad.addColorStop(0.6, "#87CEEB");
grad.addColorStop(1, "#b8e4f9");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255,255,255,0.8)";
var cloudPositions = [
{x: 100, y: 50, s: 1.2}, {x: 400, y: 80, s: 0.8},
{x: 700, y: 40, s: 1.0}, {x: 1100, y: 60, s: 1.3},
{x: 1500, y: 30, s: 0.9}, {x: 2000, y: 70, s: 1.1},
{x: 2500, y: 45, s: 0.7}, {x: 3000, y: 55, s: 1.0},
{x: 3500, y: 35, s: 1.2},
];
for (var i = 0; i < cloudPositions.length; i++) {
var c = cloudPositions[i];
var cx = c.x - camera.x * 0.3;
// Wrap clouds
cx = ((cx % (W + 200)) + (W + 200)) % (W + 200) - 100;
drawCloud(cx, c.y, c.s);
}
// Hills (parallax)
ctx.fillStyle = "#5a9e5a";
var hillPositions = [
{x: 0, w: 300, h: 80}, {x: 400, w: 250, h: 60},
{x: 800, w: 350, h: 90}, {x: 1300, w: 280, h: 70},
{x: 1800, w: 320, h: 85}, {x: 2300, w: 260, h: 65},
{x: 2800, w: 300, h: 75}, {x: 3300, w: 350, h: 80},
];
for (var hi = 0; hi < hillPositions.length; hi++) {
var h = hillPositions[hi];
var hx = h.x - camera.x * 0.5;
drawHill(hx, GROUND_Y, h.w, h.h);
}
}
function drawCloud(x, y, scale) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.arc(25, -5, 18, 0, Math.PI * 2);
ctx.arc(-22, 2, 16, 0, Math.PI * 2);
ctx.arc(10, -15, 15, 0, Math.PI * 2);
ctx.arc(-8, -12, 14, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHill(x, baseY, w, h) {
ctx.beginPath();
ctx.moveTo(x, baseY);
ctx.quadraticCurveTo(x + w / 2, baseY - h, x + w, baseY);
ctx.fill();
}
function drawPlatforms() {
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
var px = p.x - camera.x;
var py = p.y;
// Skip if off screen
if (px + p.w < -10 || px > W + 10) continue;
if (p.type === 1) {
// Ground block
ctx.fillStyle = "#8B6914";
ctx.fillRect(px, py, p.w, p.h);
// Grass top
ctx.fillStyle = "#4CAF50";
ctx.fillRect(px, py, p.w, 8);
ctx.fillStyle = "#66BB6A";
ctx.fillRect(px, py, p.w, 4);
// Dirt texture
ctx.fillStyle = "#7A5B10";
for (var dx = 0; dx < p.w; dx += 20) {
ctx.fillRect(px + dx + 5, py + 15, 8, 3);
ctx.fillRect(px + dx + 12, py + 25, 6, 3);
}
} else if (p.type === 2) {
// Brick
ctx.fillStyle = "#C67B30";
ctx.fillRect(px, py, p.w, p.h);
ctx.strokeStyle = "#8B5E14";
ctx.lineWidth = 1;
ctx.strokeRect(px, py, p.w, p.h);
// Brick pattern
ctx.fillStyle = "#D4943A";
ctx.fillRect(px + 2, py + 2, p.w / 2 - 2, p.h / 2 - 2);
ctx.fillRect(px + p.w / 2 + 1, py + 2, p.w / 2 - 3, p.h / 2 - 2);
ctx.fillRect(px + 2, py + p.h / 2 + 1, p.w - 4, p.h / 2 - 3);
} else if (p.type === 3) {
// Question block
ctx.fillStyle = "#FFB800";
ctx.fillRect(px, py, p.w, p.h);
ctx.strokeStyle = "#CC8800";
ctx.lineWidth = 2;
ctx.strokeRect(px + 1, py + 1, p.w - 2, p.h - 2);
// Question mark
ctx.fillStyle = "#FFF";
ctx.font = "bold 16px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", px + p.w / 2, py + p.h / 2 + 1);
// Shine
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(px + 3, py + 3, 6, 6);
} else if (p.type === 4) {
// Pipe
// Pipe body
ctx.fillStyle = "#2ECC40";
ctx.fillRect(px + 4, py + 16, p.w - 8, p.h - 16);
// Pipe top
ctx.fillStyle = "#3DCC4A";
ctx.fillRect(px, py, p.w, 16);
// Pipe highlights
ctx.fillStyle = "#5DEE6A";
ctx.fillRect(px + 6, py + 16, 6, p.h - 16);
ctx.fillRect(px + 2, py + 2, p.w - 4, 4);
// Pipe shadow
ctx.fillStyle = "#26A835";
ctx.fillRect(px + p.w - 10, py + 16, 6, p.h - 16);
ctx.fillRect(px + p.w - 4, py + 2, 4, 14);
}
}
}
function drawCoins() {
for (var i = 0; i < coins.length; i++) {
var coin = coins[i];
if (coin.collected) continue;
var cx = coin.x - camera.x;
var cy = coin.y + Math.sin(coin.bobTimer) * 3;
if (cx < -20 || cx > W + 20) continue;
// Coin body
var stretch = Math.abs(Math.cos(coin.bobTimer * 1.5));
ctx.save();
ctx.translate(cx + 8, cy + 8);
ctx.scale(Math.max(0.2, stretch), 1);
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFA500";
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFD700";
ctx.font = "bold 10px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 0);
ctx.restore();
}
}
function drawPlayer() {
if (gameState === "dead") return;
var px = player.x - camera.x;
var py = player.y;
// Blinking when invincible
if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
ctx.scale(player.facing, 1);
// Hat
ctx.fillStyle = "#E63946";
ctx.fillRect(-12, -18, 20, 8);
ctx.fillRect(-10, -18, 16, 4);
// Face
ctx.fillStyle = "#FFD4A0";
ctx.fillRect(-10, -10, 18, 14);
// Eyes
ctx.fillStyle = "#000";
ctx.fillRect(2, -8, 3, 4);
// Mustache
ctx.fillStyle = "#5C3317";
ctx.fillRect(0, -2, 10, 3);
// Body / shirt
ctx.fillStyle = "#E63946";
ctx.fillRect(-10, 4, 18, 10);
// Overalls
ctx.fillStyle = "#1D3557";
ctx.fillRect(-10, 8, 18, 12);
// Overall straps
ctx.fillStyle = "#1D3557";
ctx.fillRect(-8, 4, 4, 6);
ctx.fillRect(4, 4, 4, 6);
// Buttons
ctx.fillStyle = "#FFD700";
ctx.fillRect(-7, 8, 2, 2);
ctx.fillRect(5, 8, 2, 2);
// Legs animation
if (player.frame === 5) {
// Jumping pose
ctx.fillStyle = "#1D3557";
ctx.fillRect(-10, 18, 7, 4);
ctx.fillRect(3, 16, 7, 4);
// Shoes
ctx.fillStyle = "#5C3317";
ctx.fillRect(-11, 20, 8, 4);
ctx.fillRect(3, 18, 8, 4);
} else {
var legOffset = 0;
if (player.frame === 1) legOffset = 3;
if (player.frame === 3) legOffset = -3;
ctx.fillStyle = "#1D3557";
ctx.fillRect(-10, 18, 7, 4 + legOffset);
ctx.fillRect(3, 18, 7, 4 - legOffset);
// Shoes
ctx.fillStyle = "#5C3317";
ctx.fillRect(-11, 20 + Math.max(0, legOffset), 9, 4);
ctx.fillRect(2, 20 + Math.max(0, -legOffset), 9, 4);
}
ctx.restore();
}
function drawEnemies() {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var ex = e.x - camera.x;
var ey = e.y;
if (ex < -30 || ex > W + 30) continue;
if (!e.alive && e.squishTimer > 0) {
// Squished
var alpha = e.squishTimer / 30;
ctx.globalAlpha = alpha;
ctx.fillStyle = "#8B4513";
ctx.fillRect(ex, ey + e.h - 6, e.w, 6);
ctx.globalAlpha = 1;
continue;
}
if (!e.alive) continue;
// Body
ctx.fillStyle = "#8B4513";
ctx.fillRect(ex + 2, ey + 4, e.w - 4, e.h - 4);
// Head
ctx.fillStyle = "#A0522D";
ctx.beginPath();
ctx.arc(ex + e.w / 2, ey + 6, 12, Math.PI, 0);
ctx.fill();
// Eyes
ctx.fillStyle = "#FFF";
ctx.fillRect(ex + 5, ey + 4, 5, 5);
ctx.fillRect(ex + 14, ey + 4, 5, 5);
ctx.fillStyle = "#000";
ctx.fillRect(ex + 7, ey + 5, 3, 3);
ctx.fillRect(ex + 16, ey + 5, 3, 3);
// Angry eyebrows
ctx.fillStyle = "#000";
ctx.fillRect(ex + 5, ey + 2, 5, 2);
ctx.fillRect(ex + 14, ey + 2, 5, 2);
// Feet
var footFrame = e.frame;
ctx.fillStyle = "#000";
if (footFrame === 0) {
ctx.fillRect(ex, ey + e.h - 4, 8, 4);
ctx.fillRect(ex + e.w - 8, ey + e.h - 4, 8, 4);
} else {
ctx.fillRect(ex + 2, ey + e.h - 4, 8, 4);
ctx.fillRect(ex + e.w - 10, ey + e.h - 4, 8, 4);
}
}
}
function drawFlag() {
if (!flag) return;
var fx = flag.x - camera.x;
var fy = flag.y;
if (fx < -50 || fx > W + 50) return;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(fx, fy, 6, flag.h);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(fx + 3, fy, 6, 0, Math.PI * 2);
ctx.fill();
// Flag
var flagWave = Math.sin(Date.now() / 300) * 3;
ctx.fillStyle = flag.reached ? "#FFD700" : "#E63946";
ctx.beginPath();
ctx.moveTo(fx + 6, fy + 5);
ctx.lineTo(fx + 40 + flagWave, fy + 15);
ctx.lineTo(fx + 6, fy + 35);
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFF";
ctx.font = "12px monospace";
ctx.textAlign = "center";
ctx.fillText("★", fx + 20 + flagWave / 2, fy + 24);
// Base
ctx.fillStyle = "#666";
ctx.fillRect(fx - 8, fy + flag.h - 8, 22, 8);
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
var alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - camera.x, p.y, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawFloatingTexts() {
for (var i = 0; i < floatingTexts.length; i++) {
var ft = floatingTexts[i];
var alpha = ft.life / 40;
ctx.globalAlpha = alpha;
ctx.fillStyle = ft.color;
ctx.font = "bold 14px monospace";
ctx.textAlign = "center";
ctx.fillText(ft.text, ft.x - camera.x, ft.y);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score background
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, 40);
// Score
ctx.fillStyle = "#FFF";
ctx.font = "bold 18px monospace";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("SCORE: " + score, 15, 20);
// Coins
ctx.fillStyle = "#FFD700";
ctx.fillText("🪙 " + coinCount + "/" + totalCoins, 200, 20);
// Controls hint
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.font = "11px monospace";
ctx.textAlign = "right";
ctx.fillText("← → or A/D to move | SPACE or ↑ to jump", W - 15, 20);
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ff6b6b";
ctx.font = "bold 48px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("GAME OVER", W / 2, H / 2 - 30);
ctx.fillStyle = "#FFF";
ctx.font = "18px monospace";
ctx.fillText("Restarting...", W / 2, H / 2 + 20);
}
function drawWinScreen() {
var alpha = Math.min(1, winTimer / 60);
ctx.fillStyle = "rgba(0,0,0," + (alpha * 0.5) + ")";
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.globalAlpha = alpha;
// Stars
var starCount = 5;
for (var i = 0; i < starCount; i++) {
var sx = W / 2 + Math.cos(winTimer * 0.03 + i * 1.25) * 150;
var sy = H / 2 - 60 + Math.sin(winTimer * 0.04 + i * 1.25) * 20;
ctx.fillStyle = "#FFD700";
ctx.font = "24px monospace";
ctx.textAlign = "center";
ctx.fillText("★", sx, sy);
}
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px monospace";
ctx.textAlign = "center";
ctx.fillText("YOU WIN!", W / 2, H / 2 - 10);
ctx.fillStyle = "#FFF";
ctx.font = "20px monospace";
ctx.fillText("Score: " + score, W / 2, H / 2 + 30);
ctx.fillText("Coins: " + coinCount + "/" + totalCoins, W / 2, H / 2 + 55);
ctx.restore();
}
// ── Main render ────────────────────────────────────────────
function render() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawPlatforms();
drawCoins();
drawFlag();
drawEnemies();
drawPlayer();
drawParticles();
drawFloatingTexts();
drawHUD();
if (gameState === "dead") drawDeathScreen();
if (gameState === "won") drawWinScreen();
}
// ── Game loop ──────────────────────────────────────────────
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// ── Reset ──────────────────────────────────────────────────
function resetGame() {
player.x = 60;
player.y = GROUND_Y - 36;
player.vx = 0;
player.vy = 0;
player.grounded = false;
player.facing = 1;
player.frame = 0;
player.invincible = 0;
player.jumpHeld = false;
score = 0;
coinCount = 0;
particles = [];
floatingTexts = [];
gameState = "playing";
deathTimer = 0;
winTimer = 0;
camera.x = 0;
buildLevel();
totalCoins = coins.length;
}
// ── Init ───────────────────────────────────────────────────
buildLevel();
totalCoins = coins.length;
requestAnimationFrame(gameLoop);
})();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Canvas Bros</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Courier New', monospace;
overflow: hidden;
}
#game {
border: 3px solid #e94560;
border-radius: 4px;
image-rendering: pixelated;
cursor: default;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
"use strict";
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var W = canvas.width;
var H = canvas.height;
// ─── Constants ───
var GRAVITY = 0.55;
var FRICTION = 0.82;
var MAX_FALL = 12;
var TILE = 32;
// ─── Input ───
var keys = {};
var keyJustPressed = {};
window.addEventListener("keydown", function(e) {
if (!keys[e.code]) { keyJustPressed[e.code] = true; }
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
e.preventDefault();
}
});
window.addEventListener("keyup", function(e) {
keys[e.code] = false;
});
function clearJustPressed() {
keyJustPressed = {};
}
// ─── Level Data ───
// Ground tiles: 0=air, 1=ground, 2=brick, 3=question block, 4=pipe body, 5=pipe top, 6=flag pole, 7=flag top
var LEVEL_W = 120; // tiles wide
var LEVEL_H = 14; // tiles high (450/32 ≈ 14)
function buildLevel() {
var map = [];
for (var y = 0; y < LEVEL_H; y++) {
map[y] = [];
for (var x = 0; x < LEVEL_W; x++) {
// Ground: bottom 2 rows
if (y >= LEVEL_H - 2) {
map[y][x] = 1;
} else {
map[y][x] = 0;
}
}
}
// Gaps in ground
var gaps = [[18, 20], [45, 47], [78, 80]];
for (var g = 0; g < gaps.length; g++) {
for (var gx = gaps[g][0]; gx <= gaps[g][1]; gx++) {
map[LEVEL_H - 2][gx] = 0;
map[LEVEL_H - 1][gx] = 0;
}
}
// Platforms (floating rows of bricks)
var platforms = [
{ x: 8, y: 9, w: 5, type: 2 },
{ x: 15, y: 7, w: 3, type: 2 },
{ x: 22, y: 10, w: 4, type: 2 },
{ x: 30, y: 8, w: 6, type: 2 },
{ x: 38, y: 6, w: 3, type: 3 },
{ x: 42, y: 10, w: 3, type: 2 },
{ x: 50, y: 9, w: 4, type: 2 },
{ x: 56, y: 7, w: 5, type: 2 },
{ x: 62, y: 10, w: 3, type: 2 },
{ x: 68, y: 8, w: 4, type: 3 },
{ x: 75, y: 6, w: 3, type: 2 },
{ x: 82, y: 9, w: 5, type: 2 },
{ x: 88, y: 7, w: 4, type: 2 },
{ x: 95, y: 10, w: 3, type: 2 },
{ x: 100, y: 8, w: 6, type: 2 },
];
for (var p = 0; p < platforms.length; p++) {
var pl = platforms[p];
for (var px = pl.x; px < pl.x + pl.w; px++) {
map[pl.y][px] = pl.type;
}
}
// Pipes
var pipes = [
{ x: 12, h: 2 },
{ x: 35, h: 3 },
{ x: 55, h: 2 },
{ x: 72, h: 4 },
{ x: 92, h: 3 },
];
for (var pi = 0; pi < pipes.length; pi++) {
var pipe = pipes[pi];
var baseY = LEVEL_H - 2;
map[baseY - 1][pipe.x] = 5; // pipe top
for (var py = baseY; py < baseY + pipe.h - 1; py++) {
if (py < LEVEL_H) map[py][pipe.x] = 4;
}
// Pipe right side
map[baseY - 1][pipe.x + 1] = 5;
for (var py2 = baseY; py2 < baseY + pipe.h - 1; py2++) {
if (py2 < LEVEL_H) map[py2][pipe.x + 1] = 4;
}
}
// Staircase near end
for (var sx = 106; sx <= 114; sx++) {
var stairH = sx - 106 + 1;
for (var sy = LEVEL_H - 2; sy > LEVEL_H - 2 - stairH && sy >= 0; sy--) {
map[sy][sx] = 1;
}
}
// Flag pole
var flagX = 116;
for (var fy = 3; fy < LEVEL_H - 2; fy++) {
map[fy][flagX] = 6;
}
map[3][flagX] = 7; // flag top
return map;
}
var levelMap = buildLevel();
// ─── Coins ───
function buildCoins() {
var coins = [];
var coinPositions = [
[9, 8], [10, 8], [11, 8],
[16, 6], [17, 6],
[23, 9], [24, 9],
[31, 7], [32, 7], [33, 7], [34, 7],
[39, 5],
[43, 9],
[51, 8], [52, 8],
[57, 6], [58, 6], [59, 6],
[63, 9],
[69, 7], [70, 7],
[76, 5],
[83, 8], [84, 8], [85, 8],
[89, 6], [90, 6],
[96, 9],
[101, 7], [102, 7], [103, 7], [104, 7],
// Arc over gap
[19, 10],
[46, 10],
[79, 10],
];
for (var i = 0; i < coinPositions.length; i++) {
coins.push({
x: coinPositions[i][0] * TILE + 8,
y: coinPositions[i][1] * TILE + 4,
collected: false,
animFrame: 0
});
}
return coins;
}
var coins = buildCoins();
// ─── Enemies ───
function buildEnemies() {
var enemies = [];
var enemyPositions = [
[10, LEVEL_H - 3],
[25, LEVEL_H - 3],
[33, LEVEL_H - 3],
[48, LEVEL_H - 3],
[60, LEVEL_H - 3],
[70, LEVEL_H - 3],
[85, LEVEL_H - 3],
[97, LEVEL_H - 3],
];
for (var i = 0; i < enemyPositions.length; i++) {
enemies.push({
x: enemyPositions[i][0] * TILE,
y: enemyPositions[i][1] * TILE,
w: TILE - 4,
h: TILE - 4,
vx: -1.2,
alive: true,
squishTimer: 0,
animFrame: 0
});
}
return enemies;
}
var enemies = buildEnemies();
// ─── Clouds & Decorations ───
var clouds = [];
for (var i = 0; i < 25; i++) {
clouds.push({
x: i * 250 + Math.random() * 100,
y: 20 + Math.random() * 60,
w: 60 + Math.random() * 50,
h: 25 + Math.random() * 15
});
}
var bushes = [];
for (var i = 0; i < 30; i++) {
bushes.push({
x: i * 200 + Math.random() * 80,
w: 50 + Math.random() * 40,
h: 15 + Math.random() * 10
});
}
// ─── Particles ───
var particles = [];
function spawnParticles(x, y, color, count) {
for (var i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: -Math.random() * 5 - 2,
life: 30 + Math.random() * 20,
color: color,
size: 2 + Math.random() * 3
});
}
}
// ─── Player ───
var player = {
x: 3 * TILE,
y: (LEVEL_H - 4) * TILE,
w: 24,
h: 30,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
walkFrame: 0,
walkTimer: 0,
dead: false,
deathTimer: 0,
invincible: 0,
winTimer: 0,
won: false
};
// ─── Camera ───
var camera = { x: 0, y: 0 };
// ─── Score ───
var score = 0;
var coinCount = 0;
var totalCoins = coins.length;
var lives = 3;
var gameTime = 0;
var gameState = "play"; // play, dead, win, gameover
// ─── Collision Helpers ───
function getTile(tx, ty) {
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
return levelMap[ty][tx];
}
function isSolid(tx, ty) {
var t = getTile(tx, ty);
return t === 1 || t === 2 || t === 3 || t === 4 || t === 5;
}
function rectCollide(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
// ─── Hit block from below ───
function hitBlock(tx, ty) {
var t = getTile(tx, ty);
if (t === 3) { // Question block
levelMap[ty][tx] = 2; // become brick
score += 100;
coinCount++;
spawnParticles(tx * TILE + TILE / 2, ty * TILE, "#FFD700", 8);
}
}
// ─── Update Player ───
function updatePlayer() {
if (gameState !== "play") return;
var accel = 0.45;
var maxSpeed = 4.5;
var jumpForce = -10.5;
// Horizontal movement
if (keys["ArrowLeft"] || keys["KeyA"]) {
player.vx -= accel;
player.facing = -1;
}
if (keys["ArrowRight"] || keys["KeyD"]) {
player.vx += accel;
player.facing = 1;
}
// Friction
if (!keys["ArrowLeft"] && !keys["ArrowRight"] && !keys["KeyA"] && !keys["KeyD"]) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Clamp speed
if (player.vx > maxSpeed) player.vx = maxSpeed;
if (player.vx < -maxSpeed) player.vx = -maxSpeed;
// Jump
if ((keyJustPressed["Space"] || keyJustPressed["ArrowUp"] || keyJustPressed["KeyW"]) && player.onGround) {
player.vy = jumpForce;
player.onGround = false;
}
// Variable jump height (release early = shorter jump)
if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.vy < 0) {
// keep going up
} else {
// cut upward momentum on release (handled naturally by gravity)
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// Walk animation
if (Math.abs(player.vx) > 0.5 && player.onGround) {
player.walkTimer += Math.abs(player.vx) * 0.15;
if (player.walkTimer > 1) {
player.walkTimer = 0;
player.walkFrame = (player.walkFrame + 1) % 4;
}
} else if (player.onGround) {
player.walkFrame = 0;
}
// ─── Horizontal Collision ───
player.x += player.vx;
var left = Math.floor(player.x / TILE);
var right = Math.floor((player.x + player.w - 1) / TILE);
var top = Math.floor(player.y / TILE);
var bottom = Math.floor((player.y + player.h - 1) / TILE);
for (var ty = top; ty <= bottom; ty++) {
for (var tx = left; tx <= right; tx++) {
if (isSolid(tx, ty)) {
if (player.vx > 0) {
player.x = tx * TILE - player.w;
player.vx = 0;
} else if (player.vx < 0) {
player.x = (tx + 1) * TILE;
player.vx = 0;
}
}
}
}
// ─── Vertical Collision ───
player.y += player.vy;
player.onGround = false;
left = Math.floor(player.x / TILE);
right = Math.floor((player.x + player.w - 1) / TILE);
top = Math.floor(player.y / TILE);
bottom = Math.floor((player.y + player.h - 1) / TILE);
for (var ty2 = top; ty2 <= bottom; ty2++) {
for (var tx2 = left; tx2 <= right; tx2++) {
if (isSolid(tx2, ty2)) {
if (player.vy > 0) {
player.y = ty2 * TILE - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = (ty2 + 1) * TILE;
player.vy = 0;
hitBlock(tx2, ty2);
}
}
}
}
// Keep player in bounds
if (player.x < 0) player.x = 0;
// Fall death
if (player.y > LEVEL_H * TILE + 50) {
playerDie();
}
// Invincibility timer
if (player.invincible > 0) player.invincible--;
// Win check - reached flag
var playerTileX = Math.floor((player.x + player.w / 2) / TILE);
var playerTileY = Math.floor((player.y + player.h / 2) / TILE);
if (getTile(playerTileX, playerTileY) === 6 || getTile(playerTileX, playerTileY) === 7) {
if (!player.won) {
player.won = true;
gameState = "win";
player.winTimer = 120;
score += 1000;
spawnParticles(player.x + player.w / 2, player.y, "#FFD700", 20);
}
}
}
function playerDie() {
if (player.dead) return;
lives--;
if (lives <= 0) {
gameState = "gameover";
} else {
gameState = "dead";
player.deathTimer = 90;
player.vy = -8;
}
}
// ─── Update Enemies ───
function updateEnemies() {
if (gameState !== "play") return;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.alive) {
if (e.squishTimer > 0) e.squishTimer--;
continue;
}
// Only update if near camera
if (Math.abs(e.x - camera.x) > W + 100) continue;
e.x += e.vx;
e.animFrame += 0.05;
// Gravity for enemies
var eBottom = Math.floor((e.y + e.h) / TILE);
var eLeft = Math.floor(e.x / TILE);
var eRight = Math.floor((e.x + e.w) / TILE);
// Check if on ground
if (!isSolid(eLeft, eBottom) && !isSolid(eRight, eBottom)) {
e.y += 3;
} else {
// Snap to tile
e.y = eBottom * TILE - e.h;
}
// Wall collision / edge detection
var wallLeft = Math.floor(e.x / TILE);
var wallRight = Math.floor((e.x + e.w + 1) / TILE);
var wallY = Math.floor((e.y + e.h / 2) / TILE);
if (isSolid(wallLeft, wallY)) {
e.vx = Math.abs(e.vx);
}
if (isSolid(wallRight, wallY)) {
e.vx = -Math.abs(e.vx);
}
// Edge detection - turn around at edges
var edgeCheck = Math.floor((e.x + (e.vx > 0 ? e.w + 2 : -2)) / TILE);
var belowEdge = Math.floor((e.y + e.h + 2) / TILE);
if (!isSolid(edgeCheck, belowEdge)) {
e.vx = -e.vx;
}
// Player collision
if (!player.dead && player.invincible <= 0) {
var playerRect = { x: player.x, y: player.y, w: player.w, h: player.h };
var enemyRect = { x: e.x, y: e.y, w: e.w, h: e.h };
if (rectCollide(playerRect, enemyRect)) {
// Check if player is stomping
if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h / 2) {
e.alive = false;
e.squishTimer = 30;
player.vy = -7; // bounce
score += 200;
spawnParticles(e.x + e.w / 2, e.y + e.h / 2, "#8B4513", 6);
} else {
playerDie();
}
}
}
}
}
// ─── Update Coins ───
function updateCoins() {
if (gameState !== "play") return;
for (var i = 0; i < coins.length; i++) {
var c = coins[i];
if (c.collected) continue;
c.animFrame += 0.08;
var playerRect = { x: player.x, y: player.y, w: player.w, h: player.h };
var coinRect = { x: c.x, y: c.y, w: 16, h: 16 };
if (rectCollide(playerRect, coinRect)) {
c.collected = true;
coinCount++;
score += 50;
spawnParticles(c.x + 8, c.y + 8, "#FFD700", 5);
}
}
}
// ─── Update Particles ───
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
// ─── Update Camera ───
function updateCamera() {
var targetX = player.x - W / 3;
camera.x += (targetX - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
if (camera.x > LEVEL_W * TILE - W) camera.x = LEVEL_W * TILE - W;
}
// ─── Drawing ───
function drawBackground() {
// Sky gradient
var grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#5c94fc");
grad.addColorStop(0.7, "#87CEEB");
grad.addColorStop(1, "#b8d8f0");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255,255,255,0.9)";
for (var i = 0; i < clouds.length; i++) {
var c = clouds[i];
var cx = c.x - camera.x * 0.3;
// Wrap
while (cx < -100) cx += 250 * clouds.length * 0.3;
while (cx > W + 100) cx -= 250 * clouds.length * 0.3;
if (cx > -100 && cx < W + 100) {
drawCloud(cx, c.y, c.w, c.h);
}
}
// Bushes (parallax)
for (var i = 0; i < bushes.length; i++) {
var b = bushes[i];
var bx = b.x - camera.x * 0.6;
if (bx > -100 && bx < W + 100) {
drawBush(bx, H - 64, b.w, b.h);
}
}
}
function drawCloud(x, y, w, h) {
ctx.beginPath();
ctx.arc(x + w * 0.3, y + h * 0.5, h * 0.5, 0, Math.PI * 2);
ctx.arc(x + w * 0.6, y + h * 0.3, h * 0.6, 0, Math.PI * 2);
ctx.arc(x + w * 0.85, y + h * 0.5, h * 0.45, 0, Math.PI * 2);
ctx.fill();
}
function drawBush(x, y, w, h) {
ctx.fillStyle = "#228B22";
ctx.beginPath();
ctx.arc(x + w * 0.3, y, h * 0.7, 0, Math.PI * 2);
ctx.arc(x + w * 0.65, y - h * 0.1, h * 0.8, 0, Math.PI * 2);
ctx.arc(x + w * 0.9, y, h * 0.6, 0, Math.PI * 2);
ctx.fill();
}
function drawTile(tx, ty, type) {
var x = tx * TILE - camera.x;
var y = ty * TILE;
if (x < -TILE || x > W + TILE) return;
switch (type) {
case 1: // Ground
if (ty === LEVEL_H - 2) {
// Top ground row (grass)
ctx.fillStyle = "#8B4513";
ctx.fillRect(x, y + 4, TILE, TILE - 4);
ctx.fillStyle = "#228B22";
ctx.fillRect(x, y, TILE, 6);
// Grass detail
ctx.fillStyle = "#32CD32";
for (var gx = 0; gx < 4; gx++) {
ctx.fillRect(x + gx * 8 + 2, y - 2, 2, 4);
}
} else {
ctx.fillStyle = "#8B4513";
ctx.fillRect(x, y, TILE, TILE);
// Dirt pattern
ctx.fillStyle = "#A0522D";
ctx.fillRect(x + 4, y + 4, 8, 8);
ctx.fillRect(x + 20, y + 16, 8, 8);
}
break;
case 2: // Brick
ctx.fillStyle = "#C84C09";
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = "#8B3000";
ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 0.5, TILE - 1, TILE - 1);
// Brick lines
ctx.fillStyle = "#8B3000";
ctx.fillRect(x, y + TILE / 2 - 1, TILE, 2);
ctx.fillRect(x + TILE / 2 - 1, y, 2, TILE / 2);
ctx.fillRect(x + TILE / 4 - 1, y + TILE / 2, 2, TILE / 2);
ctx.fillRect(x + TILE * 3 / 4 - 1, y + TILE / 2, 2, TILE / 2);
break;
case 3: // Question block
ctx.fillStyle = "#FFB800";
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = "#CC8800";
ctx.lineWidth = 2;
ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
// Question mark
ctx.fillStyle = "#8B4513";
ctx.font = "bold 18px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", x + TILE / 2, y + TILE / 2 + 1);
break;
case 4: // Pipe body
ctx.fillStyle = "#00A800";
ctx.fillRect(x + 2, y, TILE - 4, TILE);
ctx.fillStyle = "#00D800";
ctx.fillRect(x + 4, y, 8, TILE);
ctx.fillStyle = "#006800";
ctx.fillRect(x + TILE - 6, y, 4, TILE);
break;
case 5: // Pipe top
ctx.fillStyle = "#00A800";
ctx.fillRect(x - 2, y, TILE + 4, TILE);
ctx.fillStyle = "#00D800";
ctx.fillRect(x, y + 2, 10, TILE - 4);
ctx.fillStyle = "#006800";
ctx.fillRect(x + TILE - 8, y + 2, 6, TILE - 4);
ctx.strokeStyle = "#005800";
ctx.lineWidth = 2;
ctx.strokeRect(x - 2, y, TILE + 4, TILE);
break;
case 6: // Flag pole
ctx.fillStyle = "#888";
ctx.fillRect(x + TILE / 2 - 2, y, 4, TILE);
break;
case 7: // Flag top
ctx.fillStyle = "#888";
ctx.fillRect(x + TILE / 2 - 2, y, 4, TILE);
// Flag
ctx.fillStyle = "#E03030";
ctx.beginPath();
ctx.moveTo(x + TILE / 2 + 2, y);
ctx.lineTo(x + TILE / 2 + 22, y + 8);
ctx.lineTo(x + TILE / 2 + 2, y + 16);
ctx.fill();
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(x + TILE / 2, y, 4, 0, Math.PI * 2);
ctx.fill();
break;
}
}
function drawPlayer() {
if (player.dead && gameState === "dead") {
// Death animation - rising
ctx.save();
ctx.translate(player.x - camera.x + player.w / 2, player.y + player.h / 2);
ctx.rotate(player.deathTimer * 0.05);
drawPlayerSprite(-player.w / 2, -player.h / 2);
ctx.restore();
return;
}
if (gameState === "gameover") return;
// Blink when invincible
if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;
var px = player.x - camera.x;
var py = player.y;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
if (player.facing === -1) ctx.scale(-1, 1);
drawPlayerSprite(-player.w / 2, -player.h / 2);
ctx.restore();
}
function drawPlayerSprite(x, y) {
// Hat
ctx.fillStyle = "#E03030";
ctx.fillRect(x + 2, y, 20, 8);
ctx.fillRect(x, y + 2, 24, 4);
// Face
ctx.fillStyle = "#FFCC99";
ctx.fillRect(x + 4, y + 8, 16, 10);
// Eye
ctx.fillStyle = "#000";
ctx.fillRect(x + 14, y + 10, 3, 3);
// Mustache
ctx.fillStyle = "#4A2800";
ctx.fillRect(x + 10, y + 15, 10, 2);
// Body / overalls
ctx.fillStyle = "#E03030";
ctx.fillRect(x + 2, y + 18, 20, 4);
ctx.fillStyle = "#0050D0";
ctx.fillRect(x + 2, y + 20, 20, 6);
// Buttons
ctx.fillStyle = "#FFD700";
ctx.fillRect(x + 6, y + 21, 2, 2);
ctx.fillRect(x + 16, y + 21, 2, 2);
// Legs
var legOffset = 0;
if (!player.onGround) {
legOffset = 2; // tucked legs when jumping
} else if (Math.abs(player.vx) > 0.5) {
legOffset = Math.sin(player.walkFrame * Math.PI / 2) * 3;
}
ctx.fillStyle = "#0050D0";
ctx.fillRect(x + 2, y + 26 - legOffset, 8, 4 + legOffset);
ctx.fillRect(x + 14, y + 26 + legOffset, 8, 4 - legOffset);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(x + 1, y + 28, 9, 2);
ctx.fillRect(x + 14, y + 28, 9, 2);
}
function drawCoin(c) {
var x = c.x - camera.x;
var y = c.y;
if (x < -20 || x > W + 20) return;
var scaleX = Math.cos(c.animFrame * 3);
ctx.save();
ctx.translate(x + 8, y + 8);
ctx.scale(scaleX, 1);
// Coin body
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFA500";
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
var x = e.x - camera.x;
var y = e.y;
if (x < -TILE || x > W + TILE) return;
if (!e.alive) {
if (e.squishTimer > 0) {
// Squished
ctx.fillStyle = "#8B4513";
ctx.fillRect(x + 2, y + e.h - 8, e.w - 4, 8);
}
return;
}
// Body
ctx.fillStyle = "#8B4513";
ctx.fillRect(x + 2, y + 4, e.w - 4, e.h - 4);
// Head
ctx.fillStyle = "#A0522D";
ctx.beginPath();
ctx.arc(x + e.w / 2, y + 4, e.w / 2 - 2, Math.PI, 0);
ctx.fill();
// Eyes
var eyeBounce = Math.sin(e.animFrame * 4) * 1;
ctx.fillStyle = "#FFF";
ctx.fillRect(x + 6, y + 6 + eyeBounce, 6, 6);
ctx.fillRect(x + e.w - 12, y + 6 + eyeBounce, 6, 6);
ctx.fillStyle = "#000";
ctx.fillRect(x + 8, y + 8 + eyeBounce, 3, 3);
ctx.fillRect(x + e.w - 10, y + 8 + eyeBounce, 3, 3);
// Feet
var footAnim = Math.sin(e.animFrame * 6) * 2;
ctx.fillStyle = "#000";
ctx.fillRect(x + 2, y + e.h - 4 + footAnim, 8, 4);
ctx.fillRect(x + e.w - 10, y + e.h - 4 - footAnim, 8, 4);
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
ctx.globalAlpha = p.life / 50;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - camera.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score
ctx.fillStyle = "#FFF";
ctx.font = "bold 16px Courier New";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText("SCORE: " + score, 10, 10);
// Coins
ctx.fillStyle = "#FFD700";
ctx.fillText("COINS: " + coinCount + " / " + totalCoins, 10, 30);
// Lives
ctx.fillStyle = "#E03030";
ctx.fillText("LIVES: " + lives, 10, 50);
// Controls hint
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.font = "11px Courier New";
ctx.textAlign = "right";
ctx.fillText("Arrow Keys / WASD: Move | Space / Up: Jump", W - 10, 10);
}
function drawWinScreen() {
player.winTimer--;
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("YOU WIN!", W / 2, H / 2 - 30);
ctx.fillStyle = "#FFF";
ctx.font = "20px Courier New";
ctx.fillText("Score: " + score, W / 2, H / 2 + 20);
ctx.fillText("Coins: " + coinCount + " / " + totalCoins, W / 2, H / 2 + 50);
if (player.winTimer <= 0) {
ctx.fillStyle = "#AAA";
ctx.font = "14px Courier New";
ctx.fillText("Press R to restart", W / 2, H / 2 + 90);
}
}
function drawGameOverScreen() {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#E03030";
ctx.font = "bold 48px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("GAME OVER", W / 2, H / 2 - 20);
ctx.fillStyle = "#FFF";
ctx.font = "20px Courier New";
ctx.fillText("Final Score: " + score, W / 2, H / 2 + 20);
ctx.fillStyle = "#AAA";
ctx.font = "14px Courier New";
ctx.fillText("Press R to restart", W / 2, H / 2 + 60);
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFF";
ctx.font = "bold 24px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Lives remaining: " + lives, W / 2, H / 2);
}
// ─── Restart ───
function resetGame() {
levelMap = buildLevel();
coins = buildCoins();
enemies = buildEnemies();
particles = [];
player = {
x: 3 * TILE,
y: (LEVEL_H - 4) * TILE,
w: 24,
h: 30,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
walkFrame: 0,
walkTimer: 0,
dead: false,
deathTimer: 0,
invincible: 0,
winTimer: 0,
won: false
};
camera = { x: 0, y: 0 };
score = 0;
coinCount = 0;
lives = 3;
gameState = "play";
}
// ─── Main Loop ───
function update() {
gameTime++;
if (gameState === "play") {
updatePlayer();
updateEnemies();
updateCoins();
updateCamera();
} else if (gameState === "dead") {
player.vy += GRAVITY;
player.y += player.vy;
player.deathTimer--;
if (player.deathTimer <= 0) {
gameState = "play";
player.x = 3 * TILE;
player.y = (LEVEL_H - 4) * TILE;
player.vx = 0;
player.vy = 0;
player.dead = false;
player.invincible = 60;
camera.x = 0;
}
} else if (gameState === "win") {
updateParticles();
}
updateParticles();
// Restart
if (keyJustPressed["KeyR"]) {
resetGame();
}
clearJustPressed();
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
// Draw tiles in view
var startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
var endTX = Math.min(LEVEL_W, Math.ceil((camera.x + W) / TILE) + 1);
for (var ty = 0; ty < LEVEL_H; ty++) {
for (var tx = startTX; tx < endTX; tx++) {
var tile = levelMap[ty][tx];
if (tile > 0) {
drawTile(tx, ty, tile);
}
}
}
// Draw coins
for (var i = 0; i < coins.length; i++) {
if (!coins[i].collected) drawCoin(coins[i]);
}
// Draw enemies
for (var i = 0; i < enemies.length; i++) {
drawEnemy(enemies[i]);
}
// Draw player
drawPlayer();
// Draw particles
drawParticles();
// HUD
drawHUD();
// Overlays
if (gameState === "win") drawWinScreen();
if (gameState === "gameover") drawGameOverScreen();
if (gameState === "dead") drawDeathScreen();
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ─── Start ───
gameLoop();
})();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Canvas Bros</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
background: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
overflow: hidden;
font-family: monospace;
}
#game {
border: 3px solid #333;
border-radius: 4px;
image-rendering: pixelated;
cursor: default;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
// ── Constants ──────────────────────────────────────────────
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.65;
const MAX_SPEED = 4.5;
const JUMP_FORCE = -10.5;
const BOUNCE_FORCE = -7;
const TILE = 32;
// ── Input ──────────────────────────────────────────────────
const keys = {};
window.addEventListener("keydown", e => {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code))
e.preventDefault();
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
// ── Camera ─────────────────────────────────────────────────
const cam = { x: 0, y: 0 };
// ── Level Data ─────────────────────────────────────────────
// Legend: 1=ground, 2=brick, 3=question block, 4=pipe body, 5=pipe top, 6=hard block
// Platforms are defined separately for clarity
const LEVEL_WIDTH = 120; // tiles
const LEVEL_HEIGHT = 14; // tiles (450/32 ≈ 14)
// Build a 2D tile map (0 = air)
const tiles = [];
for (let y = 0; y < LEVEL_HEIGHT; y++) {
tiles[y] = [];
for (let x = 0; x < LEVEL_WIDTH; x++) {
tiles[y][x] = 0;
}
}
// Ground (rows 12-13 = bottom two rows)
for (let x = 0; x < LEVEL_WIDTH; x++) {
tiles[13][x] = 1;
tiles[12][x] = 1;
}
// Gaps in ground
const gaps = [[18,20],[42,44],[72,75]];
gaps.forEach(([a,b]) => {
for (let x = a; x <= b; x++) {
tiles[12][x] = 0;
tiles[13][x] = 0;
}
});
// Raised platforms (brick blocks)
const platforms = [
// [startX, y, length]
[8, 9, 5],
[15, 7, 3],
[22, 9, 4],
[28, 6, 6],
[35, 9, 3],
[38, 7, 5],
[46, 9, 4],
[50, 6, 3],
[55, 9, 6],
[62, 7, 4],
[66, 5, 3],
[76, 9, 5],
[82, 7, 3],
[86, 9, 4],
[92, 6, 5],
[98, 9, 3],
[104, 7, 4],
];
platforms.forEach(([sx, y, len]) => {
for (let i = 0; i < len; i++) {
tiles[y][sx + i] = 2;
}
});
// Question blocks (hold coins)
const questionBlocks = [
[10, 9], [24, 9], [30, 6], [48, 9], [52, 6], [58, 9],
[64, 7], [78, 9], [84, 7], [94, 6], [106, 7]
];
questionBlocks.forEach(([x, y]) => { tiles[y][x] = 3; });
// Pipes
const pipes = [
[13, 10, 2], // x, topRow, height
[33, 11, 1],
[60, 10, 2],
[88, 11, 1],
[100, 10, 2],
];
pipes.forEach(([x, topRow, h]) => {
tiles[topRow][x] = 5; // pipe top
for (let dy = 1; dy < h; dy++) {
if (topRow + dy < LEVEL_HEIGHT) tiles[topRow + dy][x] = 4; // pipe body
}
});
// Staircase near the end
for (let step = 0; step < 6; step++) {
for (let dy = 0; dy <= step; dy++) {
tiles[11 - dy][110 + step] = 6;
}
}
// ── Coins ──────────────────────────────────────────────────
const coins = [];
// Coins on platforms
platforms.forEach(([sx, y, len]) => {
for (let i = 0; i < len; i += 2) {
coins.push({ x: (sx + i) * TILE + TILE/2, y: (y - 1) * TILE + TILE/2, collected: false });
}
});
// Question block coins (tracked separately)
const qCoins = questionBlocks.map(([x, y]) => ({
x: x * TILE + TILE/2, y: (y - 1) * TILE + TILE/2, collected: false
}));
// Floating coins in arcs
const arcCoins = [
[20, 10], [21, 9], [22, 10],
[44, 10], [45, 9], [46, 10],
[74, 10], [75, 9], [76, 10],
[80, 8], [81, 7], [82, 8],
[96, 8], [97, 7], [98, 8],
];
arcCoins.forEach(([x, y]) => {
coins.push({ x: x * TILE + TILE/2, y: y * TILE + TILE/2, collected: false });
});
// ── Enemies ────────────────────────────────────────────────
const enemies = [];
function spawnEnemy(x, y) {
enemies.push({
x: x * TILE, y: y * TILE,
w: TILE - 4, h: TILE - 4,
vx: -1.2, alive: true, squishTimer: 0,
startX: x * TILE, patrolDist: 80
});
}
spawnEnemy(12, 11);
spawnEnemy(25, 11);
spawnEnemy(36, 8);
spawnEnemy(47, 11);
spawnEnemy(56, 8);
spawnEnemy(63, 11);
spawnEnemy(77, 8);
spawnEnemy(83, 11);
spawnEnemy(93, 5);
spawnEnemy(105, 11);
// ── Flag / Goal ────────────────────────────────────────────
const flagX = 116 * TILE;
const flagTop = 3 * TILE;
// ── Particles ──────────────────────────────────────────────
const particles = [];
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 5 - 1,
life: 30 + Math.random() * 20,
color,
size: 2 + Math.random() * 3
});
}
}
// ── Player ─────────────────────────────────────────────────
const player = {
x: 3 * TILE, y: 10 * TILE,
w: 22, h: 28,
vx: 0, vy: 0,
onGround: false,
facing: 1,
walkFrame: 0,
walkTimer: 0,
jumpHeld: false,
coyoteTime: 0,
jumpBuffered: false,
invincible: 0,
dead: false,
winTimer: 0,
};
let score = 0;
let coinCount = 0;
let gameState = "playing"; // playing, won, dead
let deathTimer = 0;
// ── Collision helpers ──────────────────────────────────────
function isSolid(tx, ty) {
if (tx < 0 || tx >= LEVEL_WIDTH || ty < 0 || ty >= LEVEL_HEIGHT) return ty >= LEVEL_HEIGHT;
const t = tiles[ty][tx];
return t === 1 || t === 2 || t === 3 || t === 4 || t === 5 || t === 6;
}
function resolveX(entity) {
const left = Math.floor(entity.x / TILE);
const right = Math.floor((entity.x + entity.w - 1) / TILE);
const top = Math.floor(entity.y / TILE);
const bot = Math.floor((entity.y + entity.h - 1) / TILE);
for (let ty = top; ty <= bot; ty++) {
for (let tx = left; tx <= right; tx++) {
if (isSolid(tx, ty)) {
if (entity.vx > 0) {
entity.x = tx * TILE - entity.w;
entity.vx = 0;
} else if (entity.vx < 0) {
entity.x = (tx + 1) * TILE;
entity.vx = 0;
}
return;
}
}
}
}
function resolveY(entity) {
const left = Math.floor(entity.x / TILE);
const right = Math.floor((entity.x + entity.w - 1) / TILE);
const top = Math.floor(entity.y / TILE);
const bot = Math.floor((entity.y + entity.h - 1) / TILE);
entity.onGround = false;
for (let ty = top; ty <= bot; ty++) {
for (let tx = left; tx <= right; tx++) {
if (isSolid(tx, ty)) {
if (entity.vy > 0) {
entity.y = ty * TILE - entity.h;
entity.vy = 0;
entity.onGround = true;
} else if (entity.vy < 0) {
entity.y = (ty + 1) * TILE;
entity.vy = 0;
// Hit question block from below
if (tiles[ty][tx] === 3) {
tiles[ty][tx] = 6; // become hard block
coinCount++;
score += 100;
spawnParticles(tx * TILE + TILE/2, ty * TILE, "#FFD700", 8);
// Find matching qCoin
for (const qc of qCoins) {
const qtx = Math.floor(qc.x / TILE);
const qty = Math.floor((qc.y + TILE/2) / TILE);
if (qtx === tx && qty === ty + 1 && !qc.collected) {
qc.collected = true;
break;
}
}
}
}
return;
}
}
}
}
// ── Drawing helpers ────────────────────────────────────────
function drawSky() {
// Gradient sky
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#5C94FC");
grad.addColorStop(0.6, "#87CEEB");
grad.addColorStop(1, "#B0E0FF");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255,255,255,0.9)";
const clouds = [
[100, 50, 60], [350, 35, 80], [600, 65, 50], [900, 45, 70],
[1200, 55, 55], [1500, 30, 75], [1800, 60, 65], [2100, 40, 80],
[2400, 50, 50], [2700, 35, 70], [3000, 60, 55], [3300, 45, 65],
[3600, 55, 75], [3900, 40, 60], [4200, 50, 70],
];
clouds.forEach(([cx, cy, cw]) => {
const px = cx - cam.x * 0.3;
if (px > -100 && px < W + 100) {
ctx.beginPath();
ctx.ellipse(px, cy, cw/2, 14, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(px - cw*0.25, cy + 5, cw*0.3, 10, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(px + cw*0.25, cy + 3, cw*0.25, 11, 0, 0, Math.PI*2);
ctx.fill();
}
});
// Hills (parallax)
ctx.fillStyle = "#4AA52E";
const hills = [
[80, 140, 100], [400, 90, 60], [700, 120, 80], [1000, 110, 90],
[1400, 130, 70], [1700, 85, 55], [2000, 115, 95], [2400, 100, 75],
[2800, 125, 85], [3200, 95, 65], [3600, 110, 80], [4000, 105, 70],
];
hills.forEach(([hx, hw, hh]) => {
const px = hx - cam.x * 0.5;
if (px > -200 && px < W + 200) {
ctx.beginPath();
ctx.ellipse(px, H - 64, hw, hh, 0, Math.PI, 0);
ctx.fill();
}
});
// Bushes (parallax)
ctx.fillStyle = "#2D8B2D";
const bushes = [
[150, 40], [500, 30], [850, 45], [1100, 35], [1500, 40],
[1900, 30], [2200, 45], [2600, 35], [3000, 40], [3400, 30],
[3800, 45], [4100, 35],
];
bushes.forEach(([bx, bw]) => {
const px = bx - cam.x * 0.6;
if (px > -80 && px < W + 80) {
ctx.beginPath();
ctx.ellipse(px, H - 66, bw, 12, 0, Math.PI, 0);
ctx.fill();
}
});
}
function drawTile(tx, ty) {
const t = tiles[ty][tx];
if (t === 0) return;
const x = tx * TILE - cam.x;
const y = ty * TILE - cam.y;
if (x < -TILE || x > W + TILE || y < -TILE || y > H + TILE) return;
switch (t) {
case 1: // Ground
ctx.fillStyle = "#C84C09";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#E07020";
ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
// Grass top
if (ty === 0 || tiles[ty-1][tx] === 0) {
ctx.fillStyle = "#4AA52E";
ctx.fillRect(x, y, TILE, 6);
ctx.fillStyle = "#5CC03A";
for (let i = 0; i < 4; i++) {
ctx.fillRect(x + i*8 + 1, y - 2, 4, 4);
}
}
break;
case 2: // Brick
ctx.fillStyle = "#C84C09";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#E07020";
ctx.fillRect(x + 1, y + 1, 14, 14);
ctx.fillRect(x + 17, y + 1, 14, 14);
ctx.fillRect(x + 1, y + 17, 14, 14);
ctx.fillRect(x + 17, y + 17, 14, 14);
break;
case 3: // Question block
ctx.fillStyle = "#E8A000";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#FFD700";
ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
ctx.fillStyle = "#E8A000";
ctx.font = "bold 18px monospace";
ctx.textAlign = "center";
ctx.fillText("?", x + TILE/2, y + TILE/2 + 6);
break;
case 4: // Pipe body
ctx.fillStyle = "#20A020";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#30C030";
ctx.fillRect(x + 4, y, 12, TILE);
ctx.fillStyle = "#188018";
ctx.fillRect(x + TILE - 6, y, 4, TILE);
break;
case 5: // Pipe top
ctx.fillStyle = "#20A020";
ctx.fillRect(x - 2, y, TILE + 4, TILE);
ctx.fillStyle = "#30C030";
ctx.fillRect(x + 2, y + 2, 14, TILE - 2);
ctx.fillStyle = "#188018";
ctx.fillRect(x + TILE - 4, y + 2, 4, TILE - 2);
break;
case 6: // Hard block
ctx.fillStyle = "#888";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#999";
ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
ctx.fillStyle = "#777";
ctx.fillRect(x + 1, y + 1, TILE - 2, 2);
ctx.fillRect(x + 1, y + 1, 2, TILE - 2);
break;
}
}
function drawPlayer() {
if (player.dead) return;
const px = player.x - cam.x;
const py = player.y - cam.y;
// Invincibility flash
if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;
ctx.save();
ctx.translate(px + player.w/2, py + player.h/2);
ctx.scale(player.facing, 1);
ctx.translate(-player.w/2, -player.h/2);
// Body
ctx.fillStyle = "#E02020"; // Red overalls
ctx.fillRect(2, 10, player.w - 4, player.h - 10);
// Head
ctx.fillStyle = "#FDBA74"; // Skin
ctx.fillRect(3, 0, player.w - 6, 12);
// Hat
ctx.fillStyle = "#E02020";
ctx.fillRect(1, -2, player.w - 2, 7);
ctx.fillRect(4, -4, player.w - 8, 4);
// Eyes
ctx.fillStyle = "#000";
ctx.fillRect(player.w - 10, 3, 3, 4);
// Legs (animated)
const legOffset = player.onGround && Math.abs(player.vx) > 0.5
? Math.sin(player.walkTimer * 0.3) * 3 : 0;
ctx.fillStyle = "#E02020";
ctx.fillRect(2, player.h - 6, 8, 6);
ctx.fillRect(player.w - 10, player.h - 6, 8, 6);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(1, player.h - 3 + legOffset, 9, 3);
ctx.fillRect(player.w - 10, player.h - 3 - legOffset, 9, 3);
ctx.restore();
}
function drawEnemy(e) {
if (!e.alive) {
if (e.squishTimer > 0) {
const ex = e.x - cam.x;
const ey = e.y - cam.y;
ctx.fillStyle = "#8B4513";
ctx.fillRect(ex, ey + e.h - 8, e.w, 8);
}
return;
}
const ex = e.x - cam.x;
const ey = e.y - cam.y;
if (ex < -TILE || ex > W + TILE) return;
// Goomba-like body
ctx.fillStyle = "#8B4513";
ctx.beginPath();
ctx.ellipse(ex + e.w/2, ey + e.h/2 + 2, e.w/2, e.h/2 - 2, 0, 0, Math.PI*2);
ctx.fill();
// Feet
const footAnim = Math.sin(Date.now() * 0.008) * 3;
ctx.fillStyle = "#222";
ctx.fillRect(ex + 2, ey + e.h - 4 + footAnim, 8, 4);
ctx.fillRect(ex + e.w - 10, ey + e.h - 4 - footAnim, 8, 4);
// Eyes
ctx.fillStyle = "#FFF";
ctx.fillRect(ex + 5, ey + 6, 6, 7);
ctx.fillRect(ex + e.w - 11, ey + 6, 6, 7);
ctx.fillStyle = "#000";
ctx.fillRect(ex + 7, ey + 8, 3, 5);
ctx.fillRect(ex + e.w - 9, ey + 8, 3, 5);
// Angry eyebrows
ctx.fillStyle = "#000";
ctx.fillRect(ex + 4, ey + 4, 7, 2);
ctx.fillRect(ex + e.w - 11, ey + 4, 7, 2);
}
function drawCoin(cx, cy) {
const bob = Math.sin(Date.now() * 0.005 + cx * 0.1) * 3;
const stretch = Math.abs(Math.cos(Date.now() * 0.003 + cx * 0.05));
ctx.save();
ctx.translate(cx, cy + bob);
ctx.scale(stretch * 0.5 + 0.5, 1);
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.ellipse(0, 0, 8, 10, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#FFA500";
ctx.beginPath();
ctx.ellipse(0, 0, 5, 7, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#FFD700";
ctx.font = "bold 10px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 1);
ctx.restore();
}
function drawFlag() {
const fx = flagX - cam.x;
const fy = flagTop - cam.y;
if (fx < -50 || fx > W + 50) return;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(fx + 14, fy, 4, 12 * TILE - flagTop);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(fx + 16, fy, 6, 0, Math.PI*2);
ctx.fill();
// Flag
const wave = Math.sin(Date.now() * 0.004) * 3;
ctx.fillStyle = "#E02020";
ctx.beginPath();
ctx.moveTo(fx + 18, fy + 5);
ctx.lineTo(fx + 50 + wave, fy + 18);
ctx.lineTo(fx + 18, fy + 35);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFD700";
ctx.font = "14px monospace";
ctx.textAlign = "center";
ctx.fillText("★", fx + 33 + wave/2, fy + 24);
}
function drawHUD() {
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(0, 0, W, 36);
ctx.fillStyle = "#FFF";
ctx.font = "bold 16px monospace";
ctx.textAlign = "left";
ctx.fillText("SCORE", 16, 16);
ctx.fillStyle = "#FFD700";
ctx.fillText(String(score).padStart(6, "0"), 16, 30);
ctx.fillStyle = "#FFF";
ctx.textAlign = "center";
ctx.fillText("COINS", W/2 - 60, 16);
ctx.fillStyle = "#FFD700";
ctx.fillText("× " + coinCount, W/2 - 60, 30);
ctx.fillStyle = "#FFF";
ctx.textAlign = "right";
ctx.fillText("WORLD 1-1", W - 16, 24);
}
function drawParticles() {
particles.forEach(p => {
ctx.globalAlpha = p.life / 50;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - cam.x - p.size/2, p.y - cam.y - p.size/2, p.size, p.size);
});
ctx.globalAlpha = 1;
}
// ── Game Logic ─────────────────────────────────────────────
function resetPlayer() {
player.x = 3 * TILE;
player.y = 10 * TILE;
player.vx = 0;
player.vy = 0;
player.dead = false;
player.invincible = 0;
player.winTimer = 0;
cam.x = 0;
cam.y = 0;
gameState = "playing";
}
function update() {
if (gameState === "dead") {
deathTimer--;
if (deathTimer <= 0) resetPlayer();
return;
}
if (gameState === "won") {
player.winTimer++;
return;
}
// ── Player input ──
const moveLeft = keys["ArrowLeft"] || keys["KeyA"];
const moveRight = keys["ArrowRight"] || keys["KeyD"];
const jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
// Horizontal movement
if (moveLeft) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
}
if (moveRight) {
player.vx += MOVE_ACCEL;
player.facing = 1;
}
// Clamp speed
if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
// Apply friction
if (!moveLeft && !moveRight) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Coyote time (brief grace period after leaving edge)
if (player.onGround) {
player.coyoteTime = 6;
} else {
player.coyoteTime--;
}
// Jump
if (jumpKey && !player.jumpHeld) {
player.jumpBuffered = true;
}
if (!jumpKey) {
player.jumpBuffered = false;
}
player.jumpHeld = jumpKey;
if (player.jumpBuffered && player.coyoteTime > 0) {
player.vy = JUMP_FORCE;
player.coyoteTime = 0;
player.jumpBuffered = false;
}
// Variable jump height (release to fall faster)
if (!jumpKey && player.vy < -2) {
player.vy *= 0.85;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 12) player.vy = 12;
// Walk animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.walkTimer++;
}
// Move player X
player.x += player.vx;
resolveX(player);
// Move player Y
player.y += player.vy;
resolveY(player);
// Prevent going left of level start
if (player.x < 0) player.x = 0;
// Fall into pit
if (player.y > H + 50) {
playerDie();
}
// Invincibility timer
if (player.invincible > 0) player.invincible--;
// ── Coin collection ──
coins.forEach(c => {
if (c.collected) return;
const dx = (player.x + player.w/2) - c.x;
const dy = (player.y + player.h/2) - c.y;
if (Math.abs(dx) < 18 && Math.abs(dy) < 18) {
c.collected = true;
coinCount++;
score += 50;
spawnParticles(c.x, c.y, "#FFD700", 6);
}
});
// Question block coins
qCoins.forEach(c => {
if (c.collected) return;
const dx = (player.x + player.w/2) - c.x;
const dy = (player.y + player.h/2) - c.y;
if (Math.abs(dx) < 18 && Math.abs(dy) < 18) {
c.collected = true;
coinCount++;
score += 50;
spawnParticles(c.x, c.y, "#FFD700", 6);
}
});
// ── Enemy logic ──
enemies.forEach(e => {
if (!e.alive) {
if (e.squishTimer > 0) e.squishTimer--;
return;
}
// Patrol
e.x += e.vx;
if (Math.abs(e.x - e.startX) > e.patrolDist) {
e.vx *= -1;
}
// Gravity for enemies
const eBot = Math.floor((e.y + e.h) / TILE);
const eMid = Math.floor((e.x + e.w/2) / TILE);
if (!isSolid(eMid, eBot)) {
e.y += 3;
}
// Player collision
if (player.invincible <= 0) {
const overlapX = Math.min(player.x + player.w, e.x + e.w) - Math.max(player.x, e.x);
const overlapY = Math.min(player.y + player.h, e.y + e.h) - Math.max(player.y, e.y);
if (overlapX > 4 && overlapY > 4) {
// Check if player is stomping
if (player.vy > 0 && player.y + player.h - 10 < e.y + e.h/2) {
e.alive = false;
e.squishTimer = 30;
player.vy = BOUNCE_FORCE;
score += 200;
spawnParticles(e.x + e.w/2, e.y, "#8B4513", 8);
} else {
playerDie();
}
}
}
});
// ── Flag / Win condition ──
if (player.x + player.w > flagX && player.x < flagX + 32) {
gameState = "won";
score += 1000;
spawnParticles(flagX + 16, flagTop + 20, "#FFD700", 20);
spawnParticles(flagX + 16, flagTop + 20, "#E02020", 15);
}
// ── Camera ──
const targetX = player.x - W / 3;
const targetY = Math.min(player.y - H / 2, 0);
cam.x += (targetX - cam.x) * 0.1;
cam.y += (targetY - cam.y) * 0.1;
if (cam.x < 0) cam.x = 0;
if (cam.x > (LEVEL_WIDTH * TILE - W)) cam.x = LEVEL_WIDTH * TILE - W;
if (cam.y > 0) cam.y = 0;
// ── Particles ──
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
function playerDie() {
if (player.dead) return;
player.dead = true;
gameState = "dead";
deathTimer = 90;
player.vy = -8;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#E02020", 12);
}
// ── Render ─────────────────────────────────────────────────
function render() {
ctx.clearRect(0, 0, W, H);
drawSky();
// Draw tiles
const startTX = Math.max(0, Math.floor(cam.x / TILE) - 1);
const endTX = Math.min(LEVEL_WIDTH - 1, Math.ceil((cam.x + W) / TILE) + 1);
const startTY = Math.max(0, Math.floor(cam.y / TILE) - 1);
const endTY = Math.min(LEVEL_HEIGHT - 1, Math.ceil((cam.y + H) / TILE) + 1);
for (let ty = startTY; ty <= endTY; ty++) {
for (let tx = startTX; tx <= endTX; tx++) {
drawTile(tx, ty);
}
}
// Draw coins
coins.forEach(c => {
if (!c.collected) drawCoin(c.x - cam.x, c.y - cam.y);
});
qCoins.forEach(c => {
if (!c.collected) drawCoin(c.x - cam.x, c.y - cam.y);
});
// Draw flag
drawFlag();
// Draw enemies
enemies.forEach(e => drawEnemy(e));
// Draw player
drawPlayer();
// Draw particles
drawParticles();
// Draw HUD
drawHUD();
// Game states
if (gameState === "dead") {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#E02020";
ctx.font = "bold 36px monospace";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", W/2, H/2 - 10);
ctx.fillStyle = "#FFF";
ctx.font = "16px monospace";
ctx.fillText("Restarting...", W/2, H/2 + 25);
}
if (gameState === "won") {
const alpha = Math.min(1, player.winTimer / 60);
ctx.fillStyle = `rgba(0,0,0,${alpha * 0.5})`;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 40px monospace";
ctx.textAlign = "center";
ctx.fillText("★ YOU WIN! ★", W/2, H/2 - 20);
ctx.fillStyle = "#FFF";
ctx.font = "20px monospace";
ctx.fillText("Score: " + score, W/2, H/2 + 20);
ctx.fillText("Coins: " + coinCount, W/2, H/2 + 48);
if (player.winTimer > 120) {
ctx.fillStyle = "#AAA";
ctx.font = "14px monospace";
ctx.fillText("Press R to restart", W/2, H/2 + 80);
if (keys["KeyR"]) {
score = 0;
coinCount = 0;
// Reset coins
coins.forEach(c => c.collected = false);
qCoins.forEach(c => c.collected = false);
// Reset enemies
enemies.length = 0;
spawnEnemy(12, 11);
spawnEnemy(25, 11);
spawnEnemy(36, 8);
spawnEnemy(47, 11);
spawnEnemy(56, 8);
spawnEnemy(63, 11);
spawnEnemy(77, 8);
spawnEnemy(83, 11);
spawnEnemy(93, 5);
spawnEnemy(105, 11);
// Reset question blocks
questionBlocks.forEach(([x, y]) => { tiles[y][x] = 3; });
resetPlayer();
}
}
}
}
// ── Game Loop ──────────────────────────────────────────────
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// Start!
gameLoop();
</script>
</body>
</html>