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">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
font-family: monospace;
}
canvas {
border: 3px solid #e94560;
border-radius: 4px;
display: block;
image-rendering: pixelated;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 800, H = 450;
// ── 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 ─────────────────────────────────────────
const TILE = 32;
const LEVEL_W = 200; // tiles wide
const LEVEL_H = 14; // tiles tall (450/32 ≈ 14)
// tile types: 0=air, 1=ground, 2=brick, 3=coin, 4=goal_flag, 5=pipe, 6=question
const levelData = [];
for (let y = 0; y < LEVEL_H; y++) {
levelData[y] = new Array(LEVEL_W).fill(0);
}
// ground (rows 12-13)
for (let x = 0; x < LEVEL_W; x++) {
// gaps
if ((x >= 25 && x <= 27) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) continue;
levelData[12][x] = 1;
levelData[13][x] = 1;
}
// raised platforms (row 9)
for (let x = 8; x <= 13; x++) levelData[9][x] = 2;
for (let x = 20; x <= 26; x++) levelData[9][x] = 2;
for (let x = 35; x <= 42; x++) levelData[9][x] = 2;
for (let x = 48; x <= 54; x++) levelData[9][x] = 2;
for (let x = 65; x <= 72; x++) levelData[9][x] = 2;
for (let x = 78; x <= 85; x++) levelData[9][x] = 2;
for (let x = 98; x <= 105; x++) levelData[9][x] = 2;
for (let x = 110; x <= 116; x++) levelData[9][x] = 2;
for (let x = 120; x <= 128; x++) levelData[9][x] = 2;
for (let x = 140; x <= 148; x++) levelData[9][x] = 2;
// higher platforms (row 6)
for (let x = 15; x <= 18; x++) levelData[6][x] = 2;
for (let x = 40; x <= 44; x++) levelData[6][x] = 2;
for (let x = 70; x <= 74; x++) levelData[6][x] = 2;
for (let x = 100; x <= 104; x++) levelData[6][x] = 2;
for (let x = 135; x <= 139; x++) levelData[6][x] = 2;
// coins on platforms
const coinPositions = [];
function placeCoins(row, startX, count) {
for (let i = 0; i < count; i++) {
const x = startX + i * 2;
if (x < LEVEL_W) { levelData[row][x] = 3; coinPositions.push({x, y: row, collected: false}); }
}
}
placeCoins(8, 9, 3);
placeCoins(8, 21, 4);
placeCoins(8, 36, 3);
placeCoins(8, 49, 5);
placeCoins(8, 66, 4);
placeCoins(8, 79, 5);
placeCoins(8, 99, 3);
placeCoins(8, 111, 4);
placeCoins(8, 121, 5);
placeCoins(8, 141, 3);
placeCoins(5, 16, 2);
placeCoins(5, 41, 3);
placeCoins(5, 71, 2);
placeCoins(5, 101, 3);
placeCoins(5, 136, 2);
// extra coins on ground
placeCoins(11, 5, 3);
placeCoins(11, 30, 4);
placeCoins(11, 60, 3);
placeCoins(11, 80, 5);
placeCoins(11, 100, 3);
placeCoins(11, 120, 4);
placeCoins(11, 150, 3);
// pipes (obstacles)
function placePipe(x, height) {
for (let dy = 0; dy < height; dy++) {
const row = 12 - height + dy;
if (row >= 0 && row < LEVEL_H) {
levelData[row][x] = 5;
levelData[row][x+1] = 5;
}
}
}
placePipe(18, 2);
placePipe(45, 3);
placePipe(75, 2);
placePipe(108, 3);
placePipe(145, 2);
// question blocks
levelData[8][11] = 6;
levelData[8][50] = 6;
levelData[8][81] = 6;
levelData[8][113] = 6;
// goal flag
levelData[4][160] = 4;
levelData[5][160] = 4;
levelData[6][160] = 4;
levelData[7][160] = 4;
levelData[8][160] = 4;
levelData[9][160] = 4;
levelData[10][160] = 4;
levelData[11][160] = 4;
levelData[12][160] = 1; // base
levelData[13][160] = 1;
// ── Player ─────────────────────────────────────────────
const player = {
x: 3 * TILE,
y: 10 * TILE,
w: 24,
h: 30,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
score: 0,
lives: 3,
dead: false,
win: false,
animFrame: 0,
animTimer: 0,
jumpHeld: false,
coyoteTime: 0,
jumpBuffer: 0
};
// ── Camera ─────────────────────────────────────────────
const camera = { x: 0, y: 0 };
// ── 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) * 6,
vy: (Math.random() - 1) * 5,
life: 30 + Math.random() * 20,
color,
size: 2 + Math.random() * 3
});
}
}
// ── Floating score texts ───────────────────────────────
const floatingTexts = [];
function spawnFloatingText(x, y, text, color) {
floatingTexts.push({ x, y, text, color, life: 40 });
}
// ── Physics constants ──────────────────────────────────
const GRAVITY = 0.55;
const MAX_FALL = 12;
const MOVE_ACCEL = 0.6;
const MOVE_FRICTION = 0.78;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -10.5;
const JUMP_CUTOFF = -3; // when releasing jump early
const COYOTE_FRAMES = 6;
const JUMP_BUFFER_FRAMES = 8;
// ── Collision helpers ──────────────────────────────────
function getTile(tx, ty) {
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
return levelData[ty][tx];
}
function isSolid(tile) {
return tile === 1 || tile === 2 || tile === 5 || tile === 6;
}
function collides(px, py, pw, ph) {
const left = Math.floor(px / TILE);
const right = Math.floor((px + pw - 1) / TILE);
const top = Math.floor(py / TILE);
const bottom = Math.floor((py + ph - 1) / TILE);
for (let ty = top; ty <= bottom; ty++) {
for (let tx = left; tx <= right; tx++) {
if (isSolid(getTile(tx, ty))) return true;
}
}
return false;
}
// ── Update ─────────────────────────────────────────────
function update() {
if (player.dead || player.win) return;
// Input
const left = keys['ArrowLeft'] || keys['KeyA'];
const right = keys['ArrowRight'] || keys['KeyD'];
const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
// Horizontal movement
if (left) { player.vx -= MOVE_ACCEL; player.facing = -1; }
if (right) { player.vx += MOVE_ACCEL; player.facing = 1; }
if (!left && !right) player.vx *= MOVE_FRICTION;
player.vx = Math.max(-MAX_SPEED, Math.min(MAX_SPEED, player.vx));
if (Math.abs(player.vx) < 0.1) player.vx = 0;
// Coyote time & jump buffer
if (player.onGround) player.coyoteTime = COYOTE_FRAMES;
else player.coyoteTime = Math.max(0, player.coyoteTime - 1);
if (jumpKey && !player.jumpHeld) player.jumpBuffer = JUMP_BUFFER_FRAMES;
else player.jumpBuffer = Math.max(0, player.jumpBuffer - 1);
player.jumpHeld = jumpKey;
// Jump
if (player.jumpBuffer > 0 && player.coyoteTime > 0) {
player.vy = JUMP_FORCE;
player.onGround = false;
player.coyoteTime = 0;
player.jumpBuffer = 0;
spawnParticles(player.x + player.w/2, player.y + player.h, '#fff', 5);
}
// Variable jump height
if (!jumpKey && player.vy < JUMP_CUTOFF) {
player.vy = JUMP_CUTOFF;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// Move X
player.x += player.vx;
if (collides(player.x, player.y, player.w, player.h)) {
// bounce back
if (player.vx > 0) player.x = Math.floor((player.x + player.w) / TILE) * TILE - player.w;
else if (player.vx < 0) player.x = Math.floor(player.x / TILE) * TILE + TILE;
player.vx = 0;
}
// Move Y
player.y += player.vy;
player.onGround = false;
if (collides(player.x, player.y, player.w, player.h)) {
if (player.vy > 0) {
// landed
player.y = Math.floor((player.y + player.h) / TILE) * TILE - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
// hit head
player.y = Math.floor(player.y / TILE) * TILE + TILE;
player.vy = 0;
}
}
// Collect coins
const ptx = Math.floor((player.x + player.w/2) / TILE);
const pty = Math.floor((player.y + player.h/2) / TILE);
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const tx = ptx + dx, ty = pty + dy;
const tile = getTile(tx, ty);
if (tile === 3) {
levelData[ty][tx] = 0;
player.score += 100;
spawnParticles(tx * TILE + TILE/2, ty * TILE + TILE/2, '#FFD700', 8);
spawnFloatingText(tx * TILE, ty * TILE - 10, '+100', '#FFD700');
}
if (tile === 6) {
levelData[ty][tx] = 0;
player.score += 200;
spawnParticles(tx * TILE + TILE/2, ty * TILE + TILE/2, '#FF6B6B', 12);
spawnFloatingText(tx * TILE, ty * TILE - 10, '+200', '#FF6B6B');
}
}
}
// Goal check
const goalTile = getTile(160, 8);
if (goalTile === 4 || (player.x + player.w > 160 * TILE && player.x < 160 * TILE + TILE && player.y < 10 * TILE)) {
player.win = true;
player.score += 1000;
spawnParticles(160 * TILE + TILE/2, 6 * TILE, '#00FF00', 30);
}
// Fall death
if (player.y > LEVEL_H * TILE + 50) {
player.lives--;
if (player.lives <= 0) {
player.dead = true;
} else {
respawn();
}
}
// Left boundary
if (player.x < 0) player.x = 0;
// 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;
player.animTimer = 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);
}
// 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);
}
// Camera
const targetX = player.x - W * 0.35;
camera.x += (targetX - camera.x) * 0.1;
camera.x = Math.max(0, Math.min(camera.x, LEVEL_W * TILE - W));
}
function respawn() {
player.x = 3 * TILE;
player.y = 10 * TILE;
player.vx = 0;
player.vy = 0;
player.dead = false;
player.win = false;
}
// ── Drawing ────────────────────────────────────────────
function drawSky() {
// gradient sky
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#4DC9F6');
grad.addColorStop(0.7, '#87CEEB');
grad.addColorStop(1, '#B0E0E6');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawClouds() {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
const cloudPositions = [
[100, 60, 60], [350, 40, 80], [600, 70, 50], [900, 50, 70],
[1200, 65, 55], [1500, 45, 75], [1800, 55, 60], [2100, 70, 50],
[2500, 40, 65], [2800, 60, 55], [3200, 50, 70], [3600, 65, 50],
[4000, 45, 60], [4400, 55, 65], [4800, 40, 55]
];
for (const [cx, cy, size] of cloudPositions) {
const sx = cx - camera.x * 0.3;
if (sx > -150 && sx < W + 150) {
ctx.beginPath();
ctx.arc(sx, cy, size * 0.4, 0, Math.PI * 2);
ctx.arc(sx + size * 0.3, cy - size * 0.1, size * 0.35, 0, Math.PI * 2);
ctx.arc(sx + size * 0.6, cy, size * 0.3, 0, Math.PI * 2);
ctx.fill();
}
}
}
function drawHills() {
ctx.fillStyle = '#5D8A3C';
const hillData = [
[50, 120], [400, 90], [800, 130], [1300, 100], [1800, 140],
[2300, 95], [2800, 125], [3300, 105], [3800, 135], [4300, 90],
[4800, 120]
];
for (const [hx, hsize] of hillData) {
const sx = hx - camera.x * 0.5;
if (sx > -200 && sx < W + 200) {
ctx.beginPath();
ctx.moveTo(sx - hsize, H - 80);
ctx.quadraticCurveTo(sx, H - 80 - hsize, sx + hsize, H - 80);
ctx.fill();
}
}
// darker hills behind
ctx.fillStyle = '#4A7232';
const hillData2 = [
[200, 70], [600, 85], [1100, 65], [1600, 80], [2100, 75],
[2600, 90], [3100, 70], [3600, 85], [4100, 65], [4600, 80]
];
for (const [hx, hsize] of hillData2) {
const sx = hx - camera.x * 0.4;
if (sx > -200 && sx < W + 200) {
ctx.beginPath();
ctx.moveTo(sx - hsize, H - 60);
ctx.quadraticCurveTo(sx, H - 60 - hsize, sx + hsize, H - 60);
ctx.fill();
}
}
}
function drawTile(tx, ty, tile) {
const sx = tx * TILE - camera.x;
const sy = ty * TILE;
if (sx < -TILE || sx > W + TILE) return;
switch (tile) {
case 1: // ground
ctx.fillStyle = '#8B4513';
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = '#228B22';
ctx.fillRect(sx, sy, TILE, 6);
// dirt pattern
ctx.fillStyle = '#A0522D';
ctx.fillRect(sx + 4, sy + 10, 8, 6);
ctx.fillRect(sx + 18, sy + 18, 10, 6);
ctx.fillRect(sx + 8, sy + 24, 6, 6);
break;
case 2: // brick
ctx.fillStyle = '#B85C38';
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = '#8B4513';
ctx.fillRect(sx, sy, TILE, 1);
ctx.fillRect(sx, sy + TILE/2, TILE, 1);
ctx.fillRect(sx + TILE/2, sy, 1, TILE);
ctx.fillRect(sx + TILE/4, sy + TILE/2, 1, TILE/2);
ctx.fillRect(sx + TILE*3/4, sy + TILE/2, 1, TILE/2);
break;
case 3: // coin
if (sx < -TILE || sx > W + TILE) return;
const coinBob = Math.sin(Date.now() / 200 + tx) * 3;
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.ellipse(sx + TILE/2, sy + TILE/2 + coinBob, 8, 10, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.ellipse(sx + TILE/2, sy + TILE/2 + coinBob, 5, 7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.fillText('$', sx + TILE/2, sy + TILE/2 + 4 + coinBob);
break;
case 4: // goal flag
if (tx === 160 && ty === 8) {
// flag top
ctx.fillStyle = '#FF0000';
ctx.beginPath();
ctx.moveTo(sx + TILE, sy);
ctx.lineTo(sx + TILE + 20, sy + 10);
ctx.lineTo(sx + TILE, sy + 20);
ctx.fill();
// pole
ctx.fillStyle = '#654321';
ctx.fillRect(sx + TILE/2 - 2, sy - 40, 4, TILE + 40);
// ball on top
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(sx + TILE/2, sy - 42, 5, 0, Math.PI * 2);
ctx.fill();
} else if (tx === 160 && ty > 8 && ty < 12) {
// pole segments
ctx.fillStyle = '#654321';
ctx.fillRect(sx + TILE/2 - 2, sy, 4, TILE);
}
break;
case 5: // pipe
ctx.fillStyle = '#228B22';
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = '#006400';
ctx.fillRect(sx, sy, 4, TILE);
ctx.fillRect(sx + TILE - 4, sy, 4, TILE);
ctx.fillStyle = '#32CD32';
ctx.fillRect(sx + 4, sy, TILE - 8, 4);
// pipe top
if (ty === 12 || (getTile(tx, ty-1) !== 5)) {
ctx.fillStyle = '#228B22';
ctx.fillRect(sx - 4, sy, TILE + 8, 8);
ctx.fillStyle = '#32CD32';
ctx.fillRect(sx - 4, sy, TILE + 8, 3);
ctx.fillStyle = '#006400';
ctx.fillRect(sx - 4, sy + TILE - 3, TILE + 8, 3);
}
break;
case 6: // question block
const qBob = Math.sin(Date.now() / 300 + tx * 2) * 2;
ctx.fillStyle = '#FFB347';
ctx.fillRect(sx, sy + qBob, TILE, TILE);
ctx.fillStyle = '#FF8C00';
ctx.fillRect(sx, sy + qBob, TILE, 2);
ctx.fillRect(sx, sy + TILE - 2 + qBob, TILE, 2);
ctx.fillRect(sx, sy + qBob, 2, TILE);
ctx.fillRect(sx + TILE - 2, sy + qBob, 2, TILE);
ctx.fillStyle = '#FFF';
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'center';
ctx.fillText('?', sx + TILE/2, sy + TILE/2 + 6 + qBob);
break;
}
}
function drawPlayer() {
const sx = player.x - camera.x;
const sy = player.y;
const f = player.facing;
ctx.save();
ctx.translate(sx + player.w/2, sy + player.h/2);
ctx.scale(f, 1);
// Body
ctx.fillStyle = '#E94560';
ctx.fillRect(-10, -12, 20, 22);
// Head
ctx.fillStyle = '#FDBCB4';
ctx.fillRect(-8, -18, 16, 10);
// Hat
ctx.fillStyle = '#E94560';
ctx.fillRect(-9, -20, 18, 5);
ctx.fillRect(-7, -22, 14, 3);
// Eyes
ctx.fillStyle = '#000';
ctx.fillRect(1, -15, 3, 3);
// Mustache
ctx.fillStyle = '#8B4513';
ctx.fillRect(-4, -10, 10, 2);
// Legs (animated)
ctx.fillStyle = '#4169E1';
const legOffset = player.onGround ? Math.sin(player.animFrame * Math.PI / 2) * 4 : 2;
ctx.fillRect(-8, 10, 7, 6 + legOffset);
ctx.fillRect(2, 10, 7, 6 - legOffset);
// Shoes
ctx.fillStyle = '#8B4513';
ctx.fillRect(-9, 15 + legOffset, 8, 4);
ctx.fillRect(2, 15 - legOffset, 8, 4);
ctx.restore();
}
function drawHUD() {
// Score
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(10, 10, 200, 35);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'left';
ctx.fillText('🪙 COINS: ' + player.score, 20, 33);
// Lives
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(W - 150, 10, 140, 35);
ctx.fillStyle = '#FF6B6B';
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'right';
ctx.fillText('❤ LIVES: ' + player.lives, W - 20, 33);
// Controls hint
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText('Arrow Keys / WASD to move • Space / Up to jump', W/2, H - 10);
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = p.life / 50;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - camera.x, p.y, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawFloatingTexts() {
for (const ft of floatingTexts) {
ctx.globalAlpha = ft.life / 40;
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 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: ' + player.score, W/2, H/2 + 20);
ctx.fillStyle = '#AAA';
ctx.font = '16px monospace';
ctx.fillText('Press R to restart', W/2, H/2 + 60);
}
function drawGameOver() {
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 = '#AAA';
ctx.font = '18px monospace';
ctx.fillText('Press R to restart', W/2, H/2 + 30);
}
function draw() {
drawSky();
drawClouds();
drawHills();
// Draw tiles
const startTx = Math.max(0, Math.floor(camera.x / TILE) - 1);
const endTx = Math.min(LEVEL_W, Math.ceil((camera.x + W) / TILE) + 1);
for (let ty = 0; ty < LEVEL_H; ty++) {
for (let tx = startTx; tx < endTx; tx++) {
const tile = levelData[ty][tx];
if (tile !== 0) drawTile(tx, ty, tile);
}
}
drawPlayer();
drawParticles();
drawFloatingTexts();
drawHUD();
if (player.win) drawWinScreen();
if (player.dead) drawGameOver();
}
// ── Game loop ──────────────────────────────────────────
function gameLoop() {
if (keys['KeyR'] && (player.dead || player.win)) {
// restart
player.x = 3 * TILE;
player.y = 10 * TILE;
player.vx = 0;
player.vy = 0;
player.score = 0;
player.lives = 3;
player.dead = false;
player.win = false;
player.onGround = false;
player.coyoteTime = 0;
player.jumpBuffer = 0;
camera.x = 0;
particles.length = 0;
floatingTexts.length = 0;
// reset coins
for (const c of coinPositions) c.collected = false;
for (let y = 0; y < LEVEL_H; y++)
for (let x = 0; x < LEVEL_W; x++) {
// we need to re-read the original level — simplest: reload from coinPositions
}
// Rebuild level from scratch for restart
for (let y = 0; y < LEVEL_H; y++)
for (let x = 0; x < LEVEL_W; x++)
levelData[y][x] = 0;
for (let x = 0; x < LEVEL_W; x++) {
if ((x >= 25 && x <= 27) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) continue;
levelData[12][x] = 1;
levelData[13][x] = 1;
}
const platRows = [
[9, 8, 13], [9, 20, 26], [9, 35, 42], [9, 48, 54], [9, 65, 72],
[9, 78, 85], [9, 98, 105], [9, 110, 116], [9, 120, 128], [9, 140, 148],
[6, 15, 18], [6, 40, 44], [6, 70, 74], [6, 100, 104], [6, 135, 139]
];
for (const [, s, e] of platRows)
for (let x = s; x <= e; x++) levelData[9][x] = 2;
for (const [, s, e] of [[6,15,18],[6,40,44],[6,70,74],[6,100,104],[6,135,139]])
for (let x = s; x <= e; x++) levelData[6][x] = 2;
// coins
function rc(row, startX, count) {
for (let i = 0; i < count; i++) {
const x = startX + i * 2;
if (x < LEVEL_W) { levelData[row][x] = 3; coinPositions.push({x, y: row, collected: false}); }
}
}
rc(8,9,3); rc(8,21,4); rc(8,36,3); rc(8,49,5); rc(8,66,4); rc(8,79,5); rc(8,99,3); rc(8,111,4); rc(8,121,5); rc(8,141,3);
rc(5,16,2); rc(5,41,3); rc(5,71,2); rc(5,101,3); rc(5,136,2);
rc(11,5,3); rc(11,30,4); rc(11,60,3); rc(11,80,5); rc(11,100,3); rc(11,120,4); rc(11,150,3);
// pipes
function rp(x, h) {
for (let dy = 0; dy < h; dy++) {
const row = 12 - h + dy;
if (row >= 0 && row < LEVEL_H) { levelData[row][x] = 5; levelData[row][x+1] = 5; }
}
}
rp(18,2); rp(45,3); rp(75,2); rp(108,3); rp(145,2);
levelData[8][11] = 6; levelData[8][50] = 6; levelData[8][81] = 6; levelData[8][113] = 6;
levelData[4][160] = 4; levelData[5][160] = 4; levelData[6][160] = 4; levelData[7][160] = 4;
levelData[8][160] = 4; levelData[9][160] = 4; levelData[10][160] = 4; levelData[11][160] = 4;
levelData[12][160] = 1; levelData[13][160] = 1;
}
update();
draw();
requestAnimationFrame(gameLoop);
}
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%)
// ============================================================
// Super Platformer — a small Mario-style side-scroller
// ============================================================
(() => {
"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.6;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -11.5;
const COYOTE_FRAMES = 6;
const JUMP_BUFFER_FRAMES = 6;
const TILE = 40;
// ---- level data (tile map) ----
// 0=air, 1=ground, 2=brick, 3=coin, 4=flag pole, 5=goal block
const LEVEL_W = 120;
const LEVEL_H = 14;
const map = [];
for (let y = 0; y < LEVEL_H; y++) {
map[y] = [];
for (let x = 0; x < LEVEL_W; x++) {
map[y][x] = 0;
}
}
// ground (rows 12-13)
for (let x = 0; x < LEVEL_W; x++) {
// gaps
if ((x >= 28 && x <= 30) || (x >= 55 && x <= 57) || (x >= 85 && x <= 87)) continue;
map[12][x] = 1;
map[13][x] = 1;
}
// platforms
function placePlatform(y, xStart, xEnd, type) {
for (let x = xStart; x <= xEnd; x++) {
if (y >= 0 && y < LEVEL_H && x >= 0 && x < LEVEL_W) {
map[y][x] = type;
}
}
}
// floating platforms
placePlatform(9, 8, 13, 2);
placePlatform(7, 18, 24, 2);
placePlatform(5, 32, 38, 2);
placePlatform(9, 42, 48, 2);
placePlatform(6, 50, 56, 2);
placePlatform(8, 60, 66, 2);
placePlatform(5, 70, 76, 2);
placePlatform(7, 80, 86, 2);
placePlatform(4, 92, 98, 2);
placePlatform(6, 102, 108, 2);
placePlatform(3, 110, 115, 2);
// stairs near end
for (let i = 0; i < 5; i++) {
placePlatform(11 - i, 110 + i, 110 + i, 2);
}
// coins
function placeCoin(y, x) {
if (y >= 0 && y < LEVEL_H && x >= 0 && x < LEVEL_W && map[y][x] === 0) {
map[y][x] = 3;
}
}
// coin rows on platforms
for (let x = 9; x <= 12; x++) placeCoin(8, x);
for (let x = 19; x <= 23; x++) placeCoin(6, x);
for (let x = 33; x <= 37; x++) placeCoin(4, x);
for (let x = 43; x <= 47; x++) placeCoin(8, x);
for (let x = 51; x <= 55; x++) placeCoin(5, x);
for (let x = 61; x <= 65; x++) placeCoin(7, x);
for (let x = 71; x <= 75; x++) placeCoin(4, x);
// scattered coins in the air
placeCoin(10, 15);
placeCoin(10, 16);
placeCoin(10, 17);
placeCoin(3, 50);
placeCoin(3, 51);
placeCoin(3, 52);
placeCoin(10, 70);
placeCoin(10, 71);
placeCoin(2, 95);
placeCoin(2, 96);
placeCoin(2, 97);
placeCoin(2, 98);
// flag pole
map[3][117] = 4;
map[4][117] = 4;
map[5][117] = 4;
map[6][117] = 4;
map[7][117] = 4;
map[8][117] = 4;
map[9][117] = 4;
map[10][117] = 4;
map[11][117] = 4;
// goal block at base
map[11][118] = 5;
map[12][118] = 1;
map[13][118] = 1;
// ---- game state ----
let player, coins, totalCoins, score, won, gameOver, camX, gameTick;
let particles = [];
let starField = [];
let clouds = [];
function init() {
player = {
x: 3 * TILE,
y: 10 * TILE,
w: 28,
h: 36,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
frame: 0,
frameTimer: 0,
coyoteTimer: 0,
jumpBufferTimer: 0,
dead: false,
deathTimer: 0,
};
coins = [];
totalCoins = 0;
score = 0;
won = false;
gameOver = false;
camX = 0;
gameTick = 0;
particles = [];
// collect coin positions from map
for (let y = 0; y < LEVEL_H; y++) {
for (let x = 0; x < LEVEL_W; x++) {
if (map[y][x] === 3) {
coins.push({ x: x * TILE + 10, y: y * TILE + 10, collected: false, bobOffset: Math.random() * Math.PI * 2 });
totalCoins++;
}
}
}
// clouds
clouds = [];
for (let i = 0; i < 20; i++) {
clouds.push({
x: Math.random() * LEVEL_W * TILE,
y: 30 + Math.random() * 120,
w: 60 + Math.random() * 80,
speed: 0.2 + Math.random() * 0.3,
});
}
// stars
starField = [];
for (let i = 0; i < 50; i++) {
starField.push({
x: Math.random() * W,
y: Math.random() * H * 0.6,
size: 1 + Math.random() * 2,
twinkle: Math.random() * Math.PI * 2,
});
}
}
// ---- input ----
const keys = {};
window.addEventListener("keydown", (e) => {
keys[e.code] = true;
if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
e.preventDefault();
}
if (won || gameOver) {
if (e.code === "KeyR" || e.code === "Space") {
init();
}
}
});
window.addEventListener("keyup", (e) => {
keys[e.code] = false;
});
function isLeft() { return keys["ArrowLeft"] || keys["KeyA"]; }
function isRight() { return keys["ArrowRight"] || keys["KeyD"]; }
function isJump() { return keys["Space"] || keys["ArrowUp"] || keys["KeyW"]; }
// ---- collision helpers ----
function getTile(px, py) {
const tx = Math.floor(px / TILE);
const ty = Math.floor(py / TILE);
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
return map[ty][tx];
}
function isSolid(tile) {
return tile === 1 || tile === 2;
}
// ---- update ----
function update() {
if (won || gameOver) return;
gameTick++;
// player movement
if (isLeft()) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
}
if (isRight()) {
player.vx += MOVE_ACCEL;
player.facing = 1;
}
player.vx *= FRICTION;
if (Math.abs(player.vx) > MAX_SPEED) player.vx = MAX_SPEED * Math.sign(player.vx);
if (Math.abs(player.vx) < 0.1) player.vx = 0;
// jump buffer
if (isJump()) {
player.jumpBufferTimer = JUMP_BUFFER_FRAMES;
}
if (player.jumpBufferTimer > 0) player.jumpBufferTimer--;
// coyote time
if (player.onGround) {
player.coyoteTimer = COYOTE_FRAMES;
}
if (player.coyoteTimer > 0) player.coyoteTimer--;
// jump
if (player.jumpBufferTimer > 0 && player.coyoteTimer > 0) {
player.vy = JUMP_FORCE;
player.coyoteTimer = 0;
player.jumpBufferTimer = 0;
player.onGround = false;
spawnJumpParticles();
}
// variable jump height
if (!isJump() && player.vy < -3) {
player.vy *= 0.7;
}
// gravity
player.vy += GRAVITY;
if (player.vy > 15) player.vy = 15;
// horizontal movement + collision
player.x += player.vx;
resolveCollisionX();
// vertical movement + collision
player.y += player.vy;
player.onGround = false;
resolveCollisionY();
// animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.frameTimer++;
if (player.frameTimer > 6) {
player.frameTimer = 0;
player.frame = (player.frame + 1) % 4;
}
} else if (!player.onGround) {
player.frame = 1; // jump pose
} else {
player.frame = 0;
}
// coin collection
for (const c of coins) {
if (c.collected) continue;
const dx = (player.x + player.w / 2) - (c.x + 10);
const dy = (player.y + player.h / 2) - (c.y + 10);
if (Math.sqrt(dx * dx + dy * dy) < 24) {
c.collected = true;
score++;
spawnCoinParticles(c.x + 10, c.y + 10);
}
}
// flag / goal check
const flagTile = getTile(player.x + player.w / 2, player.y + player.h / 2);
if (flagTile === 4 || flagTile === 5) {
won = true;
spawnWinParticles();
}
// fall death
if (player.y > LEVEL_H * TILE + 50) {
player.dead = true;
player.deathTimer = 60;
}
if (player.dead) {
player.deathTimer--;
if (player.deathTimer <= 0) {
init();
}
}
// camera
const targetCam = player.x - W * 0.35;
camX += (targetCam - camX) * 0.1;
if (camX < 0) camX = 0;
if (camX > LEVEL_W * TILE - W) camX = LEVEL_W * TILE - W;
// particles
updateParticles();
}
function resolveCollisionX() {
const p = player;
const left = Math.floor(p.x / TILE);
const right = Math.floor((p.x + p.w - 1) / TILE);
const top = Math.floor(p.y / TILE);
const bottom = Math.floor((p.y + p.h - 1) / TILE);
for (let ty = top; ty <= bottom; ty++) {
for (let tx = left; tx <= right; tx++) {
if (isSolid(getTile(tx * TILE, ty * TILE))) {
if (p.vx > 0) {
p.x = tx * TILE - p.w;
} else if (p.vx < 0) {
p.x = (tx + 1) * TILE;
}
p.vx = 0;
}
}
}
}
function resolveCollisionY() {
const p = player;
const left = Math.floor(p.x / TILE);
const right = Math.floor((p.x + p.w - 1) / TILE);
const top = Math.floor(p.y / TILE);
const bottom = Math.floor((p.y + p.h - 1) / TILE);
for (let ty = top; ty <= bottom; ty++) {
for (let tx = left; tx <= right; tx++) {
const tile = getTile(tx * TILE, ty * TILE);
if (isSolid(tile)) {
if (p.vy > 0) {
p.y = ty * TILE - p.h;
p.vy = 0;
p.onGround = true;
} else if (p.vy < 0) {
p.y = (ty + 1) * TILE;
p.vy = 0;
}
}
// flag pole collision
if (tile === 4 || tile === 5) {
// just pass through — goal check handles it
}
}
}
}
// ---- particles ----
function spawnJumpParticles() {
for (let i = 0; i < 5; i++) {
particles.push({
x: player.x + player.w / 2 + (Math.random() - 0.5) * 20,
y: player.y + player.h,
vx: (Math.random() - 0.5) * 3,
vy: Math.random() * 2,
life: 20 + Math.random() * 10,
color: "#c8a87a",
size: 2 + Math.random() * 3,
});
}
}
function spawnCoinParticles(x, y) {
for (let i = 0; i < 8; i++) {
const angle = (Math.PI * 2 / 8) * i;
particles.push({
x, y,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3,
life: 25,
color: "#ffd700",
size: 3 + Math.random() * 2,
});
}
}
function spawnWinParticles() {
for (let i = 0; i < 30; i++) {
particles.push({
x: player.x + player.w / 2,
y: player.y + player.h / 2,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 40 + Math.random() * 20,
color: ["#ff0", "#f0f", "#0ff", "#f00", "#0f0"][Math.floor(Math.random() * 5)],
size: 3 + Math.random() * 4,
});
}
}
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 drawSky() {
// gradient sky
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#4a90d9");
grad.addColorStop(0.6, "#87ceeb");
grad.addColorStop(1, "#b8e4f0");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawClouds() {
ctx.fillStyle = "rgba(255,255,255,0.8)";
for (const c of clouds) {
const sx = c.x - camX * 0.3;
const sy = c.y;
if (sx + c.w < -100 || sx > W + 100) continue;
// simple cloud shape
ctx.beginPath();
ctx.arc(sx, sy, 20, 0, Math.PI * 2);
ctx.arc(sx + 25, sy - 10, 25, 0, Math.PI * 2);
ctx.arc(sx + 50, sy, 20, 0, Math.PI * 2);
ctx.arc(sx + 25, sy + 5, 22, 0, Math.PI * 2);
ctx.fill();
}
}
function drawHills() {
// background hills (parallax)
ctx.fillStyle = "#5a9e4e";
for (let i = 0; i < 15; i++) {
const hx = i * 200 - (camX * 0.4) % 200;
const hy = H - 80;
ctx.beginPath();
ctx.arc(hx, hy, 80 + Math.sin(i) * 30, Math.PI, 0);
ctx.fill();
}
ctx.fillStyle = "#6abf5e";
for (let i = 0; i < 15; i++) {
const hx = i * 200 + 100 - (camX * 0.4) % 200;
const hy = H - 60;
ctx.beginPath();
ctx.arc(hx, hy, 60 + Math.cos(i) * 20, Math.PI, 0);
ctx.fill();
}
}
function drawTiles() {
const startCol = Math.max(0, Math.floor(camX / TILE) - 1);
const endCol = Math.min(LEVEL_W, startCol + Math.ceil(W / TILE) + 3);
for (let y = 0; y < LEVEL_H; y++) {
for (let x = startCol; x < endCol; x++) {
const tile = map[y][x];
if (tile === 0) continue;
const sx = x * TILE - camX;
const sy = y * TILE;
if (tile === 1) {
// ground
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#6B3410";
ctx.fillRect(sx, sy, TILE, 4);
// grass on top
if (y > 0 && map[y - 1][x] === 0) {
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, TILE, 6);
ctx.fillStyle = "#388E3C";
ctx.fillRect(sx, sy, TILE, 3);
}
// texture
ctx.fillStyle = "rgba(0,0,0,0.1)";
ctx.fillRect(sx + 5, sy + 10, 12, 8);
ctx.fillRect(sx + 22, sy + 20, 10, 8);
} else if (tile === 2) {
// brick
ctx.fillStyle = "#c0392b";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#e74c3c";
ctx.fillRect(sx + 1, sy + 1, TILE - 2, TILE / 2 - 2);
ctx.fillStyle = "#a93226";
ctx.fillRect(sx + 1, sy + TILE / 2, TILE - 2, TILE / 2 - 1);
// mortar lines
ctx.strokeStyle = "#7b241c";
ctx.lineWidth = 1;
ctx.strokeRect(sx, sy, TILE, TILE);
ctx.beginPath();
ctx.moveTo(sx, sy + TILE / 2);
ctx.lineTo(sx + TILE, sy + TILE / 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx + TILE / 2, sy);
ctx.lineTo(sx + TILE / 2, sy + TILE / 2);
ctx.stroke();
} else if (tile === 3) {
// coin (handled separately)
} else if (tile === 4) {
// flag pole
ctx.fillStyle = "#888";
ctx.fillRect(sx + 18, sy, 4, TILE);
if (y === 3) {
// flag
ctx.fillStyle = "#e74c3c";
ctx.beginPath();
ctx.moveTo(sx + 22, sy + 2);
ctx.lineTo(sx + 52, sy + 14);
ctx.lineTo(sx + 22, sy + 26);
ctx.fill();
}
} else if (tile === 5) {
// goal block
ctx.fillStyle = "#f1c40f";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#f39c12";
ctx.fillRect(sx + 3, sy + 3, TILE - 6, TILE - 6);
ctx.fillStyle = "#fff";
ctx.font = "bold 20px monospace";
ctx.textAlign = "center";
ctx.fillText("★", sx + TILE / 2, sy + TILE / 2 + 7);
}
}
}
}
function drawCoins() {
for (const c of coins) {
if (c.collected) continue;
const sx = c.x - camX;
const sy = c.y + Math.sin(gameTick * 0.06 + c.bobOffset) * 4;
if (sx < -20 || sx > W + 20) continue;
// glow
ctx.fillStyle = "rgba(255,215,0,0.3)";
ctx.beginPath();
ctx.arc(sx + 10, sy + 10, 14, 0, Math.PI * 2);
ctx.fill();
// coin body
const stretch = Math.abs(Math.sin(gameTick * 0.05 + c.bobOffset));
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.ellipse(sx + 10, sy + 10, 8 * stretch + 2, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffed4a";
ctx.beginPath();
ctx.ellipse(sx + 10, sy + 10, 5 * stretch + 1, 5, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPlayer() {
if (player.dead) return;
const p = player;
const sx = p.x - camX;
const sy = p.y;
ctx.save();
ctx.translate(sx + p.w / 2, sy + p.h / 2);
if (p.facing < 0) ctx.scale(-1, 1);
// body
ctx.fillStyle = "#e74c3c"; // red shirt
ctx.fillRect(-10, -8, 20, 18);
// overalls
ctx.fillStyle = "#2980b9";
ctx.fillRect(-10, 4, 20, 12);
// straps
ctx.fillStyle = "#2980b9";
ctx.fillRect(-8, -2, 4, 8);
ctx.fillRect(4, -2, 4, 8);
// head
ctx.fillStyle = "#fdebd0";
ctx.fillRect(-8, -18, 16, 12);
// hat
ctx.fillStyle = "#e74c3c";
ctx.fillRect(-10, -22, 22, 6);
ctx.fillRect(-6, -26, 18, 6);
// eyes
ctx.fillStyle = "#000";
ctx.fillRect(1, -14, 3, 3);
// mustache
ctx.fillStyle = "#5d4037";
ctx.fillRect(-2, -9, 12, 3);
// shoes
ctx.fillStyle = "#5d4037";
if (p.onGround && Math.abs(p.vx) > 0.5) {
// walking animation
const legOffset = Math.sin(p.frame * Math.PI / 2) * 3;
ctx.fillRect(-10, 14, 10, 4 + legOffset);
ctx.fillRect(0, 14, 10, 4 - legOffset);
} else {
ctx.fillRect(-10, 14, 10, 4);
ctx.fillRect(0, 14, 10, 4);
}
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / 40;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - camX - 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, 10, 180, 36);
ctx.strokeStyle = "#ffd700";
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, 180, 36);
// coin icon
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(30, 28, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffed4a";
ctx.beginPath();
ctx.arc(30, 28, 6, 0, Math.PI * 2);
ctx.fill();
// score text
ctx.fillStyle = "#fff";
ctx.font = "bold 20px monospace";
ctx.textAlign = "left";
ctx.fillText(`${score} / ${totalCoins}`, 48, 33);
// progress bar
const progress = totalCoins > 0 ? score / totalCoins : 0;
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(W - 170, 14, 150, 12);
ctx.fillStyle = progress >= 1 ? "#2ecc71" : "#f39c12";
ctx.fillRect(W - 170, 14, 150 * progress, 12);
ctx.strokeStyle = "#fff";
ctx.lineWidth = 1;
ctx.strokeRect(W - 170, 14, 150, 12);
}
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 - 40);
ctx.fillStyle = "#fff";
ctx.font = "24px monospace";
ctx.fillText(`Coins: ${score} / ${totalCoins}`, W / 2, H / 2 + 10);
ctx.font = "18px monospace";
ctx.fillStyle = "#aaa";
ctx.fillText("Press Space or R to play again", W / 2, H / 2 + 50);
}
function drawGameOver() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#e74c3c";
ctx.font = "bold 40px monospace";
ctx.textAlign = "center";
ctx.fillText("Game Over", W / 2, H / 2);
ctx.fillStyle = "#fff";
ctx.font = "18px monospace";
ctx.fillText("Press Space or R to retry", W / 2, H / 2 + 40);
}
function draw() {
drawSky();
drawClouds();
drawHills();
drawTiles();
drawCoins();
drawPlayer();
drawParticles();
drawHUD();
if (won) drawWinScreen();
if (gameOver) drawGameOver();
}
// ---- game loop ----
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
init();
loop();
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: monospace;
overflow: hidden;
}
canvas {
border: 3px solid #e94560;
border-radius: 4px;
display: block;
background: #87ceeb;
}
#info {
color: #eee;
margin-top: 12px;
font-size: 14px;
text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">Arrow Keys / WASD to move • Space / Up to jump • Collect all coins!</div>
<script src="game.js"></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%)
// ============================================================
// Super Mario-style Platformer — game.js
// ============================================================
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 450;
// --- 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 constants ---
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_SPEED = 1.0;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -12;
const TERMINAL_VELOCITY = 14;
const WORLD_WIDTH = 4800;
const TILE = 48;
// --- Camera ---
let camera = { x: 0, y: 0 };
// --- Player ---
let player = {
x: 100,
y: 200,
w: 32,
h: 40,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
walkFrame: 0,
walkTimer: 0,
dead: false,
won: false,
wins: 0
};
// --- Level data ---
// Platforms: { x, y, w, h }
const platforms = [];
// Coins: { x, y, collected }
const coins = [];
// Pits / hazards
const pits = [];
// Flag / goal
const flag = { x: 4500, y: 0, reached: false };
function buildLevel() {
platforms.length = 0;
coins.length = 0;
pits.length = 0;
// Ground segments
const ground = [
[0, 800], [900, 1800], [2000, 3200], [3400, WORLD_WIDTH]
];
for (const [gx, gx2] of ground) {
platforms.push({ x: gx, y: 400, w: gx2 - gx, h: 50 });
pits.push({ x: gx, y: 450, w: gx2 - gx, h: 100 }); // visual pit below
}
// Raised platforms
const platData = [
[300, 310, 144],
[550, 250, 96],
[800, 310, 144],
[1050, 220, 120],
[1300, 300, 96],
[1500, 240, 144],
[1800, 200, 96],
[2100, 310, 144],
[2400, 250, 120],
[2700, 190, 96],
[3000, 310, 144],
[3300, 250, 120],
[3600, 200, 96],
[3900, 280, 144],
[4200, 220, 120],
[4500, 160, 144],
];
for (const [px, py, pw] of platData) {
platforms.push({ x: px, y: py, w: pw, h: 20 });
}
// Floating blocks (decorative / extra platforms)
const blocks = [
[600, 170, 48], [1100, 140, 48], [1900, 130, 48],
[2500, 150, 48], [3100, 160, 48], [3700, 120, 48],
[4100, 140, 48],
];
for (const [bx, by, bw] of blocks) {
platforms.push({ x: bx, y: by, w: bw, h: 20 });
}
// Coins
const coinPositions = [
// Ground coins
[200, 360], [250, 360], [300, 360],
[450, 360], [500, 360],
// Platform coins
[350, 270], [400, 270], [450, 270],
[580, 210], [620, 210],
[850, 270], [900, 270], [950, 270],
[1080, 180], [1120, 180],
[1330, 260],
[1550, 200], [1600, 200], [1650, 200],
[1830, 160],
[2150, 270], [2200, 270], [2250, 270],
[2440, 210], [2480, 210],
[2730, 150],
[3050, 270], [3100, 270], [3150, 270],
[3340, 210], [3380, 210],
[3630, 160],
[3950, 240], [4000, 240], [4050, 240],
[4240, 180], [4280, 180],
// High coins
[624, 130], [1124, 100], [1924, 90],
[2524, 110], [3124, 120], [3724, 80], [4124, 100],
];
for (const [cx, cy] of coinPositions) {
coins.push({ x: cx, y: cy, collected: false, bobOffset: Math.random() * Math.PI * 2 });
}
// Flag
flag.x = 4580;
flag.y = 160;
flag.reached = false;
}
function resetPlayer() {
player.x = 100;
player.y = 200;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.dead = false;
player.won = false;
player.wins = 0;
buildLevel();
}
let score = 0;
let totalCoins = 0;
let gameTime = 0;
let deathTimer = 0;
let particles = [];
let screenShake = 0;
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() - 1) * 5,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 4
});
}
}
// --- Physics & collision ---
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;
}
function update() {
if (player.won) {
gameTime++;
return;
}
if (player.dead) {
deathTimer--;
if (deathTimer <= 0) resetPlayer();
return;
}
// Input
let moveX = 0;
if (keys["ArrowLeft"] || keys["KeyA"]) moveX -= 1;
if (keys["ArrowRight"] || keys["KeyD"]) moveX += 1;
if (moveX !== 0) {
player.vx += moveX * MOVE_SPEED;
player.facing = moveX;
} else {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Jump
if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
spawnParticles(player.x + player.w / 2, player.y + player.h, "#fff", 5);
}
// Clamp horizontal speed
if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
// Gravity
player.vy += GRAVITY;
if (player.vy > TERMINAL_VELOCITY) player.vy = TERMINAL_VELOCITY;
// Move X
player.x += player.vx;
if (player.x < 0) player.x = 0;
if (player.x + player.w > WORLD_WIDTH) player.x = WORLD_WIDTH - player.w;
// Collision X
for (const p of platforms) {
if (rectCollide(player, p)) {
if (player.vx > 0) {
player.x = p.x - player.w;
} else if (player.vx < 0) {
player.x = p.x + p.w;
}
player.vx = 0;
}
}
// Move Y
player.y += player.vy;
player.onGround = false;
// Collision Y
for (const p of platforms) {
if (rectCollide(player, p)) {
if (player.vy > 0) {
// Landing on top
player.y = p.y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
// Hitting head
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// Fall into pit
if (player.y > 500) {
player.dead = true;
deathTimer = 45;
screenShake = 10;
spawnParticles(player.x + player.w / 2, 450, "#f44", 15);
}
// Coins
for (const c of coins) {
if (!c.collected) {
const cx = c.x, cy = c.y;
if (player.x + player.w > cx - 10 && player.x < cx + 10 &&
player.y + player.h > cy - 10 && player.y < cy + 10) {
c.collected = true;
score += 10;
spawnParticles(cx, cy, "#FFD700", 8);
}
}
}
// Flag / goal
if (!flag.reached && !player.won) {
if (player.x + player.w > flag.x && player.x < flag.x + 30) {
player.won = true;
player.wins++;
score += 500;
spawnParticles(flag.x, flag.y, "#0f0", 30);
spawnParticles(flag.x, flag.y, "#FFD700", 20);
}
}
// Walk animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.walkTimer++;
if (player.walkTimer > 6) {
player.walkTimer = 0;
player.walkFrame = (player.walkFrame + 1) % 4;
}
} else {
player.walkFrame = 0;
player.walkTimer = 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);
}
// Screen shake decay
if (screenShake > 0) screenShake *= 0.85;
if (screenShake < 0.5) screenShake = 0;
// Camera
const targetX = player.x - canvas.width / 2 + player.w / 2;
camera.x += (targetX - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
if (camera.x > WORLD_WIDTH - canvas.width) camera.x = WORLD_WIDTH - canvas.width;
}
// --- Drawing ---
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, "#5C94FC");
grad.addColorStop(0.7, "#87CEEB");
grad.addColorStop(1, "#B0E0E6");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Clouds
ctx.fillStyle = "rgba(255,255,255,0.8)";
const cloudData = [
[200, 60, 80], [700, 40, 100], [1200, 70, 70], [1800, 50, 90],
[2500, 65, 80], [3200, 45, 110], [3800, 55, 75], [4400, 35, 90],
];
for (const [cx, cy, cw] of cloudData) {
const sx = cx - camera.x * 0.3;
if (sx > -cw && sx < canvas.width + cw) {
drawCloud(sx, cy, cw);
}
}
// Hills (parallax)
ctx.fillStyle = "#4CAF50";
const hillData = [
[100, 120], [600, 90], [1100, 130], [1700, 100], [2300, 120],
[2900, 95], [3500, 110], [4100, 100],
];
for (const [hx, hh] of hillData) {
const sx = hx - camera.x * 0.4;
drawHill(sx, 420, hh);
}
// Bushes
ctx.fillStyle = "#2E7D32";
const bushData = [
[300, 30], [900, 25], [1500, 35], [2200, 28], [3000, 32], [3600, 26], [4200, 30],
];
for (const [bx, bw] of bushData) {
const sx = bx - camera.x * 0.6;
drawBush(sx, 410, bw);
}
}
function drawCloud(x, y, w) {
const h = w * 0.5;
ctx.beginPath();
ctx.arc(x, y, h * 0.5, 0, Math.PI * 2);
ctx.arc(x + w * 0.3, y - h * 0.2, h * 0.6, 0, Math.PI * 2);
ctx.arc(x + w * 0.6, y, h * 0.5, 0, Math.PI * 2);
ctx.arc(x + w * 0.3, y + h * 0.1, h * 0.45, 0, Math.PI * 2);
ctx.fill();
}
function drawHill(x, baseY, h) {
ctx.beginPath();
ctx.moveTo(x - h, baseY);
ctx.quadraticCurveTo(x, baseY - h, x + h, baseY);
ctx.fill();
}
function drawBush(x, baseY, w) {
const r = w * 0.4;
ctx.beginPath();
ctx.arc(x, baseY - r * 0.5, r, Math.PI, 0);
ctx.arc(x + w * 0.5, baseY - r * 0.3, r * 0.8, Math.PI, 0);
ctx.fill();
}
function drawPlatform(p) {
const sx = p.x - camera.x;
const sy = p.y;
if (sx + p.w < -50 || sx > canvas.width + 50) return;
if (p.h > 30) {
// Ground
// Top grass
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, p.w, 8);
// Dirt
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy + 8, p.w, p.h - 8);
// Dirt detail
ctx.fillStyle = "#A0522D";
for (let i = 0; i < p.w; i += 24) {
ctx.fillRect(sx + i + 4, sy + 16, 8, 6);
ctx.fillRect(sx + i + 14, sy + 28, 8, 6);
}
} else {
// Floating platform
// Top
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy, p.w, 6);
// Body
ctx.fillStyle = "#A0522D";
ctx.fillRect(sx, sy + 6, p.w, p.h - 6);
// Highlight
ctx.fillStyle = "rgba(255,255,255,0.15)";
ctx.fillRect(sx, sy, p.w, 3);
// Shadow
ctx.fillStyle = "rgba(0,0,0,0.15)";
ctx.fillRect(sx, sy + p.h - 3, p.w, 3);
// Brick pattern
ctx.strokeStyle = "rgba(0,0,0,0.2)";
ctx.lineWidth = 1;
for (let i = 0; i < p.w; i += 24) {
ctx.strokeRect(sx + i, sy, 24, p.h);
}
}
}
function drawCoin(c, time) {
if (c.collected) return;
const sx = c.x - camera.x;
const sy = c.y + Math.sin(time * 0.05 + c.bobOffset) * 5;
if (sx < -20 || sx > canvas.width + 20) return;
// Glow
ctx.fillStyle = "rgba(255,215,0,0.3)";
ctx.beginPath();
ctx.arc(sx, sy, 12, 0, Math.PI * 2);
ctx.fill();
// Coin body
const scaleX = Math.abs(Math.cos(time * 0.06 + c.bobOffset));
ctx.save();
ctx.translate(sx, sy);
ctx.scale(scaleX, 1);
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 9, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFA500";
ctx.beginPath();
ctx.arc(0, 0, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFD700";
ctx.font = "bold 10px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 1);
ctx.restore();
}
function drawPlayer(time) {
if (player.dead) return;
const sx = player.x - camera.x;
const sy = player.y;
const f = player.facing;
ctx.save();
ctx.translate(sx + player.w / 2, sy + player.h / 2);
ctx.scale(f, 1);
// Shadow
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.beginPath();
ctx.ellipse(0, player.h / 2 + 2, 14, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Body (overalls)
ctx.fillStyle = "#1565C0";
ctx.fillRect(-10, 2, 20, 18);
// Shirt
ctx.fillStyle = "#D32F2F";
ctx.fillRect(-12, -2, 24, 8);
// Head
ctx.fillStyle = "#FFCC80";
ctx.fillRect(-9, -14, 18, 16);
// Hat
ctx.fillStyle = "#D32F2F";
ctx.fillRect(-10, -16, 20, 6);
ctx.fillRect(-6, -20, 16, 6);
// Hat brim
ctx.fillStyle = "#B71C1C";
ctx.fillRect(-12, -12, 24, 3);
// Eyes
ctx.fillStyle = "#333";
ctx.fillRect(2, -10, 4, 4);
// Mustache
ctx.fillStyle = "#5D4037";
ctx.fillRect(0, -5, 10, 3);
// Legs animation
const legOffset = player.onGround ? Math.sin(player.walkFrame * Math.PI / 2) * 4 : 3;
ctx.fillStyle = "#1565C0";
ctx.fillRect(-9, 18, 8, 6 + legOffset);
ctx.fillRect(1, 18, 8, 6 - legOffset);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(-10, 22 + legOffset, 9, 4);
ctx.fillRect(1, 22 - legOffset, 9, 4);
ctx.restore();
}
function drawFlag(time) {
const sx = flag.x - camera.x;
const sy = flag.y;
if (sx < -50 || sx > canvas.width + 50) return;
// Pole
ctx.fillStyle = "#666";
ctx.fillRect(sx, sy, 6, 240);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(sx + 3, sy, 8, 0, Math.PI * 2);
ctx.fill();
// Flag
const wave = Math.sin(time * 0.08) * 5;
ctx.fillStyle = player.won ? "#4CAF50" : "#D32F2F";
ctx.beginPath();
ctx.moveTo(sx + 6, sy + 5);
ctx.lineTo(sx + 50 + wave, sy + 20);
ctx.lineTo(sx + 6, sy + 40);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFD700";
ctx.font = "16px sans-serif";
ctx.textAlign = "center";
ctx.fillText("★", sx + 25 + wave * 0.5, sy + 28);
// Base
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx - 8, sy + 235, 22, 15);
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
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 background
ctx.fillStyle = "rgba(0,0,0,0.5)";
roundRect(ctx, 10, 10, 180, 40, 8);
ctx.fill();
// Coin icon
ctx.fillStyle = "#FFD700";
ctx.font = "bold 18px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("🪙 " + score, 22, 31);
// Coins collected
const collected = coins.filter(c => c.collected).length;
ctx.fillStyle = "rgba(0,0,0,0.5)";
roundRect(ctx, canvas.width - 170, 10, 160, 40, 8);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "right";
ctx.fillText("Coins: " + collected + "/" + coins.length, canvas.width - 22, 31);
// Progress bar
const progress = player.x / (WORLD_WIDTH - canvas.width);
ctx.fillStyle = "rgba(0,0,0,0.3)";
roundRect(ctx, canvas.width / 2 - 100, 14, 200, 8, 4);
ctx.fill();
ctx.fillStyle = "#4CAF50";
roundRect(ctx, canvas.width / 2 - 100, 14, Math.max(8, 200 * Math.min(1, progress)), 8, 4);
ctx.fill();
// Win message
if (player.won) {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🎉 YOU WIN! 🎉", canvas.width / 2, canvas.height / 2 - 40);
ctx.fillStyle = "#fff";
ctx.font = "24px sans-serif";
ctx.fillText("Score: " + score + " | Coins: " + coins.filter(c => c.collected).length + "/" + coins.length, canvas.width / 2, canvas.height / 2 + 15);
ctx.fillStyle = "#aaa";
ctx.font = "16px sans-serif";
ctx.fillText("Press R to play again", canvas.width / 2, canvas.height / 2 + 55);
}
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawDeathOverlay() {
if (!player.dead) return;
ctx.fillStyle = "rgba(200,0,0,0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#fff";
ctx.font = "bold 32px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("OOPS!", canvas.width / 2, canvas.height / 2);
}
function draw() {
const time = gameTime + (player.won ? 0 : Date.now() / 16);
// Screen shake
ctx.save();
if (screenShake > 0) {
ctx.translate(
(Math.random() - 0.5) * screenShake * 2,
(Math.random() - 0.5) * screenShake * 2
);
}
drawSky();
// Platforms
for (const p of platforms) drawPlatform(p);
// Coins
for (const c of coins) drawCoin(c, time);
// Flag
drawFlag(time);
// Player
drawPlayer(time);
// Particles
drawParticles();
ctx.restore();
// HUD (not affected by screen shake)
drawHUD();
drawDeathOverlay();
}
// --- Game loop ---
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// --- Init ---
resetPlayer();
// Restart on R
window.addEventListener("keydown", e => {
if (e.code === "KeyR") resetPlayer();
});
gameLoop();
<!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;
}
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
h1 {
color: #FFD700;
font-size: 28px;
margin-bottom: 12px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
letter-spacing: 2px;
}
#game {
border: 3px solid #333;
border-radius: 6px;
box-shadow: 0 0 30px rgba(92, 148, 252, 0.3);
display: block;
background: #000;
}
.controls {
color: #aaa;
font-size: 13px;
margin-top: 10px;
text-align: center;
line-height: 1.6;
}
.controls kbd {
background: #333;
border: 1px solid #555;
border-radius: 3px;
padding: 1px 6px;
font-family: monospace;
color: #fff;
font-size: 12px;
}
</style>
</head>
<body>
<h1>🍄 SUPER PLATFORMER 🍄</h1>
<canvas id="game"></canvas>
<div class="controls">
<kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> to move |
<kbd>Space</kbd> or <kbd>↑</kbd> to jump |
<kbd>R</kbd> to restart
</div>
<script src="game.js"></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">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: monospace;
}
canvas {
border: 3px solid #444;
border-radius: 4px;
display: block;
background: #87CEEB;
}
#ui-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
#message {
display: none;
background: rgba(0,0,0,0.7);
color: #fff;
padding: 30px 50px;
border-radius: 12px;
font-size: 28px;
text-align: center;
pointer-events: auto;
}
#message h2 { margin-bottom: 10px; font-size: 32px; }
#message p { font-size: 16px; color: #ccc; }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="ui-overlay">
<div id="message">
<h2 id="msg-title">🏆 You Win!</h2>
<p id="msg-body">Coins collected: 0</p>
<p style="margin-top:12px;font-size:14px;color:#aaa;">Press Enter or Space to play again</p>
</div>
</div>
<script>
"use strict";
// ─── Constants ────────────────────────────────────────────────────
const W = 800, H = 450;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -12;
const TILE = 40;
// ─── Canvas ───────────────────────────────────────────────────────
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const msgDiv = document.getElementById("message");
const msgTitle = document.getElementById("msg-title");
const msgBody = document.getElementById("msg-body");
// ─── Input ────────────────────────────────────────────────────────
const keys = {};
window.addEventListener("keydown", e => {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","KeyW","KeyA","KeyS","KeyD"].includes(e.code))
e.preventDefault();
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
// ─── Level Design ─────────────────────────────────────────────────
// 0 = air, 1 = ground block, 2 = brick platform, 3 = coin, 4 = flag pole base, 5 = flag pole
// Level is 50 tiles wide (2000px), 11.25 tiles tall (450px)
const LEVEL_W = 50;
const LEVEL_H = 12;
function buildLevel() {
const tiles = [];
for (let y = 0; y < LEVEL_H; y++) {
tiles[y] = [];
for (let x = 0; x < LEVEL_W; x++) {
tiles[y][x] = 0;
}
}
// Ground (rows 10-11, with gaps)
const groundSegments = [
[0, 14], // 0-13
[17, 30], // 17-29
[33, 49], // 33-48
];
for (const [s, e] of groundSegments) {
for (let x = s; x <= e; x++) {
tiles[10][x] = 1;
tiles[11][x] = 1;
}
}
// Platforms (raised)
const platforms = [
[6, 5, 3], // x=6, y=5, w=3 → tiles 6,7,8 at row 5
[12, 7, 2], // x=12, y=7, w=2
[20, 6, 4], // x=20, y=6, w=4
[26, 4, 3], // x=26, y=4, w=3
[35, 7, 3], // x=35, y=7, w=3
[40, 5, 2], // x=40, y=5, w=2
[43, 3, 4], // x=43, y=3, w=4
];
for (const [sx, sy, w] of platforms) {
for (let x = sx; x < sx + w; x++) {
tiles[sy][x] = 2;
}
}
// Staircase near the end
for (let i = 0; i < 5; i++) {
for (let y = 9 - i; y <= 9; y++) {
tiles[y][45 + i] = 1;
}
}
// Coins
const coins = [
[3, 8], [4, 8], [5, 8],
[7, 4], [8, 4], [9, 4],
[13, 6], [14, 6],
[21, 5], [22, 5], [23, 5],
[27, 3], [28, 3],
[36, 6], [37, 6], [38, 6],
[41, 4], [42, 4],
[44, 2], [45, 2], [46, 2], [47, 2],
[10, 9], [15, 9],
];
// Flag pole at the end
const flagX = 48;
tiles[3][flagX] = 5;
tiles[4][flagX] = 5;
tiles[5][flagX] = 5;
tiles[6][flagX] = 5;
tiles[7][flagX] = 5;
tiles[8][flagX] = 5;
tiles[9][flagX] = 4; // base
return { tiles, coins, flagX };
}
// ─── Game State ───────────────────────────────────────────────────
let level, coins, score, gameState, camX, player, particles;
function resetGame() {
const lv = buildLevel();
level = lv.tiles;
coins = lv.coins.map(c => ({ x: c[0] * TILE + 8, y: c[1] * TILE + 8, w: 24, h: 24, collected: false, bobPhase: Math.random() * Math.PI * 2 }));
score = 0;
gameState = "playing";
camX = 0;
player = {
x: 2 * TILE,
y: 7 * TILE,
w: 28,
h: 36,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
walkFrame: 0,
walkTimer: 0,
};
particles = [];
msgDiv.style.display = "none";
}
resetGame();
// ─── Collision Helpers ────────────────────────────────────────────
function getTile(px, py) {
const tx = Math.floor(px / TILE);
const ty = Math.floor(py / TILE);
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
return level[ty][tx];
}
function isSolid(tx, ty) {
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return false;
return level[ty][tx] === 1 || level[ty][tx] === 2;
}
// ─── 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() - 0.5) * 6 - 2,
life: 30 + Math.random() * 20,
color,
size: 2 + Math.random() * 3,
});
}
}
// ─── Update ───────────────────────────────────────────────────────
function update() {
if (gameState !== "playing") return;
const p = player;
// Input
let moveX = 0;
if (keys["ArrowLeft"] || keys["KeyA"]) moveX = -1;
if (keys["ArrowRight"] || keys["KeyD"]) moveX = 1;
if (moveX !== 0) {
p.vx += moveX * 1.2;
if (p.vx > MAX_SPEED) p.vx = MAX_SPEED;
if (p.vx < -MAX_SPEED) p.vx = -MAX_SPEED;
p.facing = moveX;
} else {
p.vx *= FRICTION;
if (Math.abs(p.vx) < 0.1) p.vx = 0;
}
// Jump
if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && p.onGround) {
p.vy = JUMP_FORCE;
p.onGround = false;
spawnParticles(p.x + p.w / 2, p.y + p.h, "#fff", 5);
}
// Gravity
p.vy += GRAVITY;
if (p.vy > 15) p.vy = 15;
// Walk animation
if (p.onGround && Math.abs(p.vx) > 0.5) {
p.walkTimer += Math.abs(p.vx) * 0.15;
p.walkFrame = Math.floor(p.walkTimer) % 4;
} else {
p.walkFrame = 0;
p.walkTimer = 0;
}
// Horizontal collision
p.x += p.vx;
resolveCollisionsX(p);
// Vertical collision
p.y += p.vy;
p.onGround = false;
resolveCollisionsY(p);
// Keep in bounds
if (p.x < 0) p.x = 0;
if (p.y > LEVEL_H * TILE + 50) {
// Fell in pit — respawn
p.x = 2 * TILE;
p.y = 7 * TILE;
p.vx = 0;
p.vy = 0;
}
// Coin collection
for (const coin of coins) {
if (coin.collected) continue;
coin.bobPhase += 0.06;
const bobY = Math.sin(coin.bobPhase) * 4;
if (rectsOverlap(
p.x, p.y, p.w, p.h,
coin.x, coin.y + bobY, coin.w, coin.h
)) {
coin.collected = true;
score++;
spawnParticles(coin.x + coin.w / 2, coin.y + coin.h / 2, "#FFD700", 8);
}
}
// Flag pole check (win)
if (p.x + p.w > level[0] && level[0]) {
// Just check if player reaches flag X area
const flagTile = level[Math.floor(p.y / TILE)]?.[p.x + p.w / 2];
if (flagTile === 4 || flagTile === 5) {
gameState = "won";
msgTitle.textContent = "🏆 You Win!";
msgBody.textContent = `Coins collected: ${score} / ${coins.length}`;
msgDiv.style.display = "block";
spawnParticles(p.x + p.w / 2, p.y, "#FFD700", 20);
return;
}
}
// Camera
const targetCam = p.x - W / 3;
camX += (targetCam - camX) * 0.1;
if (camX < 0) camX = 0;
const maxCam = LEVEL_W * TILE - W;
if (camX > maxCam) camX = maxCam;
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const pt = particles[i];
pt.x += pt.vx;
pt.y += pt.vy;
pt.vy += 0.15;
pt.life--;
if (pt.life <= 0) particles.splice(i, 1);
}
}
function resolveCollisionsX(p) {
const top = Math.floor(p.y / TILE);
const bot = Math.floor((p.y + p.h - 1) / TILE);
for (let ty = top; ty <= bot; ty++) {
if (p.vx > 0) {
const tx = Math.floor((p.x + p.w) / TILE);
if (isSolid(tx, ty)) {
p.x = tx * TILE - p.w;
p.vx = 0;
}
} else if (p.vx < 0) {
const tx = Math.floor(p.x / TILE);
if (isSolid(tx, ty)) {
p.x = (tx + 1) * TILE;
p.vx = 0;
}
}
}
}
function resolveCollisionsY(p) {
const left = Math.floor(p.x / TILE);
const right = Math.floor((p.x + p.w - 1) / TILE);
for (let tx = left; tx <= right; tx++) {
if (p.vy > 0) {
const ty = Math.floor((p.y + p.h) / TILE);
if (isSolid(tx, ty)) {
p.y = ty * TILE - p.h;
p.vy = 0;
p.onGround = true;
}
} else if (p.vy < 0) {
const ty = Math.floor(p.y / TILE);
if (isSolid(tx, ty)) {
p.y = (ty + 1) * TILE;
p.vy = 0;
}
}
}
}
function rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
}
// ─── Drawing ──────────────────────────────────────────────────────
function drawSky() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#4FC3F7");
grad.addColorStop(1, "#B3E5FC");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255,255,255,0.8)";
const cloudData = [
[100, 60, 60], [350, 40, 45], [600, 70, 50], [900, 50, 55],
[1200, 65, 40], [1600, 45, 50], [2000, 55, 45], [2500, 40, 60],
[3000, 60, 40], [3500, 50, 50], [4000, 35, 55], [4500, 65, 45],
[5000, 45, 50], [5500, 55, 40], [6000, 40, 55],
];
for (const [cx, cy, cr] of cloudData) {
const sx = cx - camX * 0.3;
const wrap = ((sx % (W + 200)) + W + 200) % (W + 200) - 100;
drawCloud(wrap, cy, cr);
}
// Hills (parallax)
ctx.fillStyle = "#66BB6A";
const hillData = [
[50, 380, 120], [300, 370, 80], [600, 385, 100], [900, 375, 90],
[1200, 380, 110], [1500, 370, 85], [1800, 385, 95], [2200, 375, 105],
[2600, 380, 80], [3000, 370, 100], [3400, 385, 90], [3800, 375, 110],
[4200, 380, 85], [4600, 370, 95], [5000, 385, 100], [5400, 375, 90],
[5800, 380, 105], [6200, 370, 80],
];
for (const [hx, hy, hr] of hillData) {
const sx = hx - camX * 0.5;
ctx.beginPath();
ctx.arc(sx, hy, hr, Math.PI, 0);
ctx.fill();
}
}
function drawCloud(x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r * 0.6, 0, Math.PI * 2);
ctx.arc(x + r * 0.5, y - r * 0.2, r * 0.5, 0, Math.PI * 2);
ctx.arc(x + r, y, r * 0.55, 0, Math.PI * 2);
ctx.arc(x + r * 0.5, y + r * 0.1, r * 0.45, 0, Math.PI * 2);
ctx.fill();
}
function drawTiles() {
const startCol = Math.floor(camX / TILE);
const endCol = startCol + Math.ceil(W / TILE) + 1;
for (let ty = 0; ty < LEVEL_H; ty++) {
for (let tx = startCol; tx <= endCol && tx < LEVEL_W; tx++) {
const tile = level[ty][tx];
if (tile === 0) continue;
const sx = tx * TILE - camX;
const sy = ty * TILE;
if (tile === 1) {
// Ground block
ctx.fillStyle = "#5D4037";
ctx.fillRect(sx, sy, TILE, TILE);
// Grass top
if (ty === 0 || level[ty - 1][tx] === 0) {
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, TILE, 8);
// Grass blades
ctx.fillStyle = "#388E3C";
for (let i = 0; i < 4; i++) {
const gx = sx + i * 10 + 2;
ctx.fillRect(gx, sy - 3, 3, 5);
}
}
// Texture
ctx.fillStyle = "rgba(0,0,0,0.1)";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
ctx.fillStyle = "rgba(255,255,255,0.05)";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, 4);
} else if (tile === 2) {
// Brick platform
ctx.fillStyle = "#BF360C";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#D84315";
ctx.fillRect(sx + 1, sy + 1, TILE / 2 - 2, TILE / 2 - 2);
ctx.fillRect(sx + TILE / 2 + 1, sy + TILE / 2 + 1, TILE / 2 - 2, TILE / 2 - 2);
ctx.strokeStyle = "#8B2500";
ctx.lineWidth = 1;
ctx.strokeRect(sx, sy, TILE, TILE);
} else if (tile === 4) {
// Flag pole base
ctx.fillStyle = "#4E342E";
ctx.fillRect(sx + 10, sy + 10, 20, 30);
ctx.fillStyle = "#333";
ctx.fillRect(sx + 12, sy + 12, 16, 26);
} else if (tile === 5) {
// Flag pole
ctx.fillStyle = "#795548";
ctx.fillRect(sx + 17, sy, 6, TILE);
}
}
}
}
function drawFlag() {
const flagTile = 3; // top of pole
const fx = 48 * TILE + 20 - camX;
// Flag triangle
ctx.fillStyle = "#F44336";
ctx.beginPath();
ctx.moveTo(fx, flagTile * TILE + 5);
ctx.lineTo(fx + 30, flagTile * TILE + 20);
ctx.lineTo(fx, flagTile * TILE + 35);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFEB3B";
ctx.font = "14px monospace";
ctx.fillText("★", fx + 8, flagTile * TILE + 25);
}
function drawCoins() {
for (const coin of coins) {
if (coin.collected) continue;
const bobY = Math.sin(coin.bobPhase) * 4;
const sx = coin.x - camX;
const sy = coin.y + bobY;
// Glow
ctx.fillStyle = "rgba(255,215,0,0.3)";
ctx.beginPath();
ctx.arc(sx + coin.w / 2, sy + coin.h / 2, 16, 0, Math.PI * 2);
ctx.fill();
// Coin body
const stretch = Math.abs(Math.sin(coin.bobPhase * 2));
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.ellipse(sx + coin.w / 2, sy + coin.h / 2, 10 - stretch * 3, 10, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "#FFA000";
ctx.lineWidth = 2;
ctx.stroke();
// $ symbol
ctx.fillStyle = "#FFA000";
ctx.font = "bold 12px monospace";
ctx.textAlign = "center";
ctx.fillText("$", sx + coin.w / 2, sy + coin.h / 2 + 5);
ctx.textAlign = "left";
}
}
function drawPlayer() {
const p = player;
const sx = p.x - camX;
const sy = p.y;
ctx.save();
ctx.translate(sx + p.w / 2, sy + p.h / 2);
if (p.facing < 0) ctx.scale(-1, 1);
// Body
ctx.fillStyle = "#E53935";
ctx.fillRect(-10, -14, 20, 22);
// Head
ctx.fillStyle = "#FFCCBC";
ctx.fillRect(-8, -22, 16, 10);
// Hat
ctx.fillStyle = "#E53935";
ctx.fillRect(-10, -26, 22, 6);
ctx.fillRect(-6, -28, 18, 4);
// Eye
ctx.fillStyle = "#000";
ctx.fillRect(2, -19, 4, 4);
// Mustache
ctx.fillStyle = "#5D4037";
ctx.fillRect(-2, -13, 10, 3);
// Legs (animated)
ctx.fillStyle = "#1565C0";
const legOffset = p.onGround ? Math.sin(p.walkTimer) * 4 : 0;
ctx.fillRect(-8, 8, 7, 10 + legOffset);
ctx.fillRect(2, 8, 7, 10 - legOffset);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(-9, 17 + legOffset, 9, 4);
ctx.fillRect(1, 17 - legOffset, 9, 4);
ctx.restore();
}
function drawParticles() {
for (const pt of particles) {
ctx.fillStyle = pt.color;
ctx.globalAlpha = pt.life / 50;
ctx.fillRect(pt.x - camX - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score background
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(10, 10, 180, 36);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(10, 10, 180, 36);
// Coin icon
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(32, 28, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFA000";
ctx.font = "bold 12px monospace";
ctx.textAlign = "center";
ctx.fillText("$", 32, 32);
// Score text
ctx.fillStyle = "#fff";
ctx.font = "bold 18px monospace";
ctx.textAlign = "left";
ctx.fillText(`${score} / ${coins.length}`, 48, 32);
// Controls hint (fades out)
if (score === 0 && player.x < TILE * 4) {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(W / 2 - 160, H - 50, 320, 40);
ctx.fillStyle = "#fff";
ctx.font = "14px monospace";
ctx.textAlign = "center";
ctx.fillText("Arrow Keys / WASD to move • Space / Up to jump", W / 2, H - 25);
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawSky();
drawTiles();
drawFlag();
drawCoins();
drawPlayer();
drawParticles();
drawHUD();
}
// ─── Game Loop ────────────────────────────────────────────────────
function gameLoop() {
// Handle restart
if (gameState === "won" && (keys["Enter"] || keys["Space"])) {
resetGame();
}
update();
draw();
requestAnimationFrame(gameLoop);
}
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">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
font-family: monospace;
}
canvas {
border: 3px solid #444;
border-radius: 4px;
display: block;
image-rendering: pixelated;
}
#info {
color: #aaa;
margin-top: 10px;
font-size: 14px;
text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">Arrow Keys / WASD to move — Space / Up to jump — Collect coins & reach the flag!</div>
<script>
// ============================================================
// SUPER PLATFORMER — single-file, zero dependencies
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// ---- Input ----
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
// prevent page scrolling for game keys
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// ---- Game state ----
let score = 0;
let totalCoins = 0;
let gameState = 'playing'; // 'playing' | 'won'
let winTimer = 0;
// ---- World geometry ----
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.6;
const MAX_SPEED = 5.5;
const JUMP_FORCE = -11.5;
const WORLD_WIDTH = 4200;
// Platforms: { x, y, w, h }
const platforms = [
// ground segments
{ x: 0, y: 400, w: 600, h: 50 },
{ x: 700, y: 400, w: 400, h: 50 },
{ x: 1200, y: 400, w: 800, h: 50 },
{ x: 2200, y: 400, w: 300, h: 50 },
{ x: 2700, y: 400, w: 1500, h: 50 },
// raised platforms
{ x: 250, y: 310, w: 150, h: 20 },
{ x: 500, y: 250, w: 120, h: 20 },
{ x: 750, y: 280, w: 180, h: 20 },
{ x: 1000, y: 220, w: 140, h: 20 },
{ x: 1300, y: 300, w: 200, h: 20 },
{ x: 1550, y: 230, w: 120, h: 20 },
{ x: 1750, y: 170, w: 160, h: 20 },
{ x: 2000, y: 300, w: 150, h: 20 },
{ x: 2400, y: 250, w: 180, h: 20 },
{ x: 2650, y: 190, w: 130, h: 20 },
{ x: 2900, y: 270, w: 200, h: 20 },
{ x: 3200, y: 200, w: 150, h: 20 },
{ x: 3500, y: 150, w: 180, h: 20 },
];
// Coins
const coins = [];
function spawnCoins() {
coins.length = 0;
// on ground
for (let x = 100; x < 4000; x += 120) {
coins.push({ x: x + 15, y: 370, r: 10, collected: false, bobPhase: Math.random() * Math.PI * 2 });
}
// on platforms
platforms.forEach((p, i) => {
if (i === 0 || i === 1) return; // skip first two ground segments
const cx = p.x + p.w / 2 - 10;
const cy = p.y - 30;
coins.push({ x: cx, y: cy, r: 10, collected: false, bobPhase: Math.random() * Math.PI * 2 });
});
// extra floating coins
coins.push({ x: 600, y: 200, r: 10, collected: false, bobPhase: 0 });
coins.push({ x: 900, y: 170, r: 10, collected: false, bobPhase: 0 });
coins.push({ x: 1800, y: 130, r: 10, collected: false, bobPhase: 0 });
coins.push({ x: 3100, y: 160, r: 10, collected: false, bobPhase: 0 });
coins.push({ x: 3400, y: 110, r: 10, collected: false, bobPhase: 0 });
totalCoins = coins.length;
}
spawnCoins();
// Goal flag
const goal = { x: 4050, y: 150, w: 10, h: 250 };
// ---- Player ----
const player = {
x: 80, y: 300, w: 28, h: 36,
vx: 0, vy: 0,
onGround: false,
facing: 1, // 1 right, -1 left
walkFrame: 0,
walkTimer: 0,
squash: 1,
stretch: 1,
};
// ---- Camera ----
const camera = { x: 0, y: 0 };
// ---- 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) * 6,
vy: (Math.random() - 1.5) * 5,
life: 30 + Math.random() * 20,
maxLife: 50,
r: 2 + Math.random() * 3,
color,
});
}
}
// ---- Clouds (decorative) ----
const clouds = [];
for (let i = 0; i < 25; i++) {
clouds.push({
x: Math.random() * WORLD_WIDTH,
y: 30 + Math.random() * 120,
w: 60 + Math.random() * 80,
h: 25 + Math.random() * 20,
speed: 0.1 + Math.random() * 0.2,
});
}
// ---- Hills (decorative background) ----
const hills = [];
for (let i = 0; i < 15; i++) {
hills.push({
x: i * 350 + Math.random() * 100,
r: 60 + Math.random() * 80,
color: `hsl(${120 + Math.random() * 30}, ${40 + Math.random() * 20}%, ${25 + Math.random() * 15}%)`,
});
}
// ============================================================
// UPDATE
// ============================================================
function update() {
if (gameState === 'won') {
winTimer++;
updateParticles();
return;
}
// Input
const moveLeft = keys['ArrowLeft'] || keys['KeyA'];
const moveRight = keys['ArrowRight'] || keys['KeyD'];
const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
// Horizontal movement
if (moveRight) {
player.vx += MOVE_ACCEL;
player.facing = 1;
} else if (moveLeft) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
} else {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Clamp speed
if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
// Jump
if (jumpKey && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
player.squash = 0.7;
player.stretch = 1.3;
spawnParticles(player.x + player.w / 2, player.y + player.h, '#fff', 5);
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 15) player.vy = 15;
// Move X
player.x += player.vx;
// Platform collision X
for (const p of platforms) {
if (rectCollide(player, p)) {
if (player.vx > 0) {
player.x = p.x - player.w;
} else if (player.vx < 0) {
player.x = p.x + p.w;
}
player.vx = 0;
}
}
// Move Y
player.y += player.vy;
player.onGround = false;
// Platform collision Y
for (const p of platforms) {
if (rectCollide(player, p)) {
if (player.vy > 0) {
// Landing on top
player.y = p.y - player.h;
player.vy = 0;
if (!player.onGround) {
player.squash = 1.3;
player.stretch = 0.7;
spawnParticles(player.x + player.w / 2, player.y + player.h, '#ccc', 3);
}
player.onGround = true;
} else if (player.vy < 0) {
// Hit head
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// World bounds
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x > WORLD_WIDTH - player.w) { player.x = WORLD_WIDTH - player.w; player.vx = 0; }
// Fall death
if (player.y > H + 100) {
// Respawn
player.x = 80;
player.y = 300;
player.vx = 0;
player.vy = 0;
score = Math.max(0, score - 5);
}
// Coin collection
const px = player.x + player.w / 2;
const py = player.y + player.h / 2;
for (const c of coins) {
if (c.collected) continue;
const dx = px - c.x;
const dy = py - (c.y + Math.sin(c.bobPhase) * 5);
if (Math.sqrt(dx * dx + dy * dy) < c.r + 18) {
c.collected = true;
score += 10;
spawnParticles(c.x, c.y, '#FFD700', 8);
}
}
// Goal check
if (player.x + player.w > goal.x && player.x < goal.x + goal.w &&
player.y + player.h > goal.y && player.y < goal.y + goal.h) {
gameState = 'won';
winTimer = 0;
spawnParticles(goal.x, goal.y + 30, '#FFD700', 30);
spawnParticles(goal.x, goal.y + 30, '#FF6B6B', 20);
spawnParticles(goal.x, goal.y + 30, '#4ECDC4', 20);
}
// Walk animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.walkTimer++;
if (player.walkTimer % 6 === 0) player.walkFrame = (player.walkFrame + 1) % 4;
} else {
player.walkFrame = 0;
player.walkTimer = 0;
}
// Squash & stretch recovery
player.squash += (1 - player.squash) * 0.15;
player.stretch += (1 - player.stretch) * 0.15;
// Coin bob
for (const c of coins) {
c.bobPhase += 0.06;
}
// Cloud drift
for (const c of clouds) {
c.x += c.speed;
if (c.x > WORLD_WIDTH + 100) c.x = -c.w - 50;
}
updateParticles();
// Camera
const targetX = player.x - W / 2 + player.w / 2;
camera.x += (targetX - camera.x) * 0.08;
camera.x = Math.max(0, Math.min(WORLD_WIDTH - W, camera.x));
}
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);
}
}
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;
}
// ============================================================
// DRAW
// ============================================================
function draw() {
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#4FC3F7');
skyGrad.addColorStop(0.6, '#81D4FA');
skyGrad.addColorStop(1, '#B3E5FC');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.translate(-Math.round(camera.x), -Math.round(camera.y));
// Hills (parallax-ish)
for (const h of hills) {
ctx.fillStyle = h.color;
ctx.beginPath();
ctx.arc(h.x, 420, h.r, Math.PI, 0);
ctx.fill();
}
// Clouds
for (const c of clouds) {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
drawCloud(c.x, c.y, c.w, c.h);
}
// Platforms
for (const p of platforms) {
drawPlatform(p);
}
// Coins
for (const c of coins) {
if (c.collected) continue;
drawCoin(c);
}
// Goal flag
drawGoal();
// Player
drawPlayer();
// Particles
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.restore();
// HUD
drawHUD();
// Win screen
if (gameState === 'won') {
drawWinScreen();
}
// Respawn hint
if (player.y > H) {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff';
ctx.font = '24px monospace';
ctx.textAlign = 'center';
ctx.fillText('Oops! You fell. Respawn...', W / 2, H / 2);
}
}
function drawCloud(x, y, w, h) {
ctx.beginPath();
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + w * 0.25, y + h * 0.3, w * 0.3, h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + w * 0.7, y + h * 0.35, w * 0.25, h * 0.35, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawPlatform(p) {
// Main block
const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.h);
if (p.h > 30) {
// Ground
grad.addColorStop(0, '#4CAF50');
grad.addColorStop(0.15, '#388E3C');
grad.addColorStop(0.2, '#8D6E63');
grad.addColorStop(1, '#5D4037');
} else {
// Floating platform
grad.addColorStop(0, '#FF8A65');
grad.addColorStop(1, '#D84315');
}
ctx.fillStyle = grad;
ctx.fillRect(p.x, p.y, p.w, p.h);
// Top highlight
ctx.fillStyle = p.h > 30 ? 'rgba(76,175,80,0.6)' : 'rgba(255,138,101,0.5)';
ctx.fillRect(p.x, p.y, p.w, 4);
// Border
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 1;
ctx.strokeRect(p.x, p.y, p.w, p.h);
// Grass tufts on ground
if (p.h > 30) {
ctx.fillStyle = '#66BB6A';
for (let gx = p.x + 5; gx < p.x + p.w - 5; gx += 15) {
ctx.beginPath();
ctx.moveTo(gx, p.y);
ctx.quadraticCurveTo(gx + 3, p.y - 6, gx + 6, p.y);
ctx.fill();
}
}
}
function drawCoin(c) {
const bob = Math.sin(c.bobPhase) * 5;
const cy = c.y + bob;
const stretch = Math.abs(Math.cos(c.bobPhase * 0.5));
ctx.save();
ctx.translate(c.x, cy);
ctx.scale(Math.max(0.2, stretch), 1);
// Glow
ctx.fillStyle = 'rgba(255,215,0,0.3)';
ctx.beginPath();
ctx.arc(0, 0, c.r + 4, 0, Math.PI * 2);
ctx.fill();
// Coin body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(0, 0, c.r, 0, Math.PI * 2);
ctx.fill();
// Inner circle
ctx.fillStyle = '#FFC107';
ctx.beginPath();
ctx.arc(0, 0, c.r * 0.6, 0, Math.PI * 2);
ctx.fill();
// $ symbol
ctx.fillStyle = '#FFF8E1';
ctx.font = 'bold 11px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('$', 0, 1);
ctx.restore();
}
function drawGoal() {
// Pole
ctx.fillStyle = '#795548';
ctx.fillRect(goal.x, goal.y, goal.w, goal.h);
// Ball on top
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(goal.x + goal.w / 2, goal.y, 8, 0, Math.PI * 2);
ctx.fill();
// Flag
const wave = Math.sin(Date.now() / 300) * 5;
ctx.fillStyle = '#F44336';
ctx.beginPath();
ctx.moveTo(goal.x + goal.w, goal.y + 10);
ctx.lineTo(goal.x + goal.w + 40 + wave, goal.y + 25);
ctx.lineTo(goal.x + goal.w, goal.y + 45);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = '#FFD700';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('★', goal.x + goal.w + 18 + wave / 2, goal.y + 30);
}
function drawPlayer() {
ctx.save();
const cx = player.x + player.w / 2;
const cy = player.y + player.h / 2;
ctx.translate(cx, cy);
ctx.scale(player.facing, 1);
ctx.scale(player.squash, player.stretch);
const hw = player.w / 2;
const hh = player.h / 2;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(2, hh + 2, hw * 0.8, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Body (blue overalls)
ctx.fillStyle = '#1565C0';
roundRect(-hw + 2, -hh + 14, player.w - 4, hh - 2, 3);
// Shirt (red)
ctx.fillStyle = '#E53935';
roundRect(-hw + 2, -hh + 8, player.w - 4, 10, 3);
// Head
ctx.fillStyle = '#FFCC80';
roundRect(-hw + 4, -hh + 2, player.w - 8, 12, 4);
// Hat
ctx.fillStyle = '#E53935';
roundRect(-hw + 2, -hh, player.w - 4, 7, 3);
// Hat brim
ctx.fillStyle = '#C62828';
roundRect(-hw + 1, -hh + 4, player.w - 2, 4, 2);
// Eyes
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(4, -hh + 8, 2.5, 0, Math.PI * 2);
ctx.fill();
// Mustache
ctx.fillStyle = '#5D4037';
roundRect(1, -hh + 11, 12, 3, 1);
// Legs animation
const legOffset = player.onGround ? Math.sin(player.walkFrame * Math.PI / 2) * 4 : 3;
ctx.fillStyle = '#1565C0';
roundRect(-hw + 4, hh - 8, 8, 8, 2);
roundRect(hw - 12, hh - 8, 8, 8, 2);
// Shoes
ctx.fillStyle = '#5D4037';
roundRect(-hw + 3, hh - 4, 10, 4, 2);
roundRect(hw - 13, hh - 4, 10, 4, 2);
ctx.restore();
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.fill();
}
function drawHUD() {
// Score background
ctx.fillStyle = 'rgba(0,0,0,0.4)';
roundRectHUD(10, 10, 200, 40, 8);
// Coin icon
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(35, 30, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFF8E1';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.fillText('$', 35, 33);
// Score text
ctx.fillStyle = '#fff';
ctx.font = 'bold 20px monospace';
ctx.textAlign = 'left';
ctx.fillText(`${score}`, 52, 35);
// Coin counter
const collected = coins.filter(c => c.collected).length;
ctx.fillStyle = 'rgba(0,0,0,0.4)';
roundRectHUD(220, 10, 180, 40, 8);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 16px monospace';
ctx.textAlign = 'left';
ctx.fillText(`Coins: ${collected} / ${totalCoins}`, 232, 35);
// Distance marker
const dist = Math.floor(player.x / 10);
ctx.fillStyle = 'rgba(0,0,0,0.4)';
roundRectHUD(W - 130, 10, 120, 30, 8);
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.textAlign = 'right';
ctx.fillText(`${dist}m`, W - 20, 31);
}
function roundRectHUD(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.fill();
}
function drawWinScreen() {
const alpha = Math.min(1, winTimer / 60);
ctx.fillStyle = `rgba(0,0,0,${alpha * 0.6})`;
ctx.fillRect(0, 0, W, H);
if (winTimer > 30) {
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.fillText('🎉 YOU WIN! 🎉', W / 2, H / 2 - 40);
}
if (winTimer > 60) {
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.font = '24px monospace';
ctx.textAlign = 'center';
ctx.fillText(`Score: ${score} | Coins: ${coins.filter(c => c.collected).length} / ${totalCoins}`, W / 2, H / 2 + 20);
}
if (winTimer > 90) {
const pulse = 0.8 + Math.sin(Date.now() / 200) * 0.2;
ctx.fillStyle = `rgba(255,255,255,${alpha * pulse})`;
ctx.font = '18px monospace';
ctx.textAlign = 'center';
ctx.fillText('Press R to restart', W / 2, H / 2 + 60);
}
// Restart
if (winTimer > 90 && (keys['KeyR'] || keys['Enter'])) {
restartGame();
}
}
function restartGame() {
score = 0;
gameState = 'playing';
winTimer = 0;
player.x = 80;
player.y = 300;
player.vx = 0;
player.vy = 0;
player.onGround = false;
spawnCoins();
particles.length = 0;
}
// ============================================================
// GAME LOOP
// ============================================================
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Start
gameLoop();
</script>
</body>
</html>