index.html not found in workspace (the model did not create it)
(no files produced)
index.html not found in workspace (the model did not create it)
(no files produced)
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; }
body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: monospace;
overflow: hidden;
}
#game {
border: 3px solid #333;
border-radius: 4px;
display: block;
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 = 800, H = 450;
// ─── Input ───
const keys = {};
window.addEventListener("keydown", e => {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
e.preventDefault();
}
});
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"]; }
// ─── Constants ───
const GRAVITY = 0.55;
const JUMP_FORCE = -11;
const MOVE_SPEED = 4.2;
const MAX_FALL = 12;
const WORLD_W = 4800;
// ─── Camera ───
let camX = 0;
function setCam(target) {
camX = target - W * 0.35;
camX = Math.max(0, Math.min(WORLD_W - W, camX));
}
// ─── Platforms ───
// Each: { x, y, w, h, color, topColor?, pattern? }
const platforms = [];
// Ground segments (with gaps)
function ground(x, w, h) {
h = h || 32;
platforms.push({ x, y: H - h, w, h, color: "#5a8f3c", topColor: "#6abf42", pattern: "grass" });
}
ground(0, 600);
ground(680, 500);
ground(1260, 400);
ground(1720, 600);
ground(2400, 350);
ground(2840, 500);
ground(3420, 400);
ground(3900, 900); // final stretch
// Raised platforms
function plat(x, y, w, color) {
color = color || "#c47a4a";
const h = 18;
platforms.push({ x, y, w, h, color, topColor: "#e8a86a" });
}
// Level layout
plat(200, 320, 100);
plat(380, 260, 80);
plat(520, 200, 120, "#8a6ead");
plat(700, 310, 90);
plat(830, 250, 100);
plat(970, 190, 80, "#8a6ead");
plat(1320, 330, 100);
plat(1460, 270, 80);
plat(1600, 210, 120, "#8a6ead");
plat(1780, 300, 90);
plat(1920, 240, 100);
plat(2060, 180, 80, "#8a6ead");
plat(2200, 300, 100);
plat(2480, 320, 90);
plat(2620, 260, 80);
plat(2760, 200, 100, "#8a6ead");
plat(2920, 310, 90);
plat(3060, 250, 100);
plat(3200, 190, 80, "#8a6ead");
plat(3500, 300, 100);
plat(3680, 240, 90);
plat(3820, 180, 80, "#8a6ead");
plat(3960, 310, 100);
plat(4120, 250, 120, "#4a8ac4");
plat(4300, 200, 80, "#8a6ead");
// ─── Coins ───
const coins = [];
function coin(x, y) {
coins.push({ x, y, collected: false, bobPhase: Math.random() * Math.PI * 2 });
}
// Coins on/above platforms
coin(230, 290); coin(240, 290);
coin(410, 230); coin(420, 230);
coin(550, 170); coin(560, 170); coin(570, 170);
coin(730, 280); coin(740, 280);
coin(860, 220); coin(870, 220);
coin(1000, 160); coin(1010, 160);
coin(1350, 300); coin(1360, 300);
coin(1490, 240); coin(1500, 240);
coin(1630, 180); coin(1640, 180); coin(1650, 180);
coin(1810, 270); coin(1820, 270);
coin(1950, 210); coin(1960, 210);
coin(2090, 150); coin(2100, 150);
coin(2230, 270); coin(2240, 270);
coin(2510, 290); coin(2520, 290);
coin(2650, 230); coin(2660, 230);
coin(2790, 170); coin(2800, 170); coin(2810, 170);
coin(2950, 280); coin(2960, 280);
coin(3090, 220); coin(3100, 220);
coin(3230, 160); coin(3240, 160);
coin(3530, 270); coin(3540, 270);
coin(3710, 210); coin(3720, 210);
coin(3850, 150); coin(3860, 150);
coin(3990, 280); coin(4000, 280);
coin(4150, 220); coin(4160, 220);
coin(4330, 170); coin(4340, 170);
// Coins in the air (jumping challenges)
coin(640, 200);
coin(1200, 250);
coin(2350, 180);
coin(3350, 200);
let score = 0;
let coinsCollected = 0;
let totalCoins = coins.length;
// ─── Enemies (simple goombas) ───
const enemies = [];
function enemy(x, y, range) {
enemies.push({ x, y, startX: x, range, dir: 1, w: 28, h: 26, alive: true, squishTimer: 0 });
}
enemy(350, H - 32 - 26, 80);
enemy(800, H - 32 - 26, 100);
enemy(1380, H - 32 - 26, 60);
enemy(1900, H - 32 - 26, 90);
enemy(2500, H - 32 - 26, 70);
enemy(2900, H - 32 - 26, 80);
enemy(3500, H - 32 - 26, 100);
enemy(4050, H - 32 - 26, 60);
enemy(4350, H - 32 - 26, 50);
// ─── Flag ───
const flag = { x: 4580, y: H - 32 - 110, h: 110, reached: false, hit: false };
// ─── Decorations (clouds, bushes, hills) ───
const clouds = [];
for (let i = 0; i < 30; i++) {
clouds.push({
x: i * 180 + Math.random() * 100,
y: 30 + Math.random() * 80,
w: 60 + Math.random() * 80,
h: 25 + Math.random() * 15,
speed: 0.1 + Math.random() * 0.2
});
}
const bushes = [];
for (let i = 0; i < 20; i++) {
bushes.push({
x: i * 260 + Math.random() * 100,
w: 40 + Math.random() * 50,
h: 18 + Math.random() * 12
});
}
const bgHills = [];
for (let i = 0; i < 15; i++) {
bgHills.push({
x: i * 350 + Math.random() * 100,
w: 180 + Math.random() * 120,
h: 60 + Math.random() * 50
});
}
// ─── Particles ───
const particles = [];
function spawnParticles(x, y, color, count, spread) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * spread,
vy: -Math.random() * spread * 0.8,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3
});
}
}
// ─── Stars (floating decoration) ───
const stars = [];
for (let i = 0; i < 40; i++) {
stars.push({
x: Math.random() * WORLD_W,
y: Math.random() * 150,
size: 1 + Math.random() * 2,
twinkle: Math.random() * Math.PI * 2
});
}
// ─── Player ───
const player = {
x: 80, y: H - 60,
vx: 0, vy: 0,
w: 24, h: 32,
grounded: false,
facing: 1, // 1 = right, -1 = left
animFrame: 0,
animTimer: 0,
alive: true,
deadTimer: 0
};
let gameState = "playing"; // "playing", "won", "dead"
let winTimer = 0;
let deathTimer = 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;
}
// ─── Update ───
let lastTime = 0;
let fixedAccum = 0;
const STEP = 1000 / 60;
function update(dt) {
if (gameState === "won") {
winTimer++;
if (winTimer > 120) gameState = "title";
return;
}
if (gameState === "dead") {
deathTimer++;
if (deathTimer > 90) {
respawn();
}
return;
}
// Player horizontal movement
let moveX = 0;
if (isLeft()) moveX -= MOVE_SPEED;
if (isRight()) moveX += MOVE_SPEED;
player.vx = moveX;
if (moveX !== 0) player.facing = moveX > 0 ? 1 : -1;
// Jumping
if (isJump() && player.grounded) {
player.vy = JUMP_FORCE;
player.grounded = false;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// Move X
player.x += player.vx;
// Clamp to world
if (player.x < 0) player.x = 0;
if (player.x + player.w > WORLD_W) player.x = WORLD_W - player.w;
// Collision X
player.grounded = false;
for (const p of platforms) {
if (rectOverlap(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;
// Collision Y
for (const p of platforms) {
if (rectOverlap(player, p)) {
if (player.vy > 0) {
player.y = p.y - player.h;
player.vy = 0;
player.grounded = true;
} else if (player.vy < 0) {
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// Fall into pit
if (player.y > H + 50) {
killPlayer();
return;
}
// Animation
if (player.grounded && Math.abs(player.vx) > 0.5) {
player.animTimer += dt;
if (player.animTimer > 120) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
} else {
player.animFrame = 0;
player.animTimer = 0;
}
// Camera
setCam(player.x);
// Clouds
for (const c of clouds) {
c.x += c.speed;
if (c.x > WORLD_W + 100) c.x = -100;
}
// Coins
for (const c of coins) {
if (c.collected) continue;
c.bobPhase += 0.05;
const coinRect = { x: c.x - 8, y: c.y + Math.sin(c.bobPhase) * 3 - 8, w: 16, h: 16 };
if (rectOverlap(player, coinRect)) {
c.collected = true;
coinsCollected++;
score += 100;
spawnParticles(c.x, c.y, "#ffd700", 8, 5);
}
}
// Enemies
for (const e of enemies) {
if (!e.alive) {
if (e.squishTimer > 0) e.squishTimer--;
continue;
}
e.x += e.dir * 1.2;
if (e.x > e.startX + e.range || e.x + e.w < e.startX) {
e.dir *= -1;
}
// Collision with player
if (rectOverlap(player, e)) {
// Stomp from above?
if (player.vy > 0 && player.y + player.h - e.y < 12) {
e.alive = false;
e.squishTimer = 30;
player.vy = -7;
score += 200;
spawnParticles(e.x + e.w/2, e.y, "#c47a4a", 6, 4);
} else {
killPlayer();
return;
}
}
}
// Flag
if (!flag.reached && !flag.hit) {
const flagRect = { x: flag.x - 5, y: flag.y, w: 20, h: flag.h };
if (rectOverlap(player, flagRect) || Math.abs(player.x + player.w/2 - flag.x) < 20 && player.y + player.h > flag.y + flag.h - 30) {
flag.reached = true;
score += 1000;
spawnParticles(flag.x, flag.y + 20, "#ff4444", 20, 8);
spawnParticles(flag.x, flag.y + 40, "#ffd700", 15, 6);
gameState = "won";
}
}
// 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 killPlayer() {
gameState = "dead";
deathTimer = 0;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#ff6644", 12, 6);
}
function respawn() {
gameState = "playing";
player.x = 80;
player.y = H - 60;
player.vx = 0;
player.vy = 0;
player.alive = true;
deathTimer = 0;
camX = 0;
}
// ─── Drawing helpers ───
function drawSky() {
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 drawHills() {
for (const h of bgHills) {
const sx = h.x - camX * 0.3;
if (sx > -h.w && sx < W + h.w) {
ctx.fillStyle = "#5a9f55";
ctx.beginPath();
ctx.ellipse(sx + h.w/2, H - 20, h.w/2, h.h, 0, Math.PI, 0);
ctx.fill();
ctx.fillStyle = "#4d8b47";
ctx.beginPath();
ctx.ellipse(sx + h.w/2, H - 15, h.w/2 * 0.8, h.h * 0.7, 0, Math.PI, 0);
ctx.fill();
}
}
}
function drawClouds() {
for (const c of clouds) {
const sx = c.x - camX * 0.15;
if (sx > -c.w && sx < W + c.w) {
ctx.fillStyle = "rgba(255,255,255,0.85)";
ctx.beginPath();
ctx.ellipse(sx, c.y, c.w/2, c.h/2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(sx - c.w*0.25, c.y + 4, c.w*0.35, c.h*0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(sx + c.w*0.25, c.y + 2, c.w*0.3, c.h*0.35, 0, 0, Math.PI * 2);
ctx.fill();
}
}
}
function drawBushes() {
for (const b of bushes) {
const sx = b.x - camX;
if (sx > -b.w && sx < W + b.w) {
ctx.fillStyle = "#3d7a35";
ctx.beginPath();
ctx.ellipse(sx + b.w/2, H - 28, b.w/2, b.h, 0, Math.PI, 0);
ctx.fill();
ctx.fillStyle = "#4d9a42";
ctx.beginPath();
ctx.ellipse(sx + b.w/2 - 5, H - 30, b.w*0.35, b.h*0.7, 0, Math.PI, 0);
ctx.fill();
}
}
}
function drawStars() {
const t = performance.now() / 1000;
for (const s of stars) {
const sx = s.x - camX * 0.05;
const alpha = 0.4 + Math.sin(t * 2 + s.twinkle) * 0.3;
ctx.fillStyle = `rgba(255,255,200,${alpha})`;
ctx.fillRect(sx, s.y, s.size, s.size);
}
}
function drawPlatform(p) {
const sx = p.x - camX;
if (sx > -p.w - 10 && sx < W + 10) {
// Main body
ctx.fillStyle = p.color || "#c47a4a";
ctx.fillRect(sx, p.y, p.w, p.h);
// Top
ctx.fillStyle = p.topColor || "#e8a86a";
ctx.fillRect(sx, p.y, p.w, 4);
// Grass texture on ground
if (p.pattern === "grass") {
// Darker underlayer
ctx.fillStyle = "#4a6f2e";
ctx.fillRect(sx, p.y + 4, p.w, p.h - 4);
// Grass blades
ctx.fillStyle = "#6abf42";
for (let gx = sx; gx < sx + p.w; gx += 8) {
const gh = 3 + ((gx * 7) % 5);
ctx.fillRect(gx, p.y - gh, 4, gh);
}
// Dirt detail
ctx.fillStyle = "#5a7f3c";
for (let dx = sx + 4; dx < sx + p.w - 4; dx += 20) {
ctx.fillRect(dx, p.y + 10, 8, 3);
ctx.fillRect(dx + 10, p.y + 20, 6, 2);
}
}
// Side edges
ctx.fillStyle = shadeColor(p.color || "#c47a4a", -20);
ctx.fillRect(sx, p.y + 4, 2, p.h - 4);
ctx.fillRect(sx + p.w - 2, p.y + 4, 2, p.h - 4);
// Bottom
ctx.fillStyle = shadeColor(p.color || "#c47a4a", -30);
ctx.fillRect(sx, p.y + p.h - 2, p.w, 2);
}
}
function shadeColor(hex, amount) {
let r = parseInt(hex.slice(1,3), 16) + amount;
let g = parseInt(hex.slice(3,5), 16) + amount;
let b = parseInt(hex.slice(5,7), 16) + amount;
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
return "#" + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
}
function drawCoin(c) {
if (c.collected) return;
const sx = c.x - camX;
const sy = c.y + Math.sin(c.bobPhase) * 3;
if (sx > -20 && sx < W + 20) {
// 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 stretch = 0.7 + Math.abs(Math.sin(c.bobPhase * 1.5)) * 0.3;
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.ellipse(sx, sy, 7 * stretch, 7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffec44";
ctx.beginPath();
ctx.ellipse(sx - 1, sy - 1, 4 * stretch, 4, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawEnemy(e) {
const sx = e.x - camX;
if (sx > -40 && sx < W + 40) {
if (!e.alive) {
if (e.squishTimer > 0) {
ctx.fillStyle = "#8B5E3C";
ctx.fillRect(sx, e.y + e.h - 6, e.w, 6);
}
return;
}
// Body
ctx.fillStyle = "#8B4513";
ctx.beginPath();
ctx.ellipse(sx + e.w/2, e.y + e.h * 0.5, e.w/2, e.h * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Lighter top
ctx.fillStyle = "#a0652a";
ctx.beginPath();
ctx.ellipse(sx + e.w/2, e.y + e.h * 0.35, e.w/2 - 2, e.h * 0.35, 0, Math.PI, 0);
ctx.fill();
// Eyes
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(sx + e.w/2 - 6, e.y + e.h * 0.4, 4, 0, Math.PI * 2);
ctx.arc(sx + e.w/2 + 6, e.y + e.h * 0.4, 4, 0, Math.PI * 2);
ctx.fill();
// Pupils
ctx.fillStyle = "#111";
const pupilOff = e.dir * 2;
ctx.beginPath();
ctx.arc(sx + e.w/2 - 6 + pupilOff, e.y + e.h * 0.42, 2, 0, Math.PI * 2);
ctx.arc(sx + e.w/2 + 6 + pupilOff, e.y + e.h * 0.42, 2, 0, Math.PI * 2);
ctx.fill();
// Angry eyebrows
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(sx + e.w/2 - 10, e.y + e.h * 0.22);
ctx.lineTo(sx + e.w/2 - 3, e.y + e.h * 0.3);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx + e.w/2 + 10, e.y + e.h * 0.22);
ctx.lineTo(sx + e.w/2 + 3, e.y + e.h * 0.3);
ctx.stroke();
// Feet
const footAnim = Math.sin(performance.now() / 100) * 3;
ctx.fillStyle = "#5a3510";
ctx.fillRect(sx + 2, e.y + e.h - 5, 8, 5);
ctx.fillRect(sx + e.w - 10, e.y + e.h - 5, 8, 5);
}
}
function drawFlag() {
const sx = flag.x - camX;
if (sx > -30 && sx < W + 30) {
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(sx - 2, flag.y, 5, flag.h);
// Ball on top
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(sx + 1, flag.y, 6, 0, Math.PI * 2);
ctx.fill();
// Flag
const waveOffset = Math.sin(performance.now() / 200) * 4;
ctx.fillStyle = flag.reached ? "#44ff44" : "#ff4444";
ctx.beginPath();
ctx.moveTo(sx + 3, flag.y + 5);
ctx.lineTo(sx + 35 + waveOffset, flag.y + 18);
ctx.lineTo(sx + 3, flag.y + 35);
ctx.closePath();
ctx.fill();
// Flag highlight
ctx.fillStyle = flag.reached ? "#88ff88" : "#ff8888";
ctx.beginPath();
ctx.moveTo(sx + 3, flag.y + 8);
ctx.lineTo(sx + 20 + waveOffset * 0.5, flag.y + 18);
ctx.lineTo(sx + 3, flag.y + 28);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#fff";
drawStar(sx + 18 + waveOffset * 0.5, flag.y + 18, 5, 2, 5);
}
}
function drawStar(cx, cy, innerR, outerR, points) {
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = (i * Math.PI / points) - Math.PI / 2;
if (i === 0) ctx.moveTo(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
else ctx.lineTo(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
}
ctx.closePath();
ctx.fill();
}
function drawPlayer() {
if (gameState === "dead") return;
const sx = player.x - camX;
const sy = player.y;
const f = player.facing;
ctx.save();
// Shadow
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.beginPath();
ctx.ellipse(sx + player.w/2, sy + player.h + 1, player.w * 0.5, 3, 0, 0, Math.PI * 2);
ctx.fill();
// Body colors
const skinColor = "#ffcc99";
const shirtColor = "#cc3333";
const pantsColor = "#3344aa";
const shoeColor = "#8B4513";
// Legs
if (!player.grounded) {
// Jump pose
ctx.fillStyle = pantsColor;
ctx.fillRect(sx + 4, sy + 22, 7, 8);
ctx.fillRect(sx + 13, sy + 22, 7, 8);
ctx.fillStyle = shoeColor;
ctx.fillRect(sx + 2, sy + 26, 9, 6);
ctx.fillRect(sx + 13, sy + 26, 9, 6);
} else if (Math.abs(player.vx) > 0.5) {
// Running animation
const legAnim = player.animFrame;
ctx.fillStyle = pantsColor;
if (legAnim === 0) {
ctx.fillRect(sx + 5, sy + 20, 6, 8);
ctx.fillRect(sx + 13, sy + 20, 6, 8);
} else if (legAnim === 1) {
ctx.fillRect(sx + 3, sy + 20, 7, 8);
ctx.fillRect(sx + 14, sy + 22, 7, 7);
} else if (legAnim === 2) {
ctx.fillRect(sx + 7, sy + 20, 6, 8);
ctx.fillRect(sx + 11, sy + 20, 6, 8);
} else {
ctx.fillRect(sx + 14, sy + 20, 7, 8);
ctx.fillRect(sx + 3, sy + 22, 7, 7);
}
// Shoes
ctx.fillStyle = shoeColor;
const shoeOffsets = legAnim === 1 ? [-1, 2] : legAnim === 3 ? [2, -1] : [0, 0];
ctx.fillRect(sx + 4 + shoeOffsets[0], sy + 27, 8, 5);
ctx.fillRect(sx + 12 + shoeOffsets[1], sy + 27, 8, 5);
} else {
// Standing
ctx.fillStyle = pantsColor;
ctx.fillRect(sx + 5, sy + 20, 6, 8);
ctx.fillRect(sx + 13, sy + 20, 6, 8);
ctx.fillStyle = shoeColor;
ctx.fillRect(sx + 4, sy + 26, 8, 6);
ctx.fillRect(sx + 12, sy + 26, 8, 6);
}
// Torso / shirt
ctx.fillStyle = shirtColor;
ctx.fillRect(sx + 3, sy + 12, player.w - 6, 10);
// Belt
ctx.fillStyle = "#aa2222";
ctx.fillRect(sx + 4, sy + 19, player.w - 8, 3);
// Arms
ctx.fillStyle = skinColor;
if (!player.grounded) {
ctx.fillRect(sx - 2, sy + 12, 6, 2);
ctx.fillRect(sx + player.w - 4, sy + 12, 6, 2);
} else {
const armSwing = Math.abs(player.vx) > 0.5 ? Math.sin(performance.now() / 80) * 4 : 0;
ctx.fillRect(sx - 2, sy + 13 + armSwing, 6, 2);
ctx.fillRect(sx + player.w - 4, sy + 13 - armSwing, 6, 2);
}
// Head
ctx.fillStyle = skinColor;
ctx.beginPath();
ctx.arc(sx + player.w/2, sy + 8, 8, 0, Math.PI * 2);
ctx.fill();
// Hat
ctx.fillStyle = "#cc0000";
ctx.fillRect(sx + 2, sy - 1, player.w - 4, 6);
// Hat brim
ctx.fillRect(sx - f === 1 ? -1 : player.w - 3, sy + 1, 6, 3);
// Hat logo 'S'
ctx.fillStyle = "#fff";
ctx.font = "bold 7px monospace";
ctx.textAlign = "center";
ctx.fillText("S", sx + player.w/2, sy + 5);
// Eyes
ctx.fillStyle = "#222";
const eyeX = f === 1 ? 4 : -4;
ctx.fillRect(sx + player.w/2 + eyeX - 1, sy + 7, 3, 3);
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
const sx = p.x - camX;
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(sx - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score bar background
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, 36);
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(0, 34, W, 2);
// Coins icon
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(28, 18, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffec44";
ctx.beginPath();
ctx.arc(26, 16, 5, 0, Math.PI * 2);
ctx.fill();
// Coin count
ctx.fillStyle = "#fff";
ctx.font = "bold 18px monospace";
ctx.textAlign = "left";
ctx.fillText("× " + coinsCollected + " / " + totalCoins, 48, 24);
// Score
ctx.fillStyle = "#ffd700";
ctx.font = "bold 20px monospace";
ctx.textAlign = "center";
ctx.fillText("SCORE: " + score, W / 2, 25);
// Lives indicator
ctx.fillStyle = "#ff6644";
ctx.font = "bold 16px monospace";
ctx.textAlign = "right";
ctx.fillText("🏁 FLAG", W - 15, 24);
// Progress bar
const progress = Math.min(1, player.x / flag.x);
ctx.fillStyle = "rgba(255,255,255,0.2)";
ctx.fillRect(W - 160, 30, 140, 6);
ctx.fillStyle = "#44dd44";
ctx.fillRect(W - 160, 30, 140 * progress, 6);
}
function drawWinScreen() {
const alpha = Math.min(1, winTimer / 60);
ctx.fillStyle = "rgba(0,0,0," + (alpha * 0.6) + ")";
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = alpha;
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 = "bold 24px monospace";
ctx.fillText("Score: " + score, W/2, H/2 + 20);
ctx.fillText("Coins: " + coinsCollected + " / " + totalCoins, W/2, H/2 + 55);
ctx.fillStyle = "#aaa";
ctx.font = "16px monospace";
ctx.fillText("Press R to play again", W/2, H/2 + 100);
ctx.globalAlpha = 1;
if (keys["KeyR"]) {
resetGame();
}
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(60,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ff4444";
ctx.font = "bold 36px monospace";
ctx.textAlign = "center";
ctx.fillText("OUCH!", W/2, H/2 - 10);
ctx.fillStyle = "#ddd";
ctx.font = "18px monospace";
ctx.fillText("Respawning...", W/2, H/2 + 25);
}
function drawTitle() {
drawSky();
drawHills();
drawClouds();
drawBushes();
for (const p of platforms) {
if (p.x - camX >= -p.w - 10 && p.x - camX < W + 10) {
drawPlatform(p);
}
}
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ffd700";
ctx.font = "bold 44px monospace";
ctx.textAlign = "center";
ctx.fillText("SUPER PIXEL", W/2, H/2 - 60);
ctx.fillText("ADVENTURE", W/2, H/2 - 10);
ctx.fillStyle = "#fff";
ctx.font = "18px monospace";
ctx.fillText("Arrow Keys / WASD to move", W/2, H/2 + 40);
ctx.fillText("Space / Up / W to jump", W/2, H/2 + 65);
ctx.fillStyle = "#aaa";
ctx.font = "14px monospace";
ctx.fillText("Collect coins, stomp enemies, reach the flag!", W/2, H/2 + 100);
ctx.fillStyle = "#88ff88";
ctx.font = "bold 20px monospace";
const blink = Math.sin(performance.now() / 300) > 0;
if (blink) ctx.fillText("Press any key to start", W/2, H/2 + 145);
}
let titleShown = true;
let startPressed = false;
function resetGame() {
score = 0;
coinsCollected = 0;
gameState = "playing";
winTimer = 0;
deathTimer = 0;
player.x = 80;
player.y = H - 60;
player.vx = 0;
player.vy = 0;
player.alive = true;
player.grounded = false;
camX = 0;
flag.reached = false;
flag.hit = false;
particles.length = 0;
for (const c of coins) c.collected = false;
for (const e of enemies) {
e.alive = true;
e.squishTimer = 0;
e.x = e.startX;
}
startPressed = false;
titleShown = true;
}
// ─── Main Loop ───
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
let dt = timestamp - lastTime;
lastTime = timestamp;
if (dt > 100) dt = 100;
// Title screen
if (titleShown && !startPressed) {
drawTitle();
if (Object.values(keys).some(v => v)) {
startPressed = true;
}
requestAnimationFrame(gameLoop);
return;
}
// Fixed timestep update
fixedAccum += dt;
while (fixedAccum >= STEP) {
update(STEP);
fixedAccum -= STEP;
}
// Draw
ctx.clearRect(0, 0, W, H);
if (gameState === "playing" || gameState === "won" || gameState === "dead") {
drawSky();
drawStars();
drawHills();
drawClouds();
drawBushes();
for (const p of platforms) {
drawPlatform(p);
}
for (const c of coins) {
drawCoin(c);
}
for (const e of enemies) {
drawEnemy(e);
}
drawFlag();
drawParticles();
drawPlayer();
drawHUD();
if (gameState === "won") drawWinScreen();
if (gameState === "dead") drawDeathScreen();
}
requestAnimationFrame(gameLoop);
}
// ─── Prevent all keys from doing things on title ───
window.addEventListener("keydown", e => {
if (!startPressed) startPressed = true;
});
// Start
camX = 0;
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 Mini Mario</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: 'Courier New', monospace;
overflow: hidden;
}
#game {
border: 3px solid #0f0f23;
border-radius: 4px;
display: block;
}
#info {
color: #8888aa;
text-align: center;
margin-top: 12px;
font-size: 13px;
}
#info span { color: #ffcc00; }
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">
<span>← →</span> or <span>A D</span> to move ·
<span>Space</span> or <span>↑</span> to jump ·
Collect coins and reach the <span>🏷</span> flag! 🎯
</div>
<script>
(function() {
"use strict";
// ─── Canvas ───
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
// ─── Constants ───
const GRAVITY = 0.6;
const MAX_FALL = 12;
const MOVE_SPEED = 4.5;
const JUMP_FORCE = -12;
const COIN_COUNT = 25;
// ─── Level geometry ───
const LEVEL_W = 3800;
// Platforms: {x, y, w, h, color}
const platforms = [
// Ground segments
{ x: 0, y: 400, w: 900, h: 50, color: "#4a8c3f" },
{ x: 1000, y: 400, w: 1300, h: 50, color: "#4a8c3f" },
{ x: 2400, y: 400, w: 1400, h: 50, color: "#4a8c3f" },
// Raised platforms
{ x: 300, y: 310, w: 120, h: 24, color: "#b5651d" },
{ x: 520, y: 250, w: 120, h: 24, color: "#b5651d" },
{ x: 730, y: 190, w: 160, h: 24, color: "#b5651d" },
{ x: 1050, y: 310, w: 200, h: 24, color: "#b5651d" },
{ x: 1300, y: 240, w: 100, h: 24, color: "#b5651d" },
{ x: 1480, y: 180, w: 100, h: 24, color: "#b5651d" },
{ x: 1660, y: 310, w: 140, h: 24, color: "#b5651d" },
{ x: 1850, y: 250, w: 120, h: 24, color: "#b5651d" },
{ x: 2050, y: 190, w: 100, h: 24, color: "#b5651d" },
{ x: 2200, y: 310, w: 140, h: 24, color: "#b5651d" },
{ x: 2450, y: 310, w: 180, h: 24, color: "#b5651d" },
{ x: 2680, y: 240, w: 100, h: 24, color: "#b5651d" },
{ x: 2860, y: 180, w: 120, h: 24, color: "#b5651d" },
// Final approach
{ x: 3100, y: 310, w: 260, h: 24, color: "#b5651d" },
{ x: 3400, y: 340, w: 300, h: 60, color: "#5a4a3a" },
];
// Pipes
const pipes = [
{ x: 700, y: 330, w: 50, h: 70 },
{ x: 1500, y: 330, w: 50, h: 70 },
{ x: 2700, y: 330, w: 50, h: 70 },
];
// Question blocks (decorative)
const qBlocks = [
{ x: 400, y: 270 },
{ x: 1150, y: 260 },
{ x: 2500, y: 260 },
];
// Coins
const coins = [];
function spawnCoins() {
const coinPositions = [
[320, 280], [340, 280], [360, 280],
[540, 220], [560, 220], [580, 220],
[750, 160], [770, 160], [790, 160], [810, 160],
[1080, 280], [1100, 280], [1120, 280],
[1320, 210], [1340, 210],
[1500, 150], [1520, 150],
[1690, 280], [1710, 280], [1730, 280], [1750, 280],
[1880, 220], [1900, 220],
[2070, 160], [2090, 160],
[2230, 280], [2250, 280], [2270, 280],
[2480, 280], [2500, 280], [2520, 280],
[2700, 210], [2720, 210],
[2890, 150], [2910, 150], [2930, 150],
[3140, 280], [3160, 280],
[3450, 310], [3470, 310],
];
coinPositions.forEach(function(p) {
coins.push({ x: p[0], y: p[1], collected: false, bobPhase: Math.random() * Math.PI * 2 });
});
}
spawnCoins();
// Flag (goal)
const flag = { x: 3630, y: 230, w: 10, h: 110 };
// ─── Player ───
const player = {
x: 80,
y: 350,
w: 24,
h: 36,
vx: 0,
vy: 0,
grounded: false,
facing: 1, // 1 = right, -1 = left
animTimer: 0,
alive: true,
};
// ─── Camera ───
let camera = { x: 0 };
// ─── State ───
let score = 0;
let gameWon = false;
let winTimer = 0;
let deathTimer = 0;
let particles = [];
// ─── 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;
});
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 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;
}
// ─── 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() * 4,
});
}
}
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 drawParticles() {
particles.forEach(function(p) {
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;
}
// ─── Physics / Update ───
function update() {
if (gameWon) {
winTimer++;
if (winTimer > 120 && (isJump() || keys["Enter"])) {
resetGame();
}
return;
}
if (!player.alive) {
deathTimer++;
if (deathTimer > 60) resetGame();
player.vy += GRAVITY;
player.y += player.vy;
updateParticles();
return;
}
// Horizontal movement
if (isLeft()) {
player.vx = -MOVE_SPEED;
player.facing = -1;
} else if (isRight()) {
player.vx = MOVE_SPEED;
player.facing = 1;
} else {
player.vx = 0;
}
player.animTimer += Math.abs(player.vx) * 0.1;
// Apply horizontal movement
player.x += player.vx;
// Clamp to level bounds
if (player.x < 0) player.x = 0;
if (player.x + player.w > LEVEL_W) player.x = LEVEL_W - player.w;
// Horizontal collision with platforms
checkCollisionX();
// Jumping
if (isJump() && player.grounded) {
player.vy = JUMP_FORCE;
player.grounded = false;
spawnParticles(player.x + player.w / 2, player.y + player.h, "#aaa", 5);
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// Apply vertical movement
player.y += player.vy;
// Vertical collision
player.grounded = false;
checkCollisionY();
// Fall into pit
if (player.y > H + 50) {
player.alive = false;
player.vy = -8;
}
// Coin collection
coins.forEach(function(c) {
if (c.collected) return;
var bobY = c.y + Math.sin(c.bobPhase + (Date.now() * 0.003)) * 3;
var coinRect = { x: c.x - 8, y: bobY - 8, w: 16, h: 16 };
if (rectOverlap(player, coinRect)) {
c.collected = true;
score += 10;
spawnParticles(c.x, bobY, "#ffcc00", 8);
}
});
// Flag collision (win)
var flagRect = { x: flag.x - 5, y: flag.y, w: flag.w + 10, h: flag.h };
if (rectOverlap(player, flagRect)) {
gameWon = true;
winTimer = 0;
spawnParticles(player.x + player.w / 2, player.y, "#ffcc00", 20);
spawnParticles(player.x + player.w / 2, player.y, "#ff4444", 15);
spawnParticles(player.x + player.w / 2, player.y, "#44ff44", 15);
}
// Camera
var targetCam = player.x - W / 3;
if (targetCam < 0) targetCam = 0;
if (targetCam > LEVEL_W - W) targetCam = LEVEL_W - W;
camera.x += (targetCam - camera.x) * 0.12;
updateParticles();
}
function checkCollisionX() {
var allSolids = platforms.concat(pipes.map(function(p) { return { x: p.x, y: p.y, w: p.w, h: p.h }; }));
allSolids.forEach(function(s) {
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;
}
});
}
function checkCollisionY() {
var allSolids = platforms.concat(pipes.map(function(p) { return { x: p.x, y: p.y, w: p.w, h: p.h }; }));
allSolids.forEach(function(s) {
if (rectOverlap(player, s)) {
if (player.vy > 0) {
player.y = s.y - player.h;
player.vy = 0;
player.grounded = true;
} else if (player.vy < 0) {
player.y = s.y + s.h;
player.vy = 0;
}
}
});
}
function resetGame() {
player.x = 80;
player.y = 350;
player.vx = 0;
player.vy = 0;
player.alive = true;
player.grounded = false;
score = 0;
gameWon = false;
winTimer = 0;
deathTimer = 0;
particles = [];
coins.forEach(function(c) { c.collected = false; });
camera.x = 0;
}
// ─── Drawing ───
function drawSky() {
// Sky gradient
var grd = ctx.createLinearGradient(0, 0, 0, H);
grd.addColorStop(0, "#1a1a4e");
grd.addColorStop(0.4, "#2a4a7a");
grd.addColorStop(0.7, "#5a9ad5");
grd.addColorStop(1, "#87CEEB");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, W, H);
// Clouds
var cloudPositions = [
[100, 60, 80], [350, 40, 100], [700, 80, 60],
[1000, 50, 90], [1400, 70, 70], [1800, 40, 110],
[2200, 60, 80], [2600, 50, 100], [3000, 70, 70],
[3400, 45, 90],
];
cloudPositions.forEach(function(c) {
var cx = c[0] - (camera.x * 0.4); // Parallax
// Wrap around
if (cx < -150) cx += W + 300;
if (cx > W + 50) cx -= W + 300;
ctx.fillStyle = "rgba(255,255,255,0.7)";
drawCloud(cx, c[1], c[2]);
});
}
function drawCloud(x, y, size) {
ctx.beginPath();
ctx.arc(x, y, size * 0.4, 0, Math.PI * 2);
ctx.arc(x - size * 0.25, y + 5, size * 0.3, 0, Math.PI * 2);
ctx.arc(x + size * 0.25, y + 3, size * 0.35, 0, Math.PI * 2);
ctx.arc(x + size * 0.1, y - 8, size * 0.3, 0, Math.PI * 2);
ctx.fill();
}
function drawMountains() {
// Background mountains (parallax)
ctx.fillStyle = "#2d4a1a";
var mountainPositions = [
[200, 380, 200], [700, 360, 250], [1300, 370, 180],
[1900, 350, 280], [2500, 370, 200], [3200, 360, 240],
];
mountainPositions.forEach(function(m) {
var mx = m[0] - (camera.x * 0.2);
ctx.beginPath();
ctx.moveTo(mx - m[2] / 2, m[1]);
ctx.lineTo(mx, m[1] - m[2] * 0.6);
ctx.lineTo(mx + m[2] / 2, m[1]);
ctx.closePath();
ctx.fill();
});
}
function drawPlatform(p) {
var x = p.x - camera.x;
// Main block
ctx.fillStyle = p.color;
ctx.fillRect(x, p.y, p.w, p.h);
// Top edge highlight
ctx.fillStyle = "rgba(255,255,255,0.2)";
ctx.fillRect(x, p.y, p.w, 4);
// Bottom shadow
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(x, p.y + p.h - 4, p.w, 4);
// Grass on top for ground platforms
if (p.h >= 40) {
ctx.fillStyle = "#5cb840";
ctx.fillRect(x, p.y, p.w, 6);
// Grass blades
ctx.fillStyle = "#3a7d28";
for (var gx = x; gx < x + p.w; gx += 12) {
ctx.fillRect(gx, p.y - 3, 3, 6);
}
} else {
// Brick pattern on raised platforms
ctx.strokeStyle = "rgba(0,0,0,0.15)";
ctx.lineWidth = 1;
for (var bx = x; bx < x + p.w; bx += 20) {
ctx.beginPath();
ctx.moveTo(bx, p.y);
ctx.lineTo(bx, p.y + p.h);
ctx.stroke();
}
for (var by = p.y + 10; by < p.y + p.h; by += 10) {
ctx.beginPath();
ctx.moveTo(x, by);
ctx.lineTo(x + p.w, by);
ctx.stroke();
}
}
}
function drawPipe(p) {
var x = p.x - camera.x;
// Pipe body
ctx.fillStyle = "#1a8a1a";
ctx.fillRect(x + 3, p.y + 28, p.w - 6, p.h - 28);
// Pipe top
ctx.fillStyle = "#1a8a1a";
ctx.fillRect(x, p.y, p.w, 28);
// Pipe highlight
ctx.fillStyle = "#2aad2a";
ctx.fillRect(x + 5, p.y + 2, 8, 24);
ctx.fillRect(x + 8, p.y + 30, 6, p.h - 32);
// Pipe rim
ctx.fillStyle = "#0e6b0e";
ctx.fillRect(x, p.y, p.w, 3);
ctx.fillRect(x, p.y + 25, p.w, 3);
}
function drawQBlock(b) {
var x = b.x - camera.x;
// Block
ctx.fillStyle = "#e8a000";
ctx.fillRect(x, b.y, 28, 28);
ctx.strokeStyle = "#8a5e00";
ctx.lineWidth = 2;
ctx.strokeRect(x, b.y, 28, 28);
// Question mark
ctx.fillStyle = "#fff";
ctx.font = "bold 18px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", x + 14, b.y + 15);
}
function drawPlayer() {
var x = player.x - camera.x;
var y = player.y;
ctx.save();
if (player.facing === -1) {
ctx.translate(x + player.w / 2, 0);
ctx.scale(-1, 1);
ctx.translate(-(x + player.w / 2), 0);
}
// Shoes
ctx.fillStyle = "#6a2c00";
ctx.fillRect(x + 2, y + player.h - 8, 8, 8);
ctx.fillRect(x + player.w - 10, y + player.h - 8, 8, 8);
// Body (blue overalls)
ctx.fillStyle = "#2255cc";
ctx.fillRect(x + 3, y + 16, player.w - 6, 14);
// Belt
ctx.fillStyle = "#e8c800";
ctx.fillRect(x + 5, y + 26, player.w - 10, 3);
// Arms
ctx.fillStyle = "#ff9944";
if (!player.grounded) {
// Arms up when jumping
ctx.fillRect(x - 2, y + 10, 6, 4);
ctx.fillRect(x + player.w - 4, y + 10, 6, 4);
} else if (Math.abs(player.vx) > 0) {
// Running animation
var armPhase = Math.sin(player.animTimer) * 4;
ctx.fillRect(x - 2, y + 18 + armPhase, 6, 4);
ctx.fillRect(x + player.w - 4, y + 18 - armPhase, 6, 4);
} else {
ctx.fillRect(x - 2, y + 18, 6, 4);
ctx.fillRect(x + player.w - 4, y + 18, 6, 4);
}
// Legs
ctx.fillStyle = "#2255cc";
if (Math.abs(player.vx) > 0 && player.grounded) {
var legAnim = Math.sin(player.animTimer) * 3;
ctx.fillRect(x + 4, y + 28, 7, 8 + legAnim);
ctx.fillRect(x + player.w - 11, y + 28, 7, 8 - legAnim);
} else if (!player.grounded) {
ctx.fillRect(x + 4, y + 28, 7, 6);
ctx.fillRect(x + player.w - 11, y + 28, 7, 6);
} else {
ctx.fillRect(x + 4, y + 28, 7, 8);
ctx.fillRect(x + player.w - 11, y + 28, 7, 8);
}
// Head
ctx.fillStyle = "#ff9944";
ctx.fillRect(x + 4, y + 4, player.w - 8, 14);
// Hat
ctx.fillStyle = "#dd2222";
ctx.fillRect(x + 2, y - 2, player.w - 4, 8);
ctx.fillRect(x + player.w - 10, y, 12, 5); // brim
// Eyes
ctx.fillStyle = "#fff";
ctx.fillRect(x + player.w - 10, y + 8, 5, 5);
ctx.fillStyle = "#111";
ctx.fillRect(x + player.w - 8, y + 9, 3, 3);
ctx.restore();
}
function drawCoins() {
var now = Date.now() * 0.003;
coins.forEach(function(c) {
if (c.collected) return;
var x = c.x - camera.x;
var bobY = c.y + Math.sin(c.bobPhase + now) * 3;
// Glow
ctx.fillStyle = "rgba(255,204,0,0.15)";
ctx.beginPath();
ctx.arc(x, bobY, 12, 0, Math.PI * 2);
ctx.fill();
// Coin body
var stretch = Math.abs(Math.cos(c.bobPhase + now));
var cw = 10 * (0.5 + stretch * 0.5);
ctx.fillStyle = "#ffcc00";
ctx.beginPath();
ctx.ellipse(x, bobY, cw, 10, 0, 0, Math.PI * 2);
ctx.fill();
// Highlight
ctx.fillStyle = "#ffe866";
ctx.beginPath();
ctx.ellipse(x - 2, bobY - 2, cw * 0.4, 5, 0, 0, Math.PI * 2);
ctx.fill();
// $ sign
if (cw > 4) {
ctx.fillStyle = "#aa7700";
ctx.font = "bold 10px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", x, bobY + 1);
}
});
}
function drawFlag() {
var x = flag.x - camera.x;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(x, flag.y, flag.w, flag.h);
// Pole top ball
ctx.fillStyle = "#ffcc00";
ctx.beginPath();
ctx.arc(x + flag.w / 2, flag.y, 6, 0, Math.PI * 2);
ctx.fill();
// Flag (wavy)
ctx.fillStyle = "#ff3333";
ctx.beginPath();
ctx.moveTo(x + flag.w, flag.y + 5);
var now = Date.now() * 0.004;
ctx.quadraticCurveTo(x + flag.w + 30, flag.y + 10 + Math.sin(now) * 4, x + flag.w + 50, flag.y + 15);
ctx.quadraticCurveTo(x + flag.w + 30, flag.y + 25 + Math.sin(now + 1) * 4, x + flag.w, flag.y + 35);
ctx.closePath();
ctx.fill();
// Flag star
ctx.fillStyle = "#ffcc00";
ctx.font = "14px Courier New";
ctx.textAlign = "center";
ctx.fillText("★", x + flag.w + 22, flag.y + 23);
}
function drawHUD() {
// Score background
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(10, 10, 180, 36);
ctx.strokeStyle = "#ffcc00";
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, 180, 36);
// Score text
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 18px Courier New";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
var collected = coins.filter(function(c) { return c.collected; }).length;
ctx.fillText("SCORE: " + score, 22, 29);
// Coin counter
ctx.fillStyle = "#fff";
ctx.font = "14px Courier New";
ctx.fillText("Coins: " + collected + "/" + coins.filter(function(c){return true}).length, 110, 48);
// Direction arrow to flag if off screen
var flagScreenX = flag.x - camera.x;
if (flagScreenX > W) {
ctx.fillStyle = "#ff4444";
ctx.font = "20px Courier New";
ctx.textAlign = "center";
ctx.fillText("→", W - 30, H - 30);
}
}
function drawWinScreen() {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ffcc00";
ctx.font = "bold 48px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("YOU WIN!", W / 2, H / 2 - 40);
ctx.fillStyle = "#fff";
ctx.font = "24px Courier New";
ctx.fillText("Score: " + score, W / 2, H / 2 + 20);
ctx.fillText("Coins: " + coins.filter(function(c) { return c.collected; }).length + "/" + coins.length, W / 2, H / 2 + 55);
if (winTimer > 120) {
ctx.fillStyle = "#888";
ctx.font = "16px Courier New";
ctx.fillText("Press Space or Enter to play again", W / 2, H / 2 + 100);
}
}
function drawDeath() {
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ff4444";
ctx.font = "bold 32px Courier New";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("OOPS!", W / 2, H / 2);
}
// ─── Main Loop ───
function gameLoop() {
update();
// Clear
ctx.clearRect(0, 0, W, H);
// Draw world
drawSky();
drawMountains();
// Platforms
platforms.forEach(drawPlatform);
// Decorative blocks
qBlocks.forEach(drawQBlock);
// Pipes
pipes.forEach(drawPipe);
// Coins
drawCoins();
// Flag
drawFlag();
// Player
if (player.alive || deathTimer < 60) {
drawPlayer();
}
// Particles
drawParticles();
// HUD
drawHUD();
// Overlays
if (gameWon) {
drawWinScreen();
}
if (!player.alive) {
drawDeath();
}
requestAnimationFrame(gameLoop);
}
// ─── Start ───
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 Mini Mario</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #111;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: monospace;
overflow: hidden;
}
#game {
border: 2px solid #333;
image-rendering: pixelated;
display: block;
}
#instructions {
color: #777;
margin-top: 10px;
font-size: 14px;
text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="instructions">Arrow keys / WASD to move · Space / Up to jump · Collect coins and reach the flag!</div>
<script>
(function() {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width; // 800
const H = canvas.height; // 450
// --- Constants ---
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.7;
const MAX_SPEED = 4.5;
const JUMP_FORCE = -10.5;
const TILE = 32;
// --- 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;
});
function isLeft() { return keys["ArrowLeft"] || keys["KeyA"]; }
function isRight() { return keys["ArrowRight"] || keys["KeyD"]; }
function isJump() { return keys["Space"] || keys["ArrowUp"] || keys["KeyW"]; }
// --- Game state ---
let score = 0;
let gameState = "playing"; // "playing" | "won" | "dead"
let deathTimer = 0;
let winTimer = 0;
let particles = [];
let camera = { x: 0 };
// --- World definition ---
// Map: each tile = 32px. '.' = air, '#' = ground/brick, '=' = raiseable platform,
// 'P' = pipe, '!' = coin, 'F' = flag, 'S' = spawn, '^' = spike
const WORLD_W = 120; // tiles
const WORLD_H = Math.ceil(H / TILE); // ~14
// Build world tile array
var world = [];
for (var r = 0; r < WORLD_H; r++) {
world[r] = [];
for (var c = 0; c < WORLD_W; c++) {
world[r][c] = '.';
}
}
// Ground rows (rows 12 and 13)
// Put gaps at columns 28-30 and 65-67
for (var c = 0; c < WORLD_W; c++) {
if ((c >= 28 && c <= 30) || (c >= 65 && c <= 67)) continue; // pits
world[13][c] = '#';
world[12][c] = '#';
}
function placePlatform(col, row, len) {
for (var i = 0; i < len; i++) {
if (col + i < WORLD_W && row >= 0 && row < WORLD_H) {
world[row][col + i] = '=';
}
}
}
function placeBricks(col, row, len) {
for (var i = 0; i < len; i++) {
if (col + i < WORLD_W && row >= 0 && row < WORLD_H) {
world[row][col + i] = '#';
}
}
}
// Platforms
placePlatform(8, 9, 4);
placePlatform(16, 7, 5);
placePlatform(24, 5, 3);
placePlatform(32, 8, 4); // bridge over pit partially
placePlatform(31, 10, 2); // stepping stone over pit
placePlatform(35, 9, 5);
placePlatform(42, 6, 6);
placePlatform(50, 8, 4);
placePlatform(55, 5, 3);
placePlatform(60, 9, 6);
placePlatform(69, 9, 5); // after second pit
placePlatform(75, 7, 4);
placePlatform(80, 5, 5);
placePlatform(88, 8, 4);
placePlatform(93, 6, 4);
placePlatform(98, 9, 5);
// Staircase before flag
for (var s = 0; s < 6; s++) {
for (var ss = 0; ss <= s; ss++) {
var cr = 12 - ss;
var cc = 105 + s;
if (cr < 12 && cc < WORLD_W) world[cr][cc] = '#';
}
}
// Pipes
function placePipe(col, topRow) {
for (var r = topRow; r <= 12; r++) {
world[r][col] = 'P';
if (col + 1 < WORLD_W) world[r][col + 1] = 'P';
}
}
placePipe(14, 10);
placePipe(46, 11);
placePipe(84, 9);
// Spikes on ground
world[12][44] = '^';
world[12][45] = '^';
world[12][86] = '^';
// Coins
var coins = [];
function placeCoin(col, row) {
coins.push({ x: col * TILE + TILE/2, y: row * TILE + TILE/2, collected: false });
}
// Coins over platforms
placeCoin(9, 8); placeCoin(10, 8);
placeCoin(17, 6); placeCoin(18, 6);
placeCoin(24, 4); placeCoin(25, 4);
placeCoin(7, 11); placeCoin(8, 10);
placeCoin(33, 7); placeCoin(34, 7);
placeCoin(36, 8); placeCoin(37, 8);
placeCoin(43, 5); placeCoin(44, 5); placeCoin(45, 5);
placeCoin(51, 7);
placeCoin(55, 4); placeCoin(56, 4);
placeCoin(61, 8); placeCoin(62, 8);
placeCoin(70, 8); placeCoin(71, 8);
placeCoin(76, 6); placeCoin(77, 6);
placeCoin(81, 4); placeCoin(82, 4); placeCoin(83, 4);
placeCoin(89, 7); placeCoin(90, 7);
placeCoin(94, 5); placeCoin(95, 5);
placeCoin(99, 8); placeCoin(100, 8);
// Some coins in the air for extra challenge
placeCoin(12, 11); placeCoin(22, 9); placeCoin(68, 11);
// Flag position
var flag = { x: 111 * TILE, baseY: 12 * TILE, topY: 3 * TILE };
// --- Player ---
var player = {
x: 3 * TILE,
y: 11 * TILE,
w: 20,
h: 28,
vx: 0,
vy: 0,
onGround: false,
facing: 1,
frame: 0,
frameTimer: 0
};
function resetPlayer() {
player.x = 3 * TILE;
player.y = 11 * TILE;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.facing = 1;
camera.x = 0;
}
function resetGame() {
score = 0;
gameState = "playing";
coins.forEach(function(c) { c.collected = false; });
particles = [];
resetPlayer();
}
// --- Tile helpers ---
function tileAt(col, row) {
if (col < 0 || col >= WORLD_W || row < 0 || row >= WORLD_H) return '.';
return world[row][col];
}
function isSolid(t) { return t === '#' || t === '=' || t === 'P'; }
function playerTileCol() { return Math.floor((player.x + player.w/2) / TILE); }
function playerTileRow() { return Math.floor((player.y + player.h) / TILE); }
// --- Collision ---
function collide() {
player.onGround = false;
// Horizontal
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 r = top; r <= bottom; r++) {
for (var c = left; c <= right; c++) {
if (isSolid(tileAt(c, r))) {
if (player.vx > 0) {
player.x = c * TILE - player.w;
player.vx = 0;
} else if (player.vx < 0) {
player.x = (c + 1) * TILE;
player.vx = 0;
}
}
}
}
// Vertical
player.y += player.vy;
player.vx *= 1; // keep horizontal position
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 r = top; r <= bottom; r++) {
for (var c = left; c <= right; c++) {
var t = tileAt(c, r);
if (isSolid(t)) {
if (player.vy > 0) {
player.y = r * TILE - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = (r + 1) * TILE;
player.vy = 0;
}
}
// Spike check
if (t === '^') {
die();
return;
}
}
}
// Keep in bounds horizontally
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x + player.w > WORLD_W * TILE) { player.x = WORLD_W * TILE - player.w; player.vx = 0; }
}
function die() {
if (gameState !== "playing") return;
gameState = "dead";
deathTimer = 90;
// spawn death particles
for (var i = 0; i < 12; i++) {
particles.push({
x: player.x + player.w/2,
y: player.y + player.h/2,
vx: (Math.random() - 0.5) * 6,
vy: -Math.random() * 8 - 2,
life: 40 + Math.random() * 20,
color: "#e44",
size: 3 + Math.random() * 3
});
}
}
function win() {
if (gameState !== "playing") return;
gameState = "won";
winTimer = 0;
for (var i = 0; i < 30; i++) {
var hue = Math.random() * 360;
particles.push({
x: flag.x + TILE/2,
y: flag.topY + TILE/2,
vx: (Math.random() - 0.5) * 5,
vy: -Math.random() * 6 - 1,
life: 50 + Math.random() * 40,
color: "hsl(" + hue + ",90%,60%)",
size: 3 + Math.random() * 4
});
}
}
// --- Coin collection ---
function checkCoins() {
for (var i = 0; i < coins.length; i++) {
var c = coins[i];
if (c.collected) continue;
var dx = (player.x + player.w/2) - c.x;
var dy = (player.y + player.h/2) - c.y;
if (dx * dx + dy * dy < 400) { // ~20px radius
c.collected = true;
score += 10;
// Sparkle particles
for (var j = 0; j < 6; j++) {
particles.push({
x: c.x, y: c.y,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 3 - 1,
life: 15 + Math.random() * 10,
color: "#fd4",
size: 2 + Math.random() * 2
});
}
}
}
}
function checkFlag() {
if (player.x + player.w > flag.x && player.x < flag.x + TILE) {
win();
}
}
// --- Update ---
function update() {
// Update particles always
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);
}
if (gameState === "dead") {
deathTimer--;
if (deathTimer <= 0) {
resetGame();
}
return;
}
if (gameState === "won") {
winTimer++;
return;
}
// Movement
if (isLeft()) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
}
if (isRight()) {
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;
// Jump
if (isJump() && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
// Apply gravity
player.vy += GRAVITY;
// Friction on ground
if (player.onGround) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
} else {
player.vx *= 0.96; // slight air friction
}
collide();
checkCoins();
checkFlag();
// Animation
if (Math.abs(player.vx) > 0.5) {
player.frameTimer++;
if (player.frameTimer > 6) {
player.frameTimer = 0;
player.frame = (player.frame + 1) % 4;
}
} else {
player.frame = 0;
}
// Fall into abyss
if (player.y > WORLD_H * TILE + 64) {
die();
}
// Camera
var targetCam = player.x - W * 0.38;
if (targetCam < 0) targetCam = 0;
if (targetCam > WORLD_W * TILE - W) targetCam = WORLD_W * TILE - W;
camera.x += (targetCam - camera.x) * 0.1;
}
// --- Drawing helpers ---
function drawBg() {
// Sky gradient
var grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#5c94fc");
grad.addColorStop(0.7, "#87CEEB");
grad.addColorStop(1, "#b8e4ff");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255,255,255,0.8)";
var cloudPositions = [
{x: 200, y: 60, w: 80}, {x: 600, y: 40, w: 100},
{x: 1100, y: 80, w: 70}, {x: 1500, y: 50, w: 90},
{x: 2000, y: 70, w: 80}, {x: 2500, y: 55, w: 100},
{x: 3000, y: 45, w: 75}, {x: 3500, y: 65, w: 85},
{x: 4000, y: 50, w: 95}
];
for (var i = 0; i < cloudPositions.length; i++) {
var cl = cloudPositions[i];
var cx = cl.x - camera.x * 0.3;
var cy = cl.y;
drawCloud(cx, cy, cl.w);
}
// Distant hills (parallax)
ctx.fillStyle = "rgba(60,140,60,0.35)";
var hillPositions = [300, 800, 1400, 2100, 2800, 3400, 4100];
for (var i = 0; i < hillPositions.length; i++) {
var hx = hillPositions[i] - camera.x * 0.15;
drawHill(hx, H - 60, 120 + (i % 3) * 40, 80 + (i % 2) * 30);
}
}
function drawCloud(x, y, w) {
var h = w * 0.45;
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.45, 0, Math.PI * 2);
ctx.arc(x + w * 0.35, y + h * 0.1, h * 0.35, 0, Math.PI * 2);
ctx.fill();
}
function drawHill(x, baseY, w, h) {
ctx.beginPath();
ctx.moveTo(x - w, baseY);
ctx.quadraticCurveTo(x, baseY - h, x + w, baseY);
ctx.fill();
}
function drawTile(c, r, screenX, screenY) {
var t = world[r][c];
if (t === '.') return;
if (t === '#') {
// Ground / brick
var isTop = !isSolid(tileAt(c, r - 1));
if (isTop) {
// Top surface - grass
ctx.fillStyle = "#6ab84a";
ctx.fillRect(screenX, screenY, TILE, 6);
ctx.fillStyle = "#e8a830";
ctx.fillRect(screenX, screenY + 6, TILE, TILE - 6);
// Brick pattern
ctx.strokeStyle = "#c48420";
ctx.lineWidth = 1;
ctx.strokeRect(screenX + 1, screenY + 8, TILE - 2, TILE - 10);
ctx.beginPath();
ctx.moveTo(screenX + TILE/2, screenY + 8);
ctx.lineTo(screenX + TILE/2, screenY + TILE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(screenX, screenY + 18);
ctx.lineTo(screenX + TILE * 0.5, screenY + 18);
ctx.stroke();
} else {
// Underground or side surface
ctx.fillStyle = "#c48420";
ctx.fillRect(screenX, screenY, TILE, TILE);
ctx.strokeStyle = "#a06818";
ctx.lineWidth = 1;
ctx.strokeRect(screenX + 1, screenY + 1, TILE - 2, TILE - 2);
ctx.beginPath();
ctx.moveTo(screenX + TILE/2, screenY + 1);
ctx.lineTo(screenX + TILE/2, screenY + TILE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(screenX, screenY + TILE/2);
ctx.lineTo(screenX + TILE, screenY + TILE/2);
ctx.stroke();
}
} else if (t === '=') {
// Raised platform (mushroom block style)
ctx.fillStyle = "#d4922a";
ctx.fillRect(screenX, screenY, TILE, TILE);
ctx.fillStyle = "#e4a830";
ctx.fillRect(screenX + 2, screenY + 2, TILE - 4, TILE - 4);
ctx.fillStyle = "#c48020";
// question mark
ctx.font = "bold 18px monospace";
ctx.textAlign = "center";
ctx.fillText("?", screenX + TILE/2, screenY + TILE/2 + 6);
ctx.strokeStyle = "#a06010";
ctx.lineWidth = 1;
ctx.strokeRect(screenX, screenY, TILE, TILE);
} else if (t === 'P') {
// Pipe
var sx = screenX;
var sy = screenY;
// Pipe body
ctx.fillStyle = "#2a8a2a";
ctx.fillRect(sx + 2, sy, TILE - 2, TILE);
ctx.fillStyle = "#3cb83c";
ctx.fillRect(sx + 4, sy, TILE - 10, TILE);
// Highlight
ctx.fillStyle = "#5cd85c";
ctx.fillRect(sx + 6, sy, 3, TILE);
// Top rim
if (!isSolid(tileAt(c, r - 1)) || r === 0) {
ctx.fillStyle = "#2a8a2a";
ctx.fillRect(sx - 3, sy, TILE + 4, 10);
ctx.fillStyle = "#3cb83c";
ctx.fillRect(sx - 1, sy + 2, TILE, 6);
ctx.fillStyle = "#5cd85c";
ctx.fillRect(sx + 2, sy + 2, 3, 6);
}
} else if (t === '^') {
// Spike
ctx.fillStyle = "#c44";
ctx.beginPath();
ctx.moveTo(screenX + 4, screenY + TILE);
ctx.lineTo(screenX + TILE/2, screenY + 4);
ctx.lineTo(screenX + TILE - 4, screenY + TILE);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#e66";
ctx.beginPath();
ctx.moveTo(screenX + 10, screenY + TILE);
ctx.lineTo(screenX + TILE/2, screenY + 10);
ctx.lineTo(screenX + TILE - 10, screenY + TILE);
ctx.closePath();
ctx.fill();
}
}
function drawCoin(x, y) {
var t = Date.now() / 300;
var scaleX = Math.abs(Math.cos(t));
ctx.save();
ctx.translate(x, y);
ctx.scale(scaleX, 1);
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(0, 0, 9, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#c4a000";
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffe44d";
ctx.beginPath();
ctx.arc(-1, -1, 4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawFlag() {
var fx = flag.x - camera.x;
var fy = flag.topY;
var baseY = flag.baseY;
// Pole
ctx.fillStyle = "#aaa";
ctx.fillRect(fx + 14, fy, 4, baseY - fy);
// Ball on top
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.arc(fx + 16, fy, 5, 0, Math.PI * 2);
ctx.fill();
// Flag (fluttering)
var flutter = Math.sin(Date.now() / 200) * 3;
ctx.fillStyle = "#e44";
ctx.beginPath();
ctx.moveTo(fx + 18, fy + 8);
ctx.lineTo(fx + 48 + flutter, fy + 18);
ctx.lineTo(fx + 18, fy + 38);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#ff4";
ctx.font = "14px monospace";
ctx.textAlign = "center";
ctx.fillText("★", fx + 32 + flutter/2, fy + 28);
// Base
ctx.fillStyle = "#888";
ctx.fillRect(fx + 6, baseY - 4, TILE - 4, TILE);
ctx.fillStyle = "#666";
ctx.fillRect(fx + 8, baseY - 2, TILE - 8, TILE - 2);
}
function drawPlayer() {
if (gameState === "dead") return;
var px = player.x - camera.x;
var py = player.y;
var f = player.facing;
ctx.save();
ctx.translate(px + player.w/2, py + player.h/2);
if (f === -1) ctx.scale(-1, 1);
// Body
ctx.fillStyle = "#e02020";
ctx.fillRect(-8, -6, 16, 14);
// Head
ctx.fillStyle = "#fca044";
ctx.fillRect(-7, -14, 14, 10);
// Hat
ctx.fillStyle = "#e02020";
ctx.fillRect(-8, -16, 16, 5);
ctx.fillRect(-4, -18, 12, 3);
// Hat brim
ctx.fillStyle = "#c01818";
ctx.fillRect(-9, -13, 18, 2);
// Eye
ctx.fillStyle = "#111";
ctx.fillRect(1, -12, 3, 3);
// Mustache
ctx.fillStyle = "#8b4513";
ctx.fillRect(-1, -8, 8, 2);
// Legs (animated when walking)
ctx.fillStyle = "#4440cc";
var legOffset = 0;
if (!player.onGround) {
// Jump pose
ctx.fillRect(-7, 8, 6, 6);
ctx.fillRect(2, 5, 6, 6);
} else if (Math.abs(player.vx) > 0.5) {
legOffset = Math.sin(player.frame * Math.PI * 0.5) * 3;
}
if (!(!player.onGround)) {
ctx.fillRect(-7 + legOffset, 7, 6, 7);
ctx.fillRect(1 - legOffset, 7, 6, 7);
}
// Shoes
ctx.fillStyle = "#a0522d";
if (!player.onGround) {
ctx.fillRect(-8, 12, 7, 4);
ctx.fillRect(2, 9, 7, 4);
} else {
ctx.fillRect(-7 + legOffset, 13, 7, 3);
ctx.fillRect(1 - legOffset, 13, 7, 3);
}
// Belt
ctx.fillStyle = "#222";
ctx.fillRect(-8, 6, 16, 2);
ctx.restore();
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
ctx.globalAlpha = Math.min(1, p.life / 15);
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)";
ctx.fillRect(8, 8, 180, 36);
ctx.strokeStyle = "#ffd700";
ctx.lineWidth = 2;
ctx.strokeRect(8, 8, 180, 36);
// Coin icon
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(30, 26, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#c4a000";
ctx.beginPath();
ctx.arc(30, 26, 6, 0, Math.PI * 2);
ctx.fill();
// Score text
ctx.fillStyle = "#fff";
ctx.font = "bold 20px monospace";
ctx.textAlign = "left";
ctx.fillText("x " + score, 46, 32);
// Remaining coins
var remaining = 0;
for (var i = 0; i < coins.length; i++) {
if (!coins[i].collected) remaining++;
}
var totalCoins = coins.length;
ctx.fillStyle = "rgba(0,0,0,0.5)";
var text = "Coins: " + (totalCoins - remaining) + "/" + totalCoins;
var textW = ctx.measureText(text).width + 16;
ctx.fillRect(W - textW - 8, 8, textW, 36);
ctx.fillStyle = "#ffd700";
ctx.fillText(text, W - textW, 32);
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#e44";
ctx.font = "bold 48px monospace";
ctx.textAlign = "center";
ctx.fillText("OOF!", W/2, H/2 - 20);
ctx.fillStyle = "#fff";
ctx.font = "20px monospace";
ctx.fillText("Respawning...", W/2, H/2 + 25);
}
function drawWinScreen() {
ctx.fillStyle = "rgba(0,0,0,0.45)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#ffd700";
ctx.font = "bold 52px monospace";
ctx.textAlign = "center";
ctx.fillText("🏆 YOU WIN! 🏆", W/2, H/2 - 30);
ctx.fillStyle = "#fff";
ctx.font = "22px monospace";
ctx.fillText("Score: " + score, W/2, H/2 + 15);
ctx.font = "18px monospace";
ctx.fillStyle = "#aaa";
var restartHint = winTimer > 90 ? "Press R or Space to play again" : "";
ctx.fillText(restartHint, W/2, H/2 + 50);
if (winTimer > 90 && (keys["KeyR"] || keys["Space"])) {
keys["KeyR"] = false;
keys["Space"] = false;
resetGame();
}
}
// --- Draw world (optimized: only visible tiles) ---
function drawWorld() {
var startCol = Math.max(0, Math.floor(camera.x / TILE) - 1);
var endCol = Math.min(WORLD_W, Math.ceil((camera.x + W) / TILE) + 1);
for (var r = 0; r < WORLD_H; r++) {
for (var c = startCol; c < endCol; c++) {
var t = world[r][c];
if (t === '.') continue;
var sx = c * TILE - camera.x;
var sy = r * TILE;
drawTile(c, r, sx, sy);
}
}
}
function drawCoins() {
for (var i = 0; i < coins.length; i++) {
var c = coins[i];
if (c.collected) continue;
var cx = c.x - camera.x;
if (cx < -20 || cx > W + 20) continue;
drawCoin(cx, c.y);
}
}
// --- Main render ---
function draw() {
drawBg();
drawWorld();
drawFlag();
drawCoins();
drawPlayer();
drawParticles();
drawHUD();
if (gameState === "dead") drawDeathScreen();
if (gameState === "won") drawWinScreen();
}
// --- Game loop ---
var lastTime = 0;
var fixedDt = 1000 / 60;
var accumulator = 0;
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
var dt = timestamp - lastTime;
lastTime = timestamp;
if (dt > 100) dt = 100; // prevent huge jumps
accumulator += dt;
while (accumulator >= fixedDt) {
update();
accumulator -= fixedDt;
}
draw();
requestAnimationFrame(gameLoop);
}
// --- Start ---
resetPlayer();
requestAnimationFrame(gameLoop);
})();
</script>
</body>
</html>