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 Pixel Adventure — a compact Mario-style platformer
Plain vanilla JS, no external dependencies.
============================================================ */
(function () {
'use strict';
// ---- canvas & context ------------------------------------------------
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 JUMP_FORCE = -11;
const MOVE_SPEED = 4.2;
const FRICTION = 0.82;
const MAX_FALL = 12;
const TILE = 32; // base tile size
const PLAYER_W = 24;
const PLAYER_H = 32;
// ---- input tracking --------------------------------------------------
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
// Prevent page scrolling for game keys
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
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']; }
// ---- camera ------------------------------------------------------------
const camera = { x: 0 };
// ---- level data --------------------------------------------------------
// ground segments: {x, y, w} (in world coords)
const groundSegments = [
{ x: 0, y: H - TILE, w: 1200 },
{ x: 1320, y: H - TILE, w: 800 },
{ x: 2240, y: H - TILE, w: 400 },
{ x: 2720, y: H - TILE, w: 600 },
{ x: 3400, y: H - TILE, w: 1200 },
{ x: 4700, y: H - TILE, w: 1500 },
];
// platforms: { x, y, w, type } type: 'normal'|'brick'|'question'
const platforms = [
{ x: 400, y: 320, w: 96, type: 'normal' },
{ x: 560, y: 256, w: 64, type: 'normal' },
{ x: 700, y: 192, w: 128, type: 'brick' },
{ x: 960, y: 288, w: 96, type: 'question' },
{ x: 1080, y: 224, w: 64, type: 'normal' },
{ x: 1500, y: 300, w: 96, type: 'normal' },
{ x: 1640, y: 240, w: 128, type: 'brick' },
{ x: 1800, y: 180, w: 64, type: 'normal' },
{ x: 2000, y: 260, w: 96, type: 'question' },
{ x: 2300, y: 320, w: 128, type: 'normal' },
{ x: 2500, y: 240, w: 96, type: 'normal' },
{ x: 2680, y: 180, w: 64, type: 'normal' },
{ x: 3000, y: 280, w: 96, type: 'brick' },
{ x: 3160, y: 220, w: 64, type: 'normal' },
{ x: 3320, y: 280, w: 96, type: 'question' },
{ x: 3600, y: 300, w: 128, type: 'normal' },
{ x: 3800, y: 240, w: 64, type: 'normal' },
{ x: 3960, y: 180, w: 128, type: 'brick' },
{ x: 4200, y: 280, w: 96, type: 'normal' },
{ x: 4400, y: 220, w: 64, type: 'normal' },
{ x: 4600, y: 300, w: 96, type: 'question' },
{ x: 4900, y: 260, w: 128, type: 'normal' },
{ x: 5100, y: 200, w: 96, type: 'normal' },
];
// coins
const coins = [];
const coinPositions = [
// near start
420, 460, 500,
580, 620,
720, 760, 800,
980, 1000,
1100,
// mid section
1520, 1560, 1600,
1660, 1700, 1740,
1820,
2020, 2040,
// section 3
2320, 2360, 2400,
2520, 2560,
2700,
// section 4
3020, 3060,
3180,
3340, 3360, 3380,
// final stretch
3620, 3660, 3700, 3740,
3820, 3860,
3980, 4020, 4060, 4100,
4220, 4260,
4420,
4620, 4640,
// climb to flag
4920, 4960,
5120, 5160,
];
// World y position for each coin (above various platforms)
const coinYs = {};
function initCoins() {
coinPositions.forEach((cx, i) => {
// place coins at various heights; check if near a platform
let cy = 300; // default height
for (const p of platforms) {
if (cx >= p.x - 16 && cx <= p.x + p.w + 16) {
cy = p.y - 28;
break;
}
}
// some coins in the air
if (i % 5 === 2) cy -= 40;
if (i % 7 === 4) cy -= 60;
coins.push({ x: cx, y: cy, collected: false, anim: Math.random() * Math.PI * 2 });
});
}
// enemies (simple goombas that patrol)
const enemies = [];
function initEnemies() {
const enemyData = [
{ x: 600, patrol: 120 },
{ x: 1100, patrol: 100 },
{ x: 1560, patrol: 140 },
{ x: 2050, patrol: 100 },
{ x: 2400, patrol: 120 },
{ x: 3100, patrol: 140 },
{ x: 3700, patrol: 160 },
{ x: 4300, patrol: 100 },
{ x: 5000, patrol: 120 },
];
for (const d of enemyData) {
// find ground height at this x
let groundY = H - TILE;
for (const g of groundSegments) {
if (d.x >= g.x && d.x <= g.x + g.w) {
groundY = g.y;
break;
}
}
// check platform
for (const p of platforms) {
if (d.x >= p.x && d.x <= p.x + p.w && groundY > p.y + TILE) {
groundY = p.y;
}
}
enemies.push({
x: d.x, y: groundY - 28,
w: 28, h: 28,
vx: 1.2,
patrolLeft: d.x - d.patrol,
patrolRight: d.x + d.patrol,
alive: true,
squishTimer: 0,
});
}
}
// flag / goal
const flag = { x: 5600, y: H - TILE - 160, reached: false };
// particles
const particles = [];
// background elements (clouds, hills, bushes)
const clouds = [];
const hills = [];
const bushes = [];
function initBG() {
for (let i = 0; i < 30; i++) {
clouds.push({
x: i * 300 + Math.random() * 200,
y: 30 + Math.random() * 80,
w: 60 + Math.random() * 80,
h: 20 + Math.random() * 16,
});
}
for (let i = 0; i < 20; i++) {
hills.push({
x: i * 500 + Math.random() * 200,
w: 150 + Math.random() * 200,
h: 60 + Math.random() * 80,
});
}
for (let i = 0; i < 25; i++) {
bushes.push({
x: i * 350 + Math.random() * 200,
y: H - TILE,
w: 40 + Math.random() * 40,
h: 16 + Math.random() * 12,
});
}
}
// ---- player -------------------------------------------------------------
let player = {};
function resetPlayer() {
player = {
x: 80, y: H - TILE - PLAYER_H - 2,
w: PLAYER_W, h: PLAYER_H,
vx: 0, vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
frame: 0,
frameTimer: 0,
jumpPressed: false,
dead: false,
};
}
// ---- game state ---------------------------------------------------------
let score = 0;
let totalCoins = coins.length;
let gameState = 'playing'; // 'playing' | 'won' | 'dead'
let deathTimer = 0;
let winTimer = 0;
// ---- helpers ------------------------------------------------------------
function aabb(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function addParticle(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 5,
vy: -Math.random() * 6 - 2,
life: 30 + Math.random() * 20,
color,
size: 2 + Math.random() * 3,
});
}
}
// ---- init ----------------------------------------------------------------
function init() {
initCoins();
initEnemies();
initBG();
resetPlayer();
score = 0;
gameState = 'playing';
particles.length = 0;
}
// ---- collision resolution -----------------------------------------------
function collideX() {
const p = player;
// ground
for (const g of groundSegments) {
if (aabb(p.x, p.y, p.w, p.h, g.x, g.y, g.w, TILE)) {
if (p.vx > 0) { p.x = g.x - p.w; }
else if (p.vx < 0) { p.x = g.x + g.w; }
p.vx = 0;
}
}
// platforms
for (const pl of platforms) {
if (aabb(p.x, p.y, p.w, p.h, pl.x, pl.y, pl.w, TILE)) {
if (p.vx > 0) { p.x = pl.x - p.w; }
else if (p.vx < 0) { p.x = pl.x + pl.w; }
p.vx = 0;
}
}
}
function collideY() {
const p = player;
p.onGround = false;
// ground
for (const g of groundSegments) {
if (aabb(p.x, p.y, p.w, p.h, g.x, g.y, g.w, TILE)) {
if (p.vy > 0) {
p.y = g.y - p.h;
p.vy = 0;
p.onGround = true;
} else if (p.vy < 0) {
p.y = g.y + TILE;
p.vy = 0;
}
}
}
// platforms
for (const pl of platforms) {
if (aabb(p.x, p.y, p.w, p.h, pl.x, pl.y, pl.w, TILE)) {
if (p.vy > 0) {
p.y = pl.y - p.h;
p.vy = 0;
p.onGround = true;
} else if (p.vy < 0) {
p.y = pl.y + TILE;
p.vy = 0;
}
}
}
}
// ---- update ---------------------------------------------------------------
function update() {
if (gameState === 'dead') {
deathTimer--;
if (deathTimer <= 0) init();
return;
}
if (gameState === 'won') {
winTimer--;
return;
}
const p = player;
// movement
if (isLeft()) { p.vx -= 1.2; p.facing = -1; }
if (isRight()) { p.vx += 1.2; p.facing = 1; }
// limit horizontal speed
if (p.vx > MOVE_SPEED) p.vx = MOVE_SPEED;
if (p.vx < -MOVE_SPEED) p.vx = -MOVE_SPEED;
// friction when no input
if (!isLeft() && !isRight()) {
p.vx *= FRICTION;
if (Math.abs(p.vx) < 0.1) p.vx = 0;
}
// jump
if (isJump() && p.onGround && !p.jumpPressed) {
p.vy = JUMP_FORCE;
p.onGround = false;
p.jumpPressed = true;
}
if (!isJump()) p.jumpPressed = false;
// gravity
p.vy += GRAVITY;
if (p.vy > MAX_FALL) p.vy = MAX_FALL;
// move
p.x += p.vx;
p.y += p.vy;
// collisions
collideX();
collideY();
// animation frame
p.frameTimer++;
if (Math.abs(p.vx) > 0.5) {
if (p.frameTimer % 8 === 0) p.frame = (p.frame + 1) % 4;
} else {
p.frame = 0;
}
// camera follow
const targetCam = p.x - W / 3;
camera.x += (targetCam - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
// collect coins
for (const c of coins) {
if (c.collected) continue;
c.anim += 0.08;
if (aabb(p.x, p.y, p.w, p.h, c.x - 10, c.y - 10, 20, 20)) {
c.collected = true;
score += 100;
addParticle(c.x, c.y, '#FFD700', 8);
}
}
// enemy update & collision
for (const e of enemies) {
if (!e.alive) {
e.squishTimer--;
continue;
}
e.x += e.vx;
if (e.x <= e.patrolLeft || e.x >= e.patrolRight) {
e.vx *= -1;
}
// collision with player
if (aabb(p.x, p.y, p.w, p.h, e.x, e.y, e.w, e.h)) {
// stomp from above
if (p.vy > 0 && p.y + p.h - 6 < e.y + e.h / 2) {
e.alive = false;
e.squishTimer = 30;
p.vy = JUMP_FORCE * 0.6;
score += 200;
addParticle(e.x + e.w / 2, e.y + e.h / 2, '#AA4400', 10);
} else {
// player dies
p.dead = true;
gameState = 'dead';
deathTimer = 90;
addParticle(p.x + p.w / 2, p.y + p.h / 2, '#FF0000', 15);
}
}
}
// check goal
if (!flag.reached && aabb(p.x, p.y, p.w, p.h, flag.x - 8, flag.y, 16, flag.h)) {
flag.reached = true;
gameState = 'won';
winTimer = 180;
addParticle(flag.x, flag.y, '#00FF00', 25);
score += 1000;
}
// fall into pit
if (p.y > H + 100) {
gameState = 'dead';
deathTimer = 90;
}
// 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.2;
pt.life--;
if (pt.life <= 0) particles.splice(i, 1);
}
}
// ---- drawing helpers ----------------------------------------------------
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#5C94FC');
grad.addColorStop(0.7, '#87CEEB');
grad.addColorStop(1, '#B0E0FF');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawClouds() {
ctx.fillStyle = 'rgba(255,255,255,0.9)';
for (const c of clouds) {
const sx = c.x - camera.x * 0.3;
if (sx < -c.w || sx > W + c.w) continue;
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.3, c.h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(sx + c.w * 0.25, c.y + 4, c.w * 0.3, c.h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawHills() {
for (const h of hills) {
const sx = h.x - camera.x * 0.5;
if (sx < -h.w || sx > W + h.w) continue;
const baseY = H - TILE + 4;
ctx.fillStyle = '#4A8C3F';
ctx.beginPath();
ctx.ellipse(sx, baseY, h.w / 2, h.h, 0, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#5BA04A';
ctx.beginPath();
ctx.ellipse(sx, baseY, h.w / 3, h.h * 0.8, 0, Math.PI, 0);
ctx.fill();
}
}
function drawBushes() {
for (const b of bushes) {
const sx = b.x - camera.x;
if (sx < -b.w || sx > W + b.w) continue;
const by = b.y + 2;
ctx.fillStyle = '#3A7A30';
ctx.beginPath();
ctx.ellipse(sx, by, b.w / 2, b.h / 2, 0, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#4A9A40';
ctx.beginPath();
ctx.ellipse(sx + 4, by - 2, b.w / 3, b.h / 2.5, 0, Math.PI, 0);
ctx.fill();
}
}
function drawGround() {
for (const g of groundSegments) {
const sx = g.x - camera.x;
if (sx > W + 10 || sx + g.w < -10) continue;
// dirt
ctx.fillStyle = '#C84C09';
ctx.fillRect(sx, g.y, g.w, TILE);
// grass top
ctx.fillStyle = '#4AAF3A';
ctx.fillRect(sx, g.y, g.w, 8);
// grass details
ctx.fillStyle = '#5CC848';
for (let gx = 0; gx < g.w; gx += 16) {
ctx.fillRect(sx + gx, g.y, 4, 6);
}
// dirt texture
ctx.fillStyle = '#A03800';
for (let dy = 12; dy < TILE; dy += 10) {
for (let dx = 4 + ((dy / 10) | 0) * 8; dx < g.w; dx += 16) {
ctx.fillRect(sx + dx, g.y + dy, 6, 4);
}
}
}
}
function drawPlatforms() {
for (const pl of platforms) {
const sx = pl.x - camera.x;
if (sx > W + 10 || sx + pl.w < -10) continue;
if (pl.type === 'question') {
ctx.fillStyle = '#F8B800';
ctx.fillRect(sx, pl.y, pl.w, TILE);
ctx.fillStyle = '#DAA520';
ctx.fillRect(sx + 2, pl.y + 2, pl.w - 4, TILE - 4);
ctx.fillStyle = '#FFF';
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'center';
ctx.fillText('?', sx + pl.w / 2, pl.y + TILE - 8);
} else if (pl.type === 'brick') {
ctx.fillStyle = '#C84C09';
ctx.fillRect(sx, pl.y, pl.w, TILE);
ctx.fillStyle = '#E09060';
for (let bx = 0; bx < pl.w; bx += 16) {
ctx.fillRect(sx + bx + 1, pl.y + 1, 14, 14);
ctx.fillRect(sx + bx + 1, pl.y + 17, 14, 14);
ctx.fillStyle = '#A03800';
ctx.fillRect(sx + bx + 1, pl.y + 15, 14, 2);
ctx.fillStyle = '#E09060';
}
} else {
ctx.fillStyle = '#8B6914';
ctx.fillRect(sx, pl.y, pl.w, TILE);
ctx.fillStyle = '#A07828';
ctx.fillRect(sx + 1, pl.y + 1, pl.w - 2, TILE - 2);
ctx.fillStyle = '#6B4F10';
ctx.fillRect(sx, pl.y + TILE - 2, pl.w, 2);
ctx.fillRect(sx + pl.w - 2, pl.y, 2, TILE);
}
}
}
function drawCoins() {
for (const c of coins) {
if (c.collected) continue;
const sx = c.x - camera.x;
if (sx < -20 || sx > W + 20) continue;
const bounce = Math.sin(c.anim) * 3;
const stretch = 0.6 + 0.4 * Math.cos(c.anim * 0.8);
const cy = c.y + bounce;
// coin body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.ellipse(sx, cy, 8 * stretch, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFC000';
ctx.beginPath();
ctx.ellipse(sx, cy, 5 * stretch, 5, 0, 0, Math.PI * 2);
ctx.fill();
// shine
ctx.fillStyle = '#FFF';
ctx.beginPath();
ctx.ellipse(sx - 2 * stretch, cy - 3, 2, 2, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawEnemies() {
for (const e of enemies) {
if (!e.alive && e.squishTimer <= 0) continue;
const sx = e.x - camera.x;
if (sx < -40 || sx > W + 40) continue;
if (!e.alive) {
// squished
ctx.fillStyle = '#8B4513';
ctx.fillRect(sx, e.y + e.h - 8, e.w, 8);
continue;
}
// body
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.ellipse(sx + e.w / 2, e.y + e.h / 2 + 2, e.w / 2, e.h / 2 - 2, 0, 0, Math.PI * 2);
ctx.fill();
// feet
const walk = Math.sin(Date.now() / 150) * 2;
ctx.fillStyle = '#4A2008';
ctx.fillRect(sx + 2 + walk, e.y + e.h - 6, 10, 6);
ctx.fillRect(sx + e.w - 12 - walk, e.y + e.h - 6, 10, 6);
// eyes
ctx.fillStyle = '#FFF';
ctx.fillRect(sx + 6, e.y + 6, 6, 7);
ctx.fillRect(sx + e.w - 12, e.y + 6, 6, 7);
ctx.fillStyle = '#000';
const ex = e.vx > 0 ? 2 : 0;
ctx.fillRect(sx + 7 + ex, e.y + 8, 3, 4);
ctx.fillRect(sx + e.w - 11 + ex, e.y + 8, 3, 4);
// eyebrows (angry)
ctx.fillStyle = '#000';
ctx.fillRect(sx + 5, e.y + 4, 8, 2);
ctx.fillRect(sx + e.w - 13, e.y + 4, 8, 2);
}
}
function drawFlag() {
const sx = flag.x - camera.x;
if (sx < -60 || sx > W + 60) return;
// pole
ctx.fillStyle = '#888';
ctx.fillRect(sx - 2, flag.y, 4, flag.h);
// ball on top
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(sx, flag.y, 6, 0, Math.PI * 2);
ctx.fill();
// flag
const wave = Math.sin(Date.now() / 300) * 3;
ctx.fillStyle = '#FF3333';
ctx.beginPath();
ctx.moveTo(sx + 2, flag.y + 4);
ctx.lineTo(sx + 36 + wave, flag.y + 14);
ctx.lineTo(sx + 2, flag.y + 28);
ctx.closePath();
ctx.fill();
// star on flag
ctx.fillStyle = '#FFD700';
ctx.font = '12px monospace';
ctx.fillText('★', sx + 10 + wave * 0.5, flag.y + 20);
// "GOAL" text
ctx.fillStyle = '#FFF';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.fillText('GOAL', sx, flag.y - 8);
ctx.textAlign = 'left';
}
function drawPlayer() {
if (gameState === 'dead') return;
const p = player;
const sx = p.x - camera.x;
const sy = p.y;
const f = p.facing;
ctx.save();
ctx.translate(sx + p.w / 2, sy + p.h);
if (f === -1) ctx.scale(-1, 1);
// body
ctx.fillStyle = '#E03020'; // red shirt
ctx.fillRect(-p.w / 2, -p.h + 8, p.w, p.h - 16);
// overalls (blue)
ctx.fillStyle = '#2040C0';
ctx.fillRect(-p.w / 2 + 2, -12, p.w - 4, 10);
// overall straps
ctx.fillStyle = '#2040C0';
ctx.fillRect(-p.w / 2 + 2, -p.h + 14, 5, 8);
ctx.fillRect(p.w / 2 - 7, -p.h + 14, 5, 8);
// overalls buttons
ctx.fillStyle = '#FFD700';
ctx.fillRect(-p.w / 2 + 3, -8, 3, 3);
ctx.fillRect(p.w / 2 - 6, -8, 3, 3);
// legs
const legAnim = p.onGround ? Math.sin(p.frame * Math.PI / 2) * 4 : 2;
ctx.fillStyle = '#2040C0';
ctx.fillRect(-p.w / 2 + 1, -10, 9, 10 + legAnim);
ctx.fillRect(p.w / 2 - 10, -10, 9, 10 - legAnim);
// shoes
ctx.fillStyle = '#6B3300';
ctx.fillRect(-p.w / 2 - 1, -1 + legAnim, 11, 4);
ctx.fillRect(p.w / 2 - 10, -1 - legAnim, 11, 4);
// head
ctx.fillStyle = '#FFCC88';
ctx.fillRect(-p.w / 2 + 2, -p.h + 2, p.w - 4, 12);
// hair/hat
ctx.fillStyle = '#E03020';
ctx.fillRect(-p.w / 2 + 1, -p.h, p.w - 2, 6);
ctx.fillRect(-p.w / 2 + 3, -p.h + 1, p.w + 4, 4);
// hat brim
ctx.fillStyle = '#E03020';
ctx.fillRect(-p.w / 2 + 2, -p.h + 5, p.w + 4, 3);
// face
ctx.fillStyle = '#FFCC88';
ctx.fillRect(-p.w / 2 + 3, -p.h + 8, p.w - 6, 10);
// eyes
ctx.fillStyle = '#000';
ctx.fillRect(-p.w / 2 + 5, -p.h + 10, 3, 4);
ctx.fillRect(-p.w / 2 + 13, -p.h + 10, 3, 4);
// mustache
ctx.fillStyle = '#4A2800';
ctx.fillRect(-p.w / 2 + 4, -p.h + 15, 8, 2);
ctx.fillRect(-p.w / 2 + 10, -p.h + 15, 8, 2);
// arm
ctx.fillStyle = '#FFCC88';
const armSwing = p.onGround ? Math.sin(p.frame * Math.PI / 2) * 3 : -2;
ctx.fillRect(p.w / 2 - 2, -p.h + 16 + armSwing, 5, 8);
ctx.restore();
}
function drawParticles() {
for (const pt of particles) {
const alpha = pt.life / 50;
ctx.globalAlpha = Math.min(1, alpha);
ctx.fillStyle = pt.color;
ctx.fillRect(pt.x - camera.x - 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(8, 8, 180, 36);
ctx.strokeStyle = '#FFF';
ctx.lineWidth = 1;
ctx.strokeRect(8, 8, 180, 36);
// coins
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 16px monospace';
ctx.fillText('🪙 × ' + score, 16, 31);
// coins remaining
const remaining = coins.filter(c => !c.collected).length;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(W - 160, 8, 152, 36);
ctx.strokeStyle = '#FFF';
ctx.strokeRect(W - 160, 8, 152, 36);
ctx.fillStyle = '#FFF';
ctx.textAlign = 'right';
ctx.fillText('Coins: ' + (totalCoins - remaining) + '/' + totalCoins, W - 16, 31);
ctx.textAlign = 'left';
}
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('Score: ' + score, W / 2, H / 2 + 10);
ctx.fillText('Coins collected: ' + (totalCoins - coins.filter(c => !c.collected).length) + '/' + totalCoins, W / 2, H / 2 + 45);
ctx.font = '16px monospace';
ctx.fillStyle = '#AAA';
ctx.fillText('Press SPACE to play again', W / 2, H / 2 + 90);
ctx.textAlign = 'left';
if (keys['Space']) {
keys['Space'] = false;
init();
}
}
function drawDeathScreen() {
ctx.fillStyle = 'rgba(100,0,0,0.4)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#FFF';
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', W / 2, H / 2);
ctx.font = '16px monospace';
ctx.fillStyle = '#CCC';
ctx.fillText('Restarting...', W / 2, H / 2 + 30);
ctx.textAlign = 'left';
}
function drawStartHint() {
if (player.x < 200 && score === 0) {
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(W / 2 - 160, H / 2 - 40, 320, 50);
ctx.fillStyle = '#FFF';
ctx.font = '16px monospace';
ctx.textAlign = 'center';
ctx.fillText('← → or A D to move | Space or ↑ to jump', W / 2, H / 2 - 12);
ctx.fillText('Collect coins and reach the red flag!', W / 2, H / 2 + 12);
ctx.textAlign = 'left';
}
}
// ---- main loop ------------------------------------------------------------
function loop() {
update();
drawSky();
drawClouds();
drawHills();
drawBushes();
drawGround();
drawPlatforms();
drawCoins();
drawEnemies();
drawFlag();
drawPlayer();
drawParticles();
drawStartHint();
drawHUD();
if (gameState === 'dead') drawDeathScreen();
if (gameState === 'won') drawWinScreen();
requestAnimationFrame(loop);
}
// ---- start ----------------------------------------------------------------
init();
loop();
})();
<!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: #111;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
font-family: 'Courier New', monospace;
}
canvas {
border: 3px solid #333;
image-rendering: pixelated;
display: block;
}
#info {
color: #888;
margin-top: 10px;
font-size: 13px;
text-align: center;
}
#info kbd {
background: #333;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #555;
color: #ccc;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">
<kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> to move |
<kbd>Space</kbd> <kbd>↑</kbd> or <kbd>W</kbd> to jump |
Collect coins & reach the flag!
</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">
<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: #111;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: monospace;
overflow: hidden;
}
canvas {
border: 3px solid #444;
image-rendering: pixelated;
display: block;
}
#ui-overlay {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
text-shadow: 2px 2px 0 #000;
pointer-events: none;
text-align: center;
z-index: 10;
}
#instructions {
color: #888;
font-size: 13px;
margin-top: 8px;
text-align: center;
}
</style>
</head>
<body>
<div id="ui-overlay">
<span id="score-display">Coins: 0 / 0</span>
</div>
<canvas id="game" width="800" height="450"></canvas>
<div id="instructions">Arrow Keys / WASD to move • Space / Up / W to jump • R to restart</div>
<script>
// ============================================================
// SUPER PLATFORMER — vanilla JS, no dependencies
// ============================================================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('score-display');
const W = canvas.width; // 800
const H = canvas.height; // 450
const TILE = 32;
// ---------- Input ----------
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
function keyLeft() { return keys['ArrowLeft'] || keys['KeyA']; }
function keyRight() { return keys['ArrowRight'] || keys['KeyD']; }
function keyJump() { return keys['Space'] || keys['ArrowUp'] || keys['KeyW']; }
// ---------- Game state ----------
let gameState = 'playing'; // 'playing' | 'won' | 'dead'
let score = 0;
let totalCoins = 0;
let deathTimer = 0;
let winTimer = 0;
// ---------- Level data ----------
// Map: each entry is a tile type at grid position (col, row)
// 0 = empty, 1 = ground, 2 = brick, 3 = question block, 4 = pipe
// We'll build the level procedurally.
function buildLevel() {
const cols = 120;
const rows = Math.ceil(H / TILE) + 2;
const map = Array.from({ length: rows }, () => Array(cols).fill(0));
// Ground — leave gaps for pits
const groundRows = rows - 2;
const gaps = [
{ start: 18, end: 20 },
{ start: 36, end: 39 },
{ start: 70, end: 72 },
];
for (let c = 0; c < cols; c++) {
let isGap = gaps.some(g => c >= g.start && c < g.end);
if (!isGap) {
map[groundRows][c] = 1;
map[groundRows + 1][c] = 1;
}
}
// Floating platforms & blocks
const platforms = [
// (col, row, width, type)
[10, groundRows - 5, 5, 2], // bricks
[22, groundRows - 5, 3, 3], // question blocks
[30, groundRows - 4, 6, 2],
[38, groundRows - 7, 3, 3],
[50, groundRows - 5, 4, 2],
[55, groundRows - 5, 1, 3],
[62, groundRows - 4, 5, 2],
[80, groundRows - 5, 3, 2],
[85, groundRows - 8, 4, 3],
[95, groundRows - 5, 6, 2],
];
platforms.forEach(([c, r, w, t]) => {
for (let i = 0; i < w; i++) map[r][c + i] = t;
});
// Pipes
const pipes = [
{ col: 14, height: 2 },
{ col: 32, height: 3 },
{ col: 56, height: 2 },
{ col: 88, height: 4 },
];
pipes.forEach(p => {
for (let h = 0; h < p.height; h++) {
map[groundRows - h][p.col] = 4;
map[groundRows - h][p.col + 1] = 4;
}
});
// Stairs near the end
const stairStart = 103;
for (let i = 0; i < 5; i++) {
for (let h = 0; h <= i; h++) {
map[groundRows - h][stairStart + i] = 2;
}
}
// Coins on platforms
const coinPositions = [];
// Coins in the air above gaps
coinPositions.push({ col: 16, row: groundRows - 4 });
coinPositions.push({ col: 17, row: groundRows - 4 });
coinPositions.push({ col: 18, row: groundRows - 5 });
// Coins on platforms
const coinOnPlatform = [
[11, groundRows - 7], [12, groundRows - 7], [13, groundRows - 7],
[23, groundRows - 7], [24, groundRows - 7],
[31, groundRows - 6], [32, groundRows - 6], [33, groundRows - 6], [34, groundRows - 6],
[39, groundRows - 9],
[51, groundRows - 7], [52, groundRows - 7],
[63, groundRows - 6], [64, groundRows - 6], [65, groundRows - 6],
[81, groundRows - 7], [82, groundRows - 7],
[86, groundRows - 10], [87, groundRows - 10],
[96, groundRows - 7], [97, groundRows - 7], [98, groundRows - 7],
];
coinPositions.push(...coinOnPlatform);
// Stair coins
for (let i = 0; i < 4; i++) {
coinPositions.push({ col: stairStart + i, row: groundRows - i - 2 });
}
// Flag at the end
const flagCol = 115;
const flagRow = groundRows - 8;
return { map, coinPositions, flagCol, flagRow, cols, groundRow: groundRows };
}
let level;
function initGame() {
level = buildLevel();
totalCoins = level.coinPositions.length;
score = 0;
gameState = 'playing';
deathTimer = 0;
winTimer = 0;
// Player
player.x = 3 * TILE;
player.y = (level.groundRow - 2) * TILE;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.facing = 1;
player.animFrame = 0;
player.animTimer = 0;
// Enemies
enemies = [
{ x: 12 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 25 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 42 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 44 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 60 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 75 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 90 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
{ x: 110 * TILE, y: (level.groundRow - 1) * TILE, vx: -1, w: TILE, h: TILE, alive: true, type: 'goomba' },
];
// Coins
coins = level.coinPositions.map(c => ({
x: c.col * TILE + 8,
y: c.row * TILE + 8,
w: 16, h: 16,
collected: false,
animOffset: Math.random() * Math.PI * 2,
}));
// Clouds and hills for background
if (!clouds.length) {
for (let i = 0; i < 20; i++) {
clouds.push({
x: i * 120 + Math.random() * 60,
y: 20 + Math.random() * 60,
w: 60 + Math.random() * 40,
});
}
for (let i = 0; i < 15; i++) {
hills.push({
x: i * 200 + Math.random() * 80,
r: 40 + Math.random() * 30,
});
}
}
}
// ---------- Entities ----------
let player = {
x: 0, y: 0, w: 24, h: 30,
vx: 0, vy: 0,
onGround: false,
facing: 1,
animFrame: 0,
animTimer: 0,
};
const GRAVITY = 0.55;
const JUMP_FORCE = -10.5;
const MOVE_SPEED = 3.5;
const FRICTION = 0.82;
const MAX_FALL = 12;
let enemies = [];
let coins = [];
let clouds = [];
let hills = [];
let particles = [];
let cameraX = 0;
// ---------- Collision helpers ----------
function getTile(col, row) {
if (col < 0 || col >= level.cols || row < 0 || row >= level.map.length) return 0;
return level.map[row][col];
}
function isSolid(type) {
return type === 1 || type === 2 || type === 3 || type === 4;
}
function rectOverlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function resolveCollisions() {
player.onGround = false;
const p = player;
const map = level.map;
const groundRow = level.groundRow;
// Horizontal
p.x += p.vx;
// Left
if (p.vx < 0) {
const leftCol = Math.floor(p.x / TILE);
const topRow = Math.floor(p.y / TILE);
const botRow = Math.floor((p.y + p.h - 1) / TILE);
for (let r = topRow; r <= botRow; r++) {
if (isSolid(getTile(leftCol, r))) {
p.x = (leftCol + 1) * TILE;
p.vx = 0;
break;
}
}
}
// Right
if (p.vx > 0) {
const rightCol = Math.floor((p.x + p.w) / TILE);
const topRow = Math.floor(p.y / TILE);
const botRow = Math.floor((p.y + p.h - 1) / TILE);
for (let r = topRow; r <= botRow; r++) {
if (isSolid(getTile(rightCol, r))) {
p.x = rightCol * TILE - p.w;
p.vx = 0;
break;
}
}
}
// Vertical
p.y += p.vy;
// Top (hitting ceiling)
if (p.vy < 0) {
const topRow = Math.floor(p.y / TILE);
const leftCol = Math.floor(p.x / TILE);
const rightCol = Math.floor((p.x + p.w - 1) / TILE);
for (let c = leftCol; c <= rightCol; c++) {
if (isSolid(getTile(c, topRow))) {
p.y = (topRow + 1) * TILE;
p.vy = 0;
break;
}
}
}
// Bottom (landing)
if (p.vy >= 0) {
const botRow = Math.floor((p.y + p.h) / TILE);
const leftCol = Math.floor(p.x / TILE);
const rightCol = Math.floor((p.x + p.w - 1) / TILE);
for (let c = leftCol; c <= rightCol; c++) {
if (isSolid(getTile(c, botRow))) {
p.y = botRow * TILE - p.h;
p.vy = 0;
p.onGround = true;
break;
}
}
}
}
// ---------- Drawing helpers ----------
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#5c94fc');
grad.addColorStop(0.7, '#87CEEB');
grad.addColorStop(1, '#b8e0ff');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawClouds(camX) {
clouds.forEach(cl => {
const sx = cl.x - camX * 0.3;
if (sx > -100 && sx < W + 100) {
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.ellipse(sx, cl.y, cl.w * 0.5, 14, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(sx - cl.w * 0.25, cl.y + 5, cl.w * 0.3, 10, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(sx + cl.w * 0.25, cl.y + 5, cl.w * 0.35, 11, 0, 0, Math.PI * 2);
ctx.fill();
}
});
}
function drawHills(camX) {
hills.forEach(h => {
const sx = h.x - camX * 0.5;
if (sx > -h.r * 2 && sx < W + h.r * 2) {
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.ellipse(sx, level.groundRow * TILE, h.r * 2, h.r, 0, Math.PI, 0);
ctx.fill();
ctx.fillStyle = '#66BB6A';
ctx.beginPath();
ctx.ellipse(sx, level.groundRow * TILE, h.r, h.r * 0.6, 0, Math.PI, 0);
ctx.fill();
}
});
}
function drawTile(col, row, type, camX) {
const x = col * TILE - camX;
const y = row * TILE;
if (x < -TILE || x > W + TILE) return;
switch (type) {
case 1: { // Ground
ctx.fillStyle = '#8B4513';
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = '#5a8f3c';
ctx.fillRect(x, y, TILE, 6);
// Dirt texture
ctx.fillStyle = '#7a3e10';
ctx.fillRect(x + 4, y + 12, 6, 6);
ctx.fillRect(x + 18, y + 20, 8, 4);
ctx.fillRect(x + 8, y + 24, 5, 5);
break;
}
case 2: { // Brick
ctx.fillStyle = '#C84C09';
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = '#8B3000';
ctx.lineWidth = 1;
// Brick lines
ctx.strokeRect(x + 0.5, y + 0.5, 15, 15);
ctx.strokeRect(x + 16, y + 0.5, 15, 15);
ctx.strokeRect(x + 8, y + 16, 16, 15);
ctx.strokeStyle = '#E8601C';
ctx.strokeRect(x + 1, y + 1, 14, 14);
ctx.strokeRect(x + 17, y + 1, 14, 14);
ctx.strokeRect(x + 9, y + 17, 14, 13);
break;
}
case 3: { // Question block
const pulse = Math.sin(Date.now() / 300) * 0.15 + 0.85;
ctx.fillStyle = `rgb(${Math.floor(230 * pulse)}, ${Math.floor(180 * pulse)}, 0)`;
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = '#A07000';
ctx.lineWidth = 2;
ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
// Question mark
ctx.fillStyle = '#FFF';
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('?', x + TILE / 2, y + TILE / 2 + 1);
break;
}
case 4: { // Pipe
// Check if top
const below = getTile(col, row + 1);
const leftOf = getTile(col - 1, row);
const rightOf = getTile(col + 1, row);
const isLeftSide = (col === 0 || !isSolid(getTile(col - 1, row)));
const isRightSide = (col + 1 >= level.cols || !isSolid(getTile(col + 1, row)));
ctx.fillStyle = '#00AA00';
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = '#00DD00';
ctx.fillRect(x + 3, y, 8, TILE);
ctx.fillStyle = '#008800';
ctx.fillRect(x + TILE - 6, y, 5, TILE);
if (!isSolid(below) || (leftOf !== 4 && rightOf !== 4)) {
// Pipe top rim
ctx.fillStyle = '#00CC00';
ctx.fillRect(x - 2, y, TILE + 4, 8);
ctx.fillStyle = '#00FF00';
ctx.fillRect(x - 1, y + 1, 6, 6);
ctx.fillStyle = '#006600';
ctx.fillRect(x + TILE - 2, y + 1, 5, 6);
}
break;
}
}
}
function drawMap(camX) {
const startCol = Math.max(0, Math.floor(camX / TILE) - 1);
const endCol = Math.min(level.cols, Math.ceil((camX + W) / TILE) + 1);
const startRow = 0;
const endRow = level.map.length;
for (let r = startRow; r < endRow; r++) {
for (let c = startCol; c < endCol; c++) {
if (level.map[r][c]) drawTile(c, r, level.map[r][c], camX);
}
}
}
function drawPlayer(camX) {
const p = player;
const sx = Math.round(p.x - camX);
const sy = Math.round(p.y);
const f = p.facing;
// Body
ctx.fillStyle = '#E52521'; // Red shirt
ctx.fillRect(sx + 4, sy + 8, 16, 12);
// Head
ctx.fillStyle = '#FFB366'; // Skin
ctx.fillRect(sx + 5, sy, 14, 12);
// Hat
ctx.fillStyle = '#E52521';
ctx.fillRect(sx + 3, sy - 2, 16, 6);
ctx.fillRect(sx + (f > 0 ? 10 : 1), sy + 2, 14, 3);
// Eyes
ctx.fillStyle = '#000';
const eyeX = sx + (f > 0 ? 14 : 7);
ctx.fillRect(eyeX, sy + 4, 3, 3);
// Mustache
ctx.fillStyle = '#4A2800';
ctx.fillRect(sx + (f > 0 ? 11 : 5), sy + 8, 8, 2);
// Overalls
ctx.fillStyle = '#0051D8'; // Blue
ctx.fillRect(sx + 4, sy + 18, 16, 8);
// Legs
ctx.fillStyle = '#0051D8';
if (!p.onGround) {
// Jumping pose
ctx.fillRect(sx + 2, sy + 26, 7, 5);
ctx.fillRect(sx + 15, sy + 24, 7, 5);
} else if (Math.abs(p.vx) > 0.3) {
// Walking animation
const legPhase = Math.sin(p.animFrame * 0.8);
ctx.fillRect(sx + 3, sy + 26, 7, 4 + legPhase * 2);
ctx.fillRect(sx + 14, sy + 26, 7, 4 - legPhase * 2);
} else {
ctx.fillRect(sx + 3, sy + 26, 7, 4);
ctx.fillRect(sx + 14, sy + 26, 7, 4);
}
// Shoes
ctx.fillStyle = '#6B3300';
if (Math.abs(p.vx) > 0.3) {
const legPhase = Math.sin(p.animFrame * 0.8);
ctx.fillRect(sx + 2, sy + 28 + legPhase * 2, 8, 3);
ctx.fillRect(sx + 14, sy + 28 - legPhase * 2, 8, 3);
} else {
ctx.fillRect(sx + 2, sy + 30, 8, 3);
ctx.fillRect(sx + 14, sy + 30, 8, 3);
}
}
function drawEnemy(e, camX) {
if (!e.alive) return;
const sx = Math.round(e.x - camX);
const sy = Math.round(e.y);
// Goomba body
ctx.fillStyle = '#A0522D';
ctx.beginPath();
ctx.ellipse(sx + TILE / 2, sy + TILE / 2 + 2, 14, 12, 0, 0, Math.PI * 2);
ctx.fill();
// Feet
ctx.fillStyle = '#000';
const walkOff = Math.sin(Date.now() / 150) * 3;
ctx.fillRect(sx + 3, sy + TILE - 4 + walkOff, 8, 5);
ctx.fillRect(sx + TILE - 11, sy + TILE - 4 - walkOff, 8, 5);
// Eyes
ctx.fillStyle = '#FFF';
ctx.fillRect(sx + 7, sy + 8, 6, 7);
ctx.fillRect(sx + TILE - 13, sy + 8, 6, 7);
ctx.fillStyle = '#000';
ctx.fillRect(sx + 9, sy + 10, 3, 4);
ctx.fillRect(sx + TILE - 11, sy + 10, 3, 4);
// Angry eyebrows
ctx.fillStyle = '#000';
ctx.fillRect(sx + 6, sy + 6, 7, 2);
ctx.fillRect(sx + TILE - 13, sy + 6, 7, 2);
}
function drawCoin(coin, camX) {
if (coin.collected) return;
const sx = Math.round(coin.x - camX);
const sy = Math.round(coin.y + Math.sin(Date.now() / 300 + coin.animOffset) * 3);
const stretch = Math.abs(Math.sin(Date.now() / 400 + coin.animOffset));
const w = Math.max(4, 14 * stretch);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.ellipse(sx + 8, sy + 8, w / 2, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.ellipse(sx + 8, sy + 8, w / 2 - 2, 6, 0, 0, Math.PI * 2);
ctx.fill();
// Shine
if (stretch > 0.5) {
ctx.fillStyle = '#FFF';
ctx.fillRect(sx + 6, sy + 4, 2, 2);
}
}
function drawFlag(camX) {
const fc = level.flagCol;
const fr = level.flagRow;
const sx = fc * TILE - camX;
const baseY = level.groundRow * TILE;
// Pole
ctx.fillStyle = '#888';
ctx.fillRect(sx + 13, fr * TILE, 5, baseY - fr * TILE);
// Ball on top
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(sx + 15, fr * TILE, 5, 0, Math.PI * 2);
ctx.fill();
// Flag
const wave = Math.sin(Date.now() / 400) * 3;
ctx.fillStyle = '#00CC00';
ctx.beginPath();
ctx.moveTo(sx + 18, fr * TILE + 6);
ctx.lineTo(sx + 48 + wave, fr * TILE + 18);
ctx.lineTo(sx + 18, fr * TILE + 32);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = '#FFF';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText('★', sx + 26 + wave / 2, fr * TILE + 23);
}
function drawParticles(camX) {
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.globalAlpha = p.life;
ctx.fillRect(p.x - camX, p.y, p.size, p.size);
});
ctx.globalAlpha = 1;
}
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) * 6,
size: 2 + Math.random() * 3,
color,
life: 1,
decay: 0.02 + Math.random() * 0.03,
});
}
}
function drawWinText() {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🏆 YOU WIN! 🏆', W / 2, H / 2 - 30);
ctx.fillStyle = '#FFF';
ctx.font = '24px monospace';
ctx.fillText(`Coins collected: ${score} / ${totalCoins}`, W / 2, H / 2 + 20);
ctx.fillText(`Press R to play again`, W / 2, H / 2 + 60);
}
function drawDeathText() {
ctx.fillStyle = 'rgba(100,0,0,0.5)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#F00';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('GAME OVER', W / 2, H / 2);
ctx.fillStyle = '#FFF';
ctx.font = '20px monospace';
ctx.fillText(`Press R to retry`, W / 2, H / 2 + 40);
}
// ---------- Update ----------
function update() {
if (gameState === 'dead') {
deathTimer++;
if (deathTimer > 60) {
initGame();
}
// Still update particles
updateParticles();
return;
}
if (gameState === 'won') {
winTimer++;
updateParticles();
return;
}
const p = player;
// Movement
if (keyLeft()) {
p.vx -= 0.8;
p.facing = -1;
}
if (keyRight()) {
p.vx += 0.8;
p.facing = 1;
}
if (!keyLeft() && !keyRight()) {
p.vx *= FRICTION;
if (Math.abs(p.vx) < 0.1) p.vx = 0;
}
p.vx = Math.max(-MOVE_SPEED, Math.min(MOVE_SPEED, p.vx));
// Jump
if (keyJump() && p.onGround) {
p.vy = JUMP_FORCE;
p.onGround = false;
}
// Gravity
p.vy += GRAVITY;
if (p.vy > MAX_FALL) p.vy = MAX_FALL;
// Animation
if (Math.abs(p.vx) > 0.3) {
p.animTimer++;
if (p.animTimer > 5) {
p.animTimer = 0;
p.animFrame++;
}
}
// Resolve collisions
resolveCollisions();
// Fall into pit
if (p.y > level.map.length * TILE + 50) {
gameState = 'dead';
deathTimer = 0;
spawnParticles(p.x + p.w / 2, p.y, '#F00', 15);
return;
}
// Camera
const targetCamX = p.x - W / 3;
cameraX += (targetCamX - cameraX) * 0.1;
cameraX = Math.max(0, Math.min(cameraX, level.cols * TILE - W));
// Coins
coins.forEach(coin => {
if (coin.collected) return;
const coinRect = { x: coin.x, y: coin.y, w: coin.w, h: coin.h };
const playerRect = { x: p.x, y: p.y, w: p.w, h: p.h };
if (rectOverlap(playerRect, coinRect)) {
coin.collected = true;
score++;
scoreDisplay.textContent = `Coins: ${score} / ${totalCoins}`;
spawnParticles(coin.x + 8, coin.y + 8, '#FFD700', 8);
}
});
// Enemies
enemies.forEach(e => {
if (!e.alive) return;
// Move
e.x += e.vx;
// Enemy-ground collision
const eBotCol = Math.floor((e.y + e.h) / TILE);
const eLeftCol = Math.floor(e.x / TILE);
const eRightCol = Math.floor((e.x + e.w - 1) / TILE);
let onGround = false;
for (let c = eLeftCol; c <= eRightCol; c++) {
if (isSolid(getTile(c, eBotCol))) {
e.y = eBotCol * TILE - e.h;
onGround = true;
break;
}
}
// Check wall collision — reverse direction
const edgeCol = e.vx > 0 ? Math.floor((e.x + e.w) / TILE) : Math.floor((e.x - 1) / TILE);
const eyeRow = Math.floor((e.y + e.h / 2) / TILE);
if (isSolid(getTile(edgeCol, eyeRow))) {
e.vx *= -1;
}
// Fall into pit — kill
if (e.y > level.map.length * TILE + 50) {
e.alive = false;
}
// Player collision
const pRect = { x: p.x, y: p.y, w: p.w, h: p.h };
const eRect = { x: e.x, y: e.y, w: e.w, h: e.h };
if (rectOverlap(pRect, eRect)) {
// Stomp from above
if (p.vy > 0 && p.y + p.h - e.y < 16) {
e.alive = false;
p.vy = JUMP_FORCE * 0.5;
spawnParticles(e.x + e.w / 2, e.y + e.h / 2, '#A0522D', 10);
scoreDisplay.textContent = `Coins: ${score} / ${totalCoins}`;
} else {
// Player dies
gameState = 'dead';
deathTimer = 0;
spawnParticles(p.x + p.w / 2, p.y + p.h / 2, '#E52521', 20);
}
}
});
// Flag check (win)
const flagRect = {
x: level.flagCol * TILE,
y: level.flagRow * TILE,
w: TILE,
h: TILE * 8,
};
const pRect2 = { x: p.x, y: p.y, w: p.w, h: p.h };
if (rectOverlap(pRect2, flagRect)) {
gameState = 'won';
winTimer = 0;
spawnParticles(p.x + p.w / 2, p.y, '#FFD700', 30);
spawnParticles(p.x + p.w / 2, p.y, '#00CC00', 20);
}
updateParticles();
}
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 -= p.decay;
if (p.life <= 0) particles.splice(i, 1);
}
}
function restartGame() {
clouds = [];
hills = [];
particles = [];
initGame();
}
// ---------- Render ----------
function render() {
drawSky();
drawClouds(cameraX);
drawHills(cameraX);
drawMap(cameraX);
drawFlag(cameraX);
coins.forEach(c => drawCoin(c, cameraX));
enemies.forEach(e => drawEnemy(e, cameraX));
drawPlayer(cameraX);
drawParticles(cameraX);
if (gameState === 'won') drawWinText();
if (gameState === 'dead') drawDeathText();
}
// ---------- Game loop ----------
function gameLoop() {
if (keys['KeyR']) {
restartGame();
keys['KeyR'] = false;
}
update();
render();
requestAnimationFrame(gameLoop);
}
// ---------- Start ----------
initGame();
gameLoop();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
/* === Super Platformer — game.js === */
(function () {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width; // 800
const H = canvas.height; // 450
/* ---------- colours ---------- */
const C = {
sky: '#5c94fc',
ground: '#c84c0c',
groundTop: '#fcb746',
platform: '#c84c0c',
platformT: '#fcb746',
brick: '#c0600a',
brickLine: '#8b3808',
coin: '#fcfc00',
coinDark: '#c4a800',
player: '#e00000',
playerSkin:'#ffbb99',
playerHat: '#e00000',
playerPants:'#0000cc',
flag: '#00cc00',
flagPole: '#888',
cloud: '#ffffff',
bush: '#30a030',
hill: '#60d060',
hillDark: '#48a848',
pipe: '#00a800',
pipeDark: '#006800',
scoreBg: 'rgba(0,0,0,0.45)',
winBg: 'rgba(0,0,0,0.6)',
};
/* ---------- input ---------- */
const keys = {};
window.addEventListener('keydown', function (e) {
keys[e.code] = true;
// Prevent page-scroll for game keys
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', function (e) {
keys[e.code] = false;
});
/* ---------- helper ---------- */
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;
}
/* ================================================================
LEVEL DESIGN
================================================================ */
// Tile size = 32 px — we build the level as a series of platform rects,
// coin rects, a pipe, a flag, and decorative elements.
const GRAVITY = 0.55;
const PLAYER_SPEED = 3.2;
const JUMP_FORCE = -10.5;
const FRICTION = 0.82;
const AIR_FRICTION = 0.92;
// The level is longer than the viewport; the camera scrolls with the player.
// levelWidth in pixels.
const LEVEL_WIDTH = 4800;
/* Platforms – {x, y, w, h} */
const platforms = [
// Ground segments (with gaps)
{ x: 0, y: H - 32, w: 900, h: 32 },
{ x: 1000, y: H - 32, w: 600, h: 32 },
{ x: 1700, y: H - 32, w: 400, h: 32 },
{ x: 2250, y: H - 32, w: 1200, h: 32 },
{ x: 3600, y: H - 32, w: 1200, h: 32 },
// Raised platforms
{ x: 320, y: 320, w: 128, h: 16 },
{ x: 520, y: 240, w: 128, h: 16 },
{ x: 720, y: 176, w: 96, h: 16 },
{ x: 1100, y: 300, w: 160, h: 16 },
{ x: 1320, y: 224, w: 128, h: 16 },
{ x: 1800, y: 300, w: 96, h: 16 },
{ x: 1960, y: 220, w: 96, h: 16 },
{ x: 2100, y: 152, w: 96, h: 16 },
{ x: 2400, y: 280, w: 160, h: 16 },
{ x: 2640, y: 200, w: 128, h: 16 },
{ x: 2880, y: 140, w: 128, h: 16 },
{ x: 3700, y: 300, w: 128, h: 16 },
{ x: 3900, y: 224, w: 128, h: 16 },
{ x: 4100, y: 152, w: 160, h: 16 },
];
/* Coins – {x, y} (radius 10, shown as 20×20 rect) */
const coins = [
{ x: 350, y: 288, taken: false },
{ x: 400, y: 288, taken: false },
{ x: 560, y: 208, taken: false },
{ x: 610, y: 208, taken: false },
{ x: 760, y: 144, taken: false },
{ x: 810, y: 144, taken: false },
{ x: 1140, y: 268, taken: false },
{ x: 1190, y: 268, taken: false },
{ x: 1360, y: 192, taken: false },
{ x: 1840, y: 268, taken: false },
{ x: 2000, y: 188, taken: false },
{ x: 2140, y: 120, taken: false },
{ x: 2440, y: 248, taken: false },
{ x: 2490, y: 248, taken: false },
{ x: 2680, y: 168, taken: false },
{ x: 2920, y: 108, taken: false },
{ x: 2970, y: 108, taken: false },
{ x: 3740, y: 268, taken: false },
{ x: 3940, y: 192, taken: false },
{ x: 4140, y: 120, taken: false },
{ x: 4200, y: 120, taken: false },
];
// Decorative elements
const clouds = [
{ x: 200, y: 60, s: 1.0 },
{ x: 600, y: 40, s: 0.7 },
{ x: 1050, y: 70, s: 0.9 },
{ x: 1500, y: 50, s: 0.8 },
{ x: 2000, y: 35, s: 1.1 },
{ x: 2500, y: 65, s: 0.6 },
{ x: 3000, y: 45, s: 1.0 },
{ x: 3500, y: 55, s: 0.85 },
{ x: 4000, y: 40, s: 0.95 },
{ x: 4500, y: 75, s: 0.7 },
];
const hills = [
{ x: 100, s: 1.2 },
{ x: 800, s: 0.8 },
{ x: 1600, s: 1.0 },
{ x: 2400, s: 1.3 },
{ x: 3200, s: 0.9 },
{ x: 4000, s: 1.1 },
];
const bushes = [
{ x: 450, s: 0.8 },
{ x: 1200, s: 1.0 },
{ x: 1900, s: 0.7 },
{ x: 2700, s: 0.9 },
{ x: 3400, s: 0.8 },
];
/* Pipe */
const pipe = { x: 1550, y: H - 32 - 56, w: 48, h: 56 };
/* Goal flag */
const flag = { x: 4600, y: H - 32 - 160, w: 8, h: 160 };
/* ================================================================
PLAYER
================================================================ */
const player = {
x: 60,
y: H - 32 - 48,
w: 24,
h: 40,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
walkFrame: 0,
walkTimer: 0,
alive: true,
won: false,
};
let score = 0;
let totalCoins = coins.length;
let camera = { x: 0 };
let animFrame = 0; // for coin shimmer etc.
let winTimer = 0; // countdown after winning
function resetPlayer() {
player.x = 60;
player.y = H - 32 - 48;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.alive = false;
player.won = false;
player.facing = 1;
winTimer = 0;
for (let i = 0; i < coins.length; i++) coins[i].taken = false;
score = 0;
}
/* ================================================================
GAME LOOP
================================================================ */
function update() {
if (player.won) {
winTimer++;
return;
}
if (!player.alive) {
// Death animation / waiting — just let gravity pull them down
player.vy += GRAVITY;
player.y += player.vy;
return;
}
/* --- input --- */
var left = keys['ArrowLeft'] || keys['KeyA'];
var right = keys['ArrowRight'] || keys['KeyD'];
var jump = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
/* horizontal movement */
if (left) {
player.vx -= 0.8;
player.facing = -1;
}
if (right) {
player.vx += 0.8;
player.facing = 1;
}
if (!left && !right) {
player.vx *= player.onGround ? FRICTION : AIR_FRICTION;
}
// Clamp horizontal speed
if (player.vx > PLAYER_SPEED) player.vx = PLAYER_SPEED;
if (player.vx < -PLAYER_SPEED) player.vx = -PLAYER_SPEED;
// Small dead-zone to stop completely
if (Math.abs(player.vx) < 0.1) player.vx = 0;
/* walking 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 if (!player.onGround) {
player.walkFrame = 1; // hang in air
} else {
player.walkFrame = 0;
player.walkTimer = 0;
}
/* jump */
if (jump && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
/* gravity */
player.vy += GRAVITY;
if (player.vy > 14) player.vy = 14; // terminal velocity
/* --- horizontal collision --- */
player.x += player.vx;
if (player.x < 0) player.x = 0;
if (player.x + player.w > LEVEL_WIDTH) player.x = LEVEL_WIDTH - player.w;
for (var i = 0; i < platforms.length; i++) {
if (rectOverlap(player, platforms[i])) {
if (player.vx > 0) {
player.x = platforms[i].x - player.w;
} else if (player.vx < 0) {
player.x = platforms[i].x + platforms[i].w;
}
player.vx = 0;
}
}
/* --- vertical collision --- */
player.y += player.vy;
player.onGround = false;
for (var j = 0; j < platforms.length; j++) {
if (rectOverlap(player, platforms[j])) {
if (player.vy > 0) {
// Falling — land on top
player.y = platforms[j].y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
// Jumping up — hit platform bottom
player.y = platforms[j].y + platforms[j].h;
player.vy = 0;
}
}
}
/* fall off the world */
if (player.y > H + 64) {
player.alive = false;
}
/* --- coin collection --- */
for (var c = 0; c < coins.length; c++) {
if (!coins[c].taken) {
var cr = coins[c];
// Check if player overlaps the coin rect (coin is 20×20)
var coinRect = { x: cr.x - 10, y: cr.y - 10, w: 20, h: 20 };
if (rectOverlap(player, coinRect)) {
coins[c].taken = true;
score++;
}
}
}
/* --- flag (goal) --- */
var flagRect = { x: flag.x - 4, y: flag.y, w: flag.w + 8, h: flag.h };
if (rectOverlap(player, flagRect)) {
player.won = true;
winTimer = 0;
}
/* --- camera --- */
var target = player.x - W / 2 + player.w / 2;
if (target < 0) target = 0;
if (target + W > LEVEL_WIDTH) target = LEVEL_WIDTH - W;
// Smooth scroll
camera.x += (target - camera.x) * 0.12;
}
/* ================================================================
DRAWING
================================================================ */
function drawBackground() {
// Sky gradient
var grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#5c94fc');
grad.addColorStop(1, '#98c4ff');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Hills (parallax)
for (var i = 0; i < hills.length; i++) {
var h = hills[i];
var hx = h.x - camera.x * 0.5;
if (hx < -200 || hx > W + 200) continue;
ctx.fillStyle = C.hill;
ctx.beginPath();
ctx.arc(hx, H - 32, 80 * h.s, Math.PI, 0);
ctx.fill();
ctx.fillStyle = C.hillDark;
ctx.beginPath();
ctx.arc(hx + 20 * h.s, H - 32, 50 * h.s, Math.PI, 0);
ctx.fill();
}
// Clouds (parallax)
for (var ci = 0; ci < clouds.length; ci++) {
var cl = clouds[ci];
var cx = cl.x - camera.x * 0.3;
if (cx < -120 || cx > W + 120) continue;
var s = cl.s;
ctx.fillStyle = C.cloud;
ctx.beginPath();
ctx.arc(cx, cl.y, 18 * s, 0, Math.PI * 2);
ctx.arc(cx + 22 * s, cl.y - 6 * s, 14 * s, 0, Math.PI * 2);
ctx.arc(cx + 40 * s, cl.y + 2 * s, 16 * s, 0, Math.PI * 2);
ctx.fill();
}
// Bushes (parallax)
for (var bi = 0; bi < bushes.length; bi++) {
var b = bushes[bi];
var bx = b.x - camera.x * 0.7;
if (bx < -80 || bx > W + 80) continue;
ctx.fillStyle = C.bush;
ctx.beginPath();
ctx.arc(bx, H - 32, 14 * b.s, 0, Math.PI * 2);
ctx.arc(bx + 18 * b.s, H - 32, 12 * b.s, 0, Math.PI * 2);
ctx.arc(bx - 14 * b.s, H - 32, 10 * b.s, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPlatforms() {
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
var px = p.x - camera.x;
// Off-screen skip
if (px + p.w < -10 || px > W + 10) continue;
// Top border (grass-like)
ctx.fillStyle = C.platformT;
ctx.fillRect(px, p.y, p.w, Math.min(6, p.h));
// Main body
ctx.fillStyle = C.platform;
ctx.fillRect(px, p.y + 6, p.w, p.h - 6);
// Brick pattern for wider platforms
if (p.h <= 16) {
ctx.strokeStyle = C.brickLine;
ctx.lineWidth = 1;
for (var bx = 0; bx < p.w; bx += 32) {
ctx.beginPath();
ctx.moveTo(px + bx, p.y + 6);
ctx.lineTo(px + bx, p.y + p.h);
ctx.stroke();
}
// Horizontal mortar
if (p.h > 16) {
ctx.beginPath();
ctx.moveTo(px, p.y + p.h / 2);
ctx.lineTo(px + p.w, p.y + p.h / 2);
ctx.stroke();
}
}
}
}
function drawPipe() {
var px = pipe.x - camera.x;
if (px + pipe.w < -10 || px > W + 10) return;
// Pipe body
ctx.fillStyle = C.pipe;
ctx.fillRect(px + 4, pipe.y + 20, pipe.w - 8, pipe.h - 20);
// Pipe top rim
ctx.fillStyle = C.pipeDark;
ctx.fillRect(px, pipe.y, pipe.w, 20);
ctx.fillStyle = C.pipe;
ctx.fillRect(px + 2, pipe.y + 2, pipe.w - 4, 16);
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(px + 8, pipe.y + 4, 6, 14);
}
function drawCoins() {
var t = Date.now() / 200;
for (var i = 0; i < coins.length; i++) {
if (coins[i].taken) continue;
var c = coins[i];
var cx = c.x - camera.x;
if (cx < -20 || cx > W + 20) continue;
var stretch = 0.5 + 0.5 * Math.sin(t + i * 1.7);
var cw = 18 * stretch;
var ch = 18;
ctx.fillStyle = C.coin;
ctx.beginPath();
ctx.ellipse(cx, c.y, cw / 2, ch / 2, 0, 0, Math.PI * 2);
ctx.fill();
// Inner detail
ctx.fillStyle = C.coinDark;
ctx.beginPath();
ctx.ellipse(cx, c.y, cw / 3, ch / 3, 0, 0, Math.PI * 2);
ctx.fill();
// Shimmer
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.beginPath();
ctx.ellipse(cx - cw * 0.15, c.y - ch * 0.2, cw * 0.1, ch * 0.12, -0.3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawFlag() {
var fx = flag.x - camera.x;
if (fx < -20 || fx > W + 20) return;
// Pole
ctx.fillStyle = C.flagPole;
ctx.fillRect(fx, flag.y, flag.w, flag.h);
// Ball on top
ctx.fillStyle = '#fc0';
ctx.beginPath();
ctx.arc(fx + flag.w / 2, flag.y - 2, 6, 0, Math.PI * 2);
ctx.fill();
// Flag cloth
var flap = Math.sin(Date.now() / 300) * 4;
ctx.fillStyle = C.flag;
ctx.beginPath();
ctx.moveTo(fx + flag.w, flag.y + 4);
ctx.lineTo(fx + flag.w + 36 + flap, flag.y + 16);
ctx.lineTo(fx + flag.w + 34 + flap, flag.y + 40);
ctx.lineTo(fx + flag.w, flag.y + 44);
ctx.closePath();
ctx.fill();
// Flag star
ctx.fillStyle = '#fff';
ctx.font = '12px sans-serif';
ctx.fillText('★', fx + flag.w + 12 + flap * 0.5, flag.y + 30);
}
function drawPlayer() {
if (!player.alive && !player.won) return;
var px = player.x - camera.x;
var py = player.y;
var f = player.facing; // 1 = right, -1 = left
ctx.save();
if (f === -1) {
ctx.translate(px + player.w, py);
ctx.scale(-1, 1);
} else {
ctx.translate(px, py);
}
// Shadow under feet
if (player.onGround) {
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(player.w / 2 + 4, player.h + 2, 14, 4, 0, 0, Math.PI * 2);
ctx.fill();
}
// --- Body (overalls / shirt) ---
ctx.fillStyle = C.player;
ctx.fillRect(4, 14, 16, 18);
// --- Head ---
ctx.fillStyle = C.playerSkin;
ctx.fillRect(5, 0, 14, 14);
// --- Hat ---
ctx.fillStyle = C.playerHat;
ctx.fillRect(2, 0, 18, 5);
ctx.fillRect(0, 4, 22, 4);
// --- Eyes ---
ctx.fillStyle = '#000';
ctx.fillRect(12, 5, 3, 3);
// --- Mustache ---
ctx.fillStyle = '#3a1a00';
ctx.fillRect(10, 9, 9, 2);
// --- Pants ---
ctx.fillStyle = C.playerPants;
ctx.fillRect(4, 30, 16, 10);
// --- Shoes ---
ctx.fillStyle = '#6b3300';
if (player.onGround && Math.abs(player.vx) > 0.5) {
// Walk animation
var legOff = [0, 3, 0, -3][player.walkFrame];
ctx.fillRect(2, 38 + legOff, 8, 4);
ctx.fillRect(14, 38 - legOff, 8, 4);
} else if (!player.onGround) {
// Jump pose — legs apart
ctx.fillRect(0, 36, 8, 5);
ctx.fillRect(16, 34, 8, 5);
} else {
ctx.fillRect(2, 38, 8, 4);
ctx.fillRect(14, 38, 8, 4);
}
// --- Arms ---
ctx.fillStyle = C.playerSkin;
if (!player.onGround) {
// Arms up when jumping
ctx.fillRect(-2, 10, 5, 10);
ctx.fillRect(21, 8, 5, 10);
} else if (Math.abs(player.vx) > 0.5) {
// Swing arms while walking
var armSwing = [0, 4, 0, -4][player.walkFrame];
ctx.fillRect(-2, 16 + armSwing, 5, 10);
ctx.fillRect(21, 16 - armSwing, 5, 10);
} else {
ctx.fillRect(-2, 18, 5, 10);
ctx.fillRect(21, 18, 5, 10);
}
ctx.restore();
}
function drawHUD() {
// Score background
ctx.fillStyle = C.scoreBg;
ctx.beginPath();
ctx.roundRect(12, 8, 150, 30, 6);
ctx.fill();
// Coin icon
ctx.fillStyle = C.coin;
ctx.beginPath();
ctx.arc(28, 23, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = C.coinDark;
ctx.beginPath();
ctx.arc(28, 23, 4, 0, Math.PI * 2);
ctx.fill();
// Score text
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px "Segoe UI", Tahoma, sans-serif';
ctx.fillText('× ' + score, 42, 28);
// Total coins
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '13px "Segoe UI", Tahoma, sans-serif';
ctx.fillText(totalCoins + ' coins', 100, 28);
}
function drawDeathScreen() {
ctx.fillStyle = C.winBg;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#f00';
ctx.font = 'bold 48px "Segoe UI", Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Game Over', W / 2, H / 2 - 20);
ctx.fillStyle = '#fff';
ctx.font = '20px "Segoe UI", Tahoma, sans-serif';
ctx.fillText('Press R to restart', W / 2, H / 2 + 30);
ctx.textAlign = 'left';
if (keys['KeyR']) {
resetPlayer();
}
}
function drawWinScreen() {
ctx.fillStyle = C.winBg;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fc0';
ctx.font = 'bold 48px "Segoe UI", Tahoma, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('🎉 You Win! 🎉', W / 2, H / 2 - 50);
ctx.fillStyle = '#fff';
ctx.font = '28px "Segoe UI", Tahoma, sans-serif';
ctx.fillText('Coins: ' + score + ' / ' + totalCoins, W / 2, H / 2 + 10);
if (score >= totalCoins) {
ctx.fillStyle = '#0f0';
ctx.font = 'bold 22px "Segoe UI", Tahoma, sans-serif';
ctx.fillText('★ Perfect! All coins collected! ★', W / 2, H / 2 + 50);
}
ctx.fillStyle = '#aaa';
ctx.font = '18px "Segoe UI", Tahoma, sans-serif';
ctx.fillText('Press R to play again', W / 2, H / 2 + 90);
ctx.textAlign = 'left';
if (keys['KeyR']) {
resetPlayer();
}
}
/* ================================================================
MAIN LOOP
================================================================ */
function gameLoop() {
animFrame++;
update();
drawBackground();
drawPlatforms();
drawPipe();
drawCoins();
drawFlag();
drawPlayer();
drawHUD();
if (!player.alive && !player.won) {
drawDeathScreen();
}
if (player.won) {
drawWinScreen();
}
requestAnimationFrame(gameLoop);
}
/* roundRect polyfill for older browsers */
if (!ctx.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
if (typeof r === 'number') r = [r];
var rad = r[0] || 0;
this.moveTo(x + rad, y);
this.lineTo(x + w - rad, y);
this.quadraticCurveTo(x + w, y, x + w, y + rad);
this.lineTo(x + w, y + h - rad);
this.quadraticCurveTo(x + w, y + h, x + w - rad, y + h);
this.lineTo(x + rad, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - rad);
this.lineTo(x, y + rad);
this.quadraticCurveTo(x, y, x + rad, y);
this.closePath();
};
}
/* Start */
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: #222;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
canvas {
border: 2px solid #444;
display: block;
background: #5c94fc;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<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 Platformer — game.js
* Plain JS, no frameworks, no network requests.
* ============================================================ */
/* ───────── helpers ───────── */
function aabb(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
}
/* ───────── constants ───────── */
const CANVAS_W = 800, CANVAS_H = 450;
const TILE = 32;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_SPEED = 5;
const JUMP_FORCE = -11;
/* ───────── level map ─────────
* S = start, # = ground, B = block/platform, C = coin, F = flag/goal
* G = gap (no ground).
*/
const LEVEL = [
" ",
" ",
" ",
" ",
" ",
" C C C ",
" B B B B B B B B B C ",
" B ",
" C C C C B B F ",
" B B B B B B B B B C B C B B B B B B B ",
" B B C ",
" S G G G G G G G G G G G G G G G G G G G ",
"################################################################################",
"################################################################################",
];
/* ───────── state ───────── */
let canvas, ctx;
let camera = { x: 0, y: 0 };
let score = 0;
let won = false;
let particles = [];
const keys = {};
/* player object */
const player = {
x: 0, y: 0, w: 22, h: 30,
vx: 0, vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
frame: 0, // animation frame
frameTimer: 0,
};
let platforms = [];
let coins = [];
let flag = null;
let startPos = { x: 60, y: 300 };
/* ───────── build level ───────── */
function buildLevel() {
platforms = [];
coins = [];
flag = null;
for (let row = 0; row < LEVEL.length; row++) {
const line = LEVEL[row];
for (let col = 0; col < line.length; col++) {
const ch = line[col];
const x = col * TILE;
const y = row * TILE;
if (ch === '#') {
platforms.push({ x, y, w: TILE, h: TILE, type: 'ground' });
} else if (ch === 'B') {
platforms.push({ x, y, w: TILE, h: TILE, type: 'brick' });
} else if (ch === 'C') {
coins.push({ x: x + 8, y: y + 8, w: 16, h: 16, collected: false, bobPhase: Math.random() * Math.PI * 2 });
} else if (ch === 'F') {
flag = { x, y: y - TILE, w: 8, h: TILE * 2 };
} else if (ch === 'S') {
startPos = { x: x + 4, y: y - TILE };
}
}
}
player.x = startPos.x;
player.y = startPos.y;
player.vx = 0;
player.vy = 0;
}
/* ───────── particles ───────── */
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.8) * 5,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3,
});
}
}
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);
}
}
/* ───────── input ───────── */
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
e.preventDefault();
}
if (won && e.code === 'Space') {
resetGame();
}
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
/* ───────── update ───────── */
function update() {
if (won) return;
// movement
const left = keys['ArrowLeft'] || keys['KeyA'];
const right = keys['ArrowRight'] || keys['KeyD'];
const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
if (left) { player.vx -= 1.2; player.facing = -1; }
if (right) { player.vx += 1.2; player.facing = 1; }
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
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;
}
// gravity
player.vy += GRAVITY;
if (player.vy > 14) player.vy = 14; // terminal velocity
// horizontal movement + collision
player.x += player.vx;
for (const p of platforms) {
if (aabb(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;
}
}
// vertical movement + collision
player.y += player.vy;
player.onGround = false;
for (const p of platforms) {
if (aabb(player, p)) {
if (player.vy > 0) {
player.y = p.y - player.h;
player.onGround = true;
player.vy = 0;
// landing particles
if (player.vy > 3) spawnParticles(player.x + player.w / 2, player.y + player.h, '#ccc', 4);
} else if (player.vy < 0) {
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// prevent going off the left side
if (player.x < 0) { player.x = 0; player.vx = 0; }
// fall off the world — respawn
if (player.y > LEVEL.length * TILE + 200) {
player.x = startPos.x;
player.y = startPos.y;
player.vx = 0;
player.vy = 0;
}
// collect coins
for (const c of coins) {
if (c.collected) continue;
c.bobPhase += 0.06;
const bobY = c.y + Math.sin(c.bobPhase) * 3;
if (aabb(player, { x: c.x, y: bobY, w: c.w, h: c.h })) {
c.collected = true;
score++;
spawnParticles(c.x + c.w / 2, bobY + c.h / 2, '#FFD700', 10);
}
}
// flag / win
if (flag && aabb(player, { x: flag.x, y: flag.y, w: flag.w, h: flag.h })) {
won = true;
spawnParticles(flag.x + flag.w / 2, flag.y + flag.h / 2, '#00FF00', 30);
spawnParticles(flag.x + flag.w / 2, flag.y + flag.h / 2, '#FFD700', 20);
}
// animation
player.frameTimer++;
if (player.frameTimer > 8) {
player.frame = (player.frame + 1) % 4;
player.frameTimer = 0;
}
// camera
const targetX = player.x + player.w / 2 - CANVAS_W / 2;
camera.x += (targetX - camera.x) * 0.1;
if (camera.x < 0) camera.x = 0;
updateParticles();
}
/* ───────── drawing helpers ───────── */
function drawRoundRect(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();
}
/* ───────── draw ───────── */
function draw() {
// sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, CANVAS_H);
skyGrad.addColorStop(0, '#5C94FC');
skyGrad.addColorStop(0.7, '#87CEEB');
skyGrad.addColorStop(1, '#B0E0FF');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// clouds (parallax — move slower)
ctx.fillStyle = 'rgba(255,255,255,0.85)';
const cloudOffset = -camera.x * 0.3;
for (let i = 0; i < 8; i++) {
const cx = ((i * 220 + 50) + cloudOffset) % (LEVEL.length * TILE + 400) - 200;
const cy = 40 + (i % 3) * 50;
ctx.beginPath();
ctx.arc(cx, cy, 25, 0, Math.PI * 2);
ctx.arc(cx + 25, cy - 8, 20, 0, Math.PI * 2);
ctx.arc(cx + 50, cy, 22, 0, Math.PI * 2);
ctx.arc(cx + 20, cy + 5, 18, 0, Math.PI * 2);
ctx.fill();
}
// hills (parallax — medium speed)
ctx.fillStyle = '#3DA34A';
const hillOffset = -camera.x * 0.5;
for (let i = 0; i < 6; i++) {
const hx = ((i * 350 + 100) + hillOffset) % (LEVEL.length * TILE + 500) - 250;
const hy = LEVEL.length * TILE - 32;
ctx.beginPath();
ctx.ellipse(hx, hy, 120 + (i % 2) * 40, 60 + (i % 3) * 20, 0, Math.PI, 0);
ctx.fill();
}
ctx.save();
ctx.translate(-Math.round(camera.x), 0);
// platforms
for (const p of platforms) {
if (p.x + p.w < camera.x - TILE || p.x > camera.x + CANVAS_W + TILE) continue;
if (p.type === 'ground') {
// green top
ctx.fillStyle = '#4CAF50';
ctx.fillRect(p.x, p.y, p.w, 6);
// brown dirt
ctx.fillStyle = '#8B5E3C';
ctx.fillRect(p.x, p.y + 6, p.w, p.h - 6);
// dirt speckles
ctx.fillStyle = '#7A5230';
ctx.fillRect(p.x + 4, p.y + 12, 4, 4);
ctx.fillRect(p.x + 18, p.y + 20, 3, 3);
} else {
// brick
ctx.fillStyle = '#C85A3C';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#A84430';
ctx.fillRect(p.x + 1, p.y + 1, p.w - 2, 2);
ctx.fillRect(p.x + 1, p.y + p.h / 2, p.w - 2, 2);
ctx.fillRect(p.x + p.w / 2 - 1, p.y, 2, p.h);
}
}
// coins
for (const c of coins) {
if (c.collected) continue;
const bobY = c.y + Math.sin(c.bobPhase) * 3;
// spin effect
const scaleX = Math.abs(Math.cos(c.bobPhase * 0.7));
ctx.save();
ctx.translate(c.x + c.w / 2, bobY + c.h / 2);
ctx.scale(scaleX || 0.1, 1);
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fillStyle = '#FFD700';
ctx.fill();
ctx.strokeStyle = '#DAA520';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
// flag
if (flag) {
// pole
ctx.fillStyle = '#888';
ctx.fillRect(flag.x, flag.y, 4, flag.h);
// ball on top
ctx.beginPath();
ctx.arc(flag.x + 2, flag.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#FFD700';
ctx.fill();
// flag cloth
const flagWave = Math.sin(Date.now() / 200) * 3;
ctx.beginPath();
ctx.moveTo(flag.x + 4, flag.y + 4);
ctx.lineTo(flag.x + 34 + flagWave, flag.y + 14);
ctx.lineTo(flag.x + 4, flag.y + 28);
ctx.closePath();
ctx.fillStyle = '#E53935';
ctx.fill();
ctx.strokeStyle = '#B71C1C';
ctx.lineWidth = 1;
ctx.stroke();
// star on flag
ctx.fillStyle = '#FFD700';
ctx.font = '10px sans-serif';
ctx.fillText('★', flag.x + 12 + flagWave * 0.5, flag.y + 21);
}
// player
drawPlayer();
// particles
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
ctx.restore();
// HUD
drawHUD();
}
/* ───────── draw player character ───────── */
function drawPlayer() {
const px = Math.round(player.x);
const py = Math.round(player.y);
const f = player.facing;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
ctx.scale(f, 1);
ctx.translate(-(player.w / 2), -(player.h / 2));
// body (red shirt)
ctx.fillStyle = '#E53935';
drawRoundRect(2, 12, 18, 14, 3);
ctx.fill();
// head
ctx.fillStyle = '#FFCC80';
ctx.beginPath();
ctx.arc(11, 8, 8, 0, Math.PI * 2);
ctx.fill();
// hat
ctx.fillStyle = '#E53935';
ctx.beginPath();
ctx.ellipse(11, 3, 9, 5, 0, Math.PI, 0);
ctx.fill();
// hat brim
ctx.fillRect(2, 4, 18, 3);
// hat logo (white circle)
ctx.fillStyle = '#FFF';
ctx.beginPath();
ctx.arc(11, 2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#E53935';
ctx.beginPath();
ctx.arc(11, 2, 1.5, 0, Math.PI * 2);
ctx.fill();
// eyes
ctx.fillStyle = '#333';
ctx.fillRect(13, 6, 2, 3);
// mustache
ctx.fillStyle = '#5D4037';
ctx.fillRect(10, 10, 7, 2);
// legs
const legAnim = player.onGround ? Math.sin(player.frame * Math.PI / 2) * 4 : 3;
ctx.fillStyle = '#1565C0';
ctx.fillRect(3, 26, 7, 6);
ctx.fillRect(12, 26, 7, 6);
// shoes
ctx.fillStyle = '#5D4037';
const shoeOff = player.onGround ? legAnim : 0;
ctx.fillRect(1 - shoeOff, 29, 8, 3);
ctx.fillRect(13 + shoeOff, 29, 8, 3);
// arms
ctx.fillStyle = '#FFCC80';
const armSwing = player.onGround ? Math.sin(player.frame * Math.PI / 2) * 5 : -3;
ctx.fillRect(-2, 14 + armSwing, 5, 8);
ctx.fillRect(19, 14 - armSwing, 5, 8);
ctx.restore();
}
/* ───────── draw HUD ───────── */
function drawHUD() {
// semi-transparent bar
ctx.fillStyle = 'rgba(0,0,0,0.45)';
drawRoundRect(10, 8, 180, 36, 8);
ctx.fill();
// coin icon
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(28, 26, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#DAA520';
ctx.font = 'bold 11px sans-serif';
ctx.fillText('$', 24, 30);
// score
ctx.fillStyle = '#FFF';
ctx.font = 'bold 18px monospace';
ctx.fillText(score + ' / ' + coins.length, 46, 32);
// win overlay
if (won) {
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 48px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('🎉 YOU WIN! 🎉', CANVAS_W / 2, CANVAS_H / 2 - 20);
ctx.fillStyle = '#FFF';
ctx.font = '22px sans-serif';
ctx.fillText('Coins collected: ' + score + ' / ' + coins.length, CANVAS_W / 2, CANVAS_H / 2 + 25);
ctx.fillText('Press SPACE to play again', CANVAS_W / 2, CANVAS_H / 2 + 60);
ctx.textAlign = 'left';
}
}
/* ───────── reset ───────── */
function resetGame() {
score = 0;
won = false;
particles = [];
buildLevel();
}
/* ───────── game loop ───────── */
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
/* ───────── init ───────── */
window.addEventListener('load', () => {
canvas = document.getElementById('game');
if (!canvas) return;
ctx = canvas.getContext('2d');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
buildLevel();
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; }
html, body {
width: 100%;
height: 100%;
background: #1a1a2e;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#game {
border: 3px solid #333;
border-radius: 8px;
box-shadow: 0 0 30px rgba(92, 148, 252, 0.3);
display: block;
image-rendering: pixelated;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<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 Pixel Platformer — game.js
Plain vanilla JS, no frameworks, no external assets.
================================================================ */
(() => {
"use strict";
// ─── Canvas & Context ──────────────────────────────────────────
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width; // 800
const H = canvas.height; // 450
// ─── Constants ─────────────────────────────────────────────────
const TILE = 32;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MOVE_ACCEL = 0.6;
const MAX_SPEED = 4.5;
const JUMP_VEL = -11.5;
const BOUNCE_VEL = -7; // velocity on enemy stomp
// ─── Level map (characters → meaning) ─────────────────────────
// g = ground, b = brick, q = question block, c = coin
// e = enemy, f = flagpole, x = flag head, p = pipe top
// n = pipe body, P = platform (floating), s = stone step
// # = wall (right edge), . = empty
const LEVEL_RAW = [
"............................................................................................................................",
"............................................................................................................................",
"............................................................................................................................",
"............................................................................................................................",
"............................................................................................................................",
"...............................................................cccc.........................................................",
"............................................................................................................................",
"...................bqbbqbb......................................P.....P......PP..PP............................................",
"............................................................................................................................",
"...........cc..................................cc.................b.....b............................................b.......",
"............................................................................................................................",
"..........bbbbb................................bbbb.........e...P.....P......PP..PP.........................................",
"............................................................................................................................",
"....................................bqbbqbb.......e.....................b..b.................................bbbbbbb.......",
"............................................................................................................................",
"............................................................................................................................",
"..................................................nn..........................................nn................................",
"..................................................pp....e...........e....pp..........e........................pp................",
"##############################################################################################################",
];
// Derived level dimensions
const COLS = LEVEL_RAW[0].length; // 120
const ROWS = LEVEL_RAW.length; // 18
const LEVEL_W = COLS * TILE;
const LEVEL_H = ROWS * TILE;
// ─── Build world objects ───────────────────────────────────────
let blocks = []; // {x, y, type, hit}
let coins = []; // {x, y, collected}
let enemies = []; // {x, y, vx, type, alive, squishTimer}
let particles = []; // visual juice
let score = 0;
let flagX = -1; // x coordinate of the flagpole
let levelOver = false;
function buildLevel() {
blocks = [];
coins = [];
enemies = [];
particles = [];
score = 0;
flagX = -1;
levelOver = false;
for (let r = 0; r < ROWS; r++) {
const row = LEVEL_RAW[r];
for (let c = 0; c < row.length; c++) {
const ch = row[c];
const x = c * TILE;
const y = r * TILE;
if (ch === "b") {
blocks.push({ x, y, type: "brick", hit: false });
} else if (ch === "q") {
blocks.push({ x, y, type: "question", hit: false });
} else if (ch === "p") {
blocks.push({ x, y, type: "pipe" });
} else if (ch === "n") {
blocks.push({ x, y, type: "pipeBody" });
} else if (ch === "P") {
blocks.push({ x, y, type: "platform" });
} else if (ch === "s") {
blocks.push({ x, y, type: "stone" });
} else if (ch === "c") {
coins.push({ x: x + 8, y: y + 4, w: 16, h: 24, collected: false, bobOffset: Math.random() * Math.PI * 2 });
} else if (ch === "e") {
enemies.push({ x, y, w: TILE, h: TILE, vx: -1.2, vy: 0, type: "goomba", alive: true, squishTimer: 0 });
} else if (ch === "f") {
blocks.push({ x, y, type: "flagpole" });
} else if (ch === "x") {
blocks.push({ x, y, type: "flag" });
flagX = x;
}
}
}
}
// ─── Player ────────────────────────────────────────────────────
const player = {
x: 80, y: 0, // start pos
w: 22, h: 30,
vx: 0, vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
walkFrame: 0,
walkTimer: 0,
dead: false,
deathTimer: 0,
won: false,
winTimer: 0,
};
// Camera
const camera = { x: 0, y: 0 };
// ─── Input ─────────────────────────────────────────────────────
const keys = {};
document.addEventListener("keydown", (e) => {
keys[e.code] = true;
// Prevent scrolling
if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
e.preventDefault();
}
});
document.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 aabb(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
// Get all solid blocks
function getSolids() {
return blocks.filter(b => b.type !== "flag" && b.type !== "flagpole");
}
// ─── Particle effects ──────────────────────────────────────────
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 5,
vy: -Math.random() * 6 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3,
});
}
}
function spawnCoinParticles(x, y) {
spawnParticles(x, y, "#FFD700", 8);
}
function spawnBrickParticles(x, y) {
const colors = ["#C84C09", "#E09050", "#A06030"];
for (let i = 0; i < 10; i++) {
particles.push({
x: x + Math.random() * TILE,
y: y + Math.random() * TILE,
vx: (Math.random() - 0.5) * 7,
vy: -Math.random() * 8 - 3,
life: 40 + Math.random() * 20,
maxLife: 60,
color: colors[Math.floor(Math.random() * colors.length)],
size: 3 + Math.random() * 4,
});
}
}
// ─── Update ────────────────────────────────────────────────────
function update() {
if (player.dead) {
player.deathTimer++;
player.vy += GRAVITY;
player.y += player.vy;
if (player.deathTimer > 90) {
// Restart
player.x = 80;
player.y = 200;
player.vx = 0;
player.vy = 0;
player.dead = false;
player.deathTimer = 0;
player.onGround = false;
camera.x = 0;
buildLevel();
}
return;
}
if (player.won) {
player.winTimer++;
return;
}
// ── Horizontal movement ──
if (isLeft()) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
}
if (isRight()) {
player.vx += MOVE_ACCEL;
player.facing = 1;
}
// Clamp speed
player.vx = Math.max(-MAX_SPEED, Math.min(MAX_SPEED, player.vx));
// Friction
if (!isLeft() && !isRight()) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Walk animation
if (player.onGround && Math.abs(player.vx) > 0.5) {
player.walkTimer += Math.abs(player.vx);
if (player.walkTimer > 8) {
player.walkTimer = 0;
player.walkFrame = (player.walkFrame + 1) % 4;
}
} else if (player.onGround) {
player.walkFrame = 0;
}
// Apply horizontal velocity
player.x += player.vx;
// Horizontal collision with solids
const solids = getSolids();
for (const b of solids) {
if (aabb(player.x, player.y, player.w, player.h, b.x, b.y, TILE, TILE)) {
if (player.vx > 0) {
player.x = b.x - player.w;
} else if (player.vx < 0) {
player.x = b.x + TILE;
}
player.vx = 0;
}
}
// ── Vertical movement ──
player.vy += GRAVITY;
if (player.vy > 12) player.vy = 12;
// Jump
if (isJump() && player.onGround) {
player.vy = JUMP_VEL;
player.onGround = false;
}
player.y += player.vy;
player.onGround = false;
// Vertical collision with solids
for (const b of solids) {
if (aabb(player.x, player.y, player.w, player.h, b.x, b.y, TILE, TILE)) {
if (player.vy > 0) {
// Landing on top
player.y = b.y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
// Hitting from below
player.y = b.y + TILE;
player.vy = 0;
// Hit question block or brick
if (b.type === "question" && !b.hit) {
b.hit = true;
score += 50;
spawnCoinParticles(b.x + 8, b.y);
// Spawn floating coin text
particles.push({
x: b.x + 8, y: b.y - 10,
vx: 0, vy: -1.5, life: 40, maxLife: 40,
color: "#FFD700", size: 0, text: "+50"
});
} else if (b.type === "brick") {
spawnBrickParticles(b.x, b.y);
// Break brick
b.type = "broken";
score += 10;
}
}
}
}
// ── Coins ──
for (const c of coins) {
if (c.collected) continue;
if (aabb(player.x, player.y, player.w, player.h, c.x, c.y, c.w, c.h)) {
c.collected = true;
score += 10;
spawnCoinParticles(c.x + 8, c.y + 12);
}
}
// ── Enemies ──
for (const e of enemies) {
if (!e.alive) {
if (e.squishTimer > 0) e.squishTimer--;
continue;
}
// Only update if near camera
if (Math.abs(e.x - camera.x) > W + 200) continue;
e.vy += GRAVITY;
e.x += e.vx;
e.y += e.vy;
// Ground collision for enemies
let enemyOnGround = false;
for (const b of solids) {
if (b.type === "flag" || b.type === "flagpole") continue;
if (aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
if (e.vy > 0) {
e.y = b.y - e.h;
e.vy = 0;
enemyOnGround = true;
} else if (e.vy < 0) {
e.y = b.y + TILE;
e.vy = 0;
}
if (e.vx > 0 && aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
e.x = b.x - e.w;
e.vx = -e.vx;
} else if (e.vx < 0 && aabb(e.x, e.y, e.w, e.h, b.x, b.y, TILE, TILE)) {
e.x = b.x + TILE;
e.vx = -e.vx;
}
}
}
// Enemy-player collision
if (aabb(player.x, player.y, player.w, player.h, e.x + 2, e.y + 2, e.w - 4, e.h - 4)) {
if (player.vy > 0 && player.y + player.h - e.y < 16) {
// Stomp!
e.alive = false;
e.squishTimer = 30;
player.vy = BOUNCE_VEL;
score += 100;
spawnParticles(e.x + e.w / 2, e.y + e.h / 2, "#CC4400", 6);
particles.push({
x: e.x + e.w / 2, y: e.y - 5,
vx: 0, vy: -2, life: 40, maxLife: 40,
color: "#FFF", size: 0, text: "+100"
});
} else if (!player.dead) {
// Player hit by enemy
playerDie();
}
}
}
// ── Flag / Goal ──
if (flagX > 0) {
const poleX = flagX;
const poleTop = blocks.find(b => b.type === "flag")?.y ?? 0;
if (player.x + player.w > poleX && player.x < poleX + TILE && player.y + player.h > poleTop) {
player.won = true;
player.winTimer = 0;
score += 500;
// Big celebration
for (let i = 0; i < 30; i++) {
setTimeout(() => {
spawnParticles(
poleX + Math.random() * TILE,
poleTop + Math.random() * (LEVEL_H - poleTop),
["#FF0", "#F0F", "#0FF", "#F00", "#0F0"][Math.floor(Math.random() * 5)],
3
);
}, i * 50);
}
}
}
// ── Fall death ──
if (player.y > LEVEL_H + 64) {
playerDie();
}
// ── 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);
}
// ── Camera ──
const targetX = player.x - W / 3;
camera.x += (targetX - camera.x) * 0.1;
camera.x = Math.max(0, Math.min(LEVEL_W - W, camera.x));
}
function playerDie() {
if (player.dead) return;
player.dead = true;
player.deathTimer = 0;
player.vy = -10;
player.vx = 0;
}
// ─── Drawing ───────────────────────────────────────────────────
// Pre-render sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, "#5C94FC");
skyGrad.addColorStop(0.7, "#87CEEB");
skyGrad.addColorStop(1, "#B0E0FF");
function drawCloud(cx, cy, scale) {
const s = scale || 1;
ctx.fillStyle = "rgba(255,255,255,0.9)";
ctx.beginPath();
ctx.arc(cx, cy, 18 * s, 0, Math.PI * 2);
ctx.arc(cx + 18 * s, cy - 6 * s, 14 * s, 0, Math.PI * 2);
ctx.arc(cx + 34 * s, cy, 16 * s, 0, Math.PI * 2);
ctx.arc(cx + 16 * s, cy + 4 * s, 12 * s, 0, Math.PI * 2);
ctx.fill();
}
function drawHill(baseX, baseY, width, height) {
ctx.fillStyle = "#5DA84A";
ctx.beginPath();
ctx.moveTo(baseX - width / 2, baseY);
ctx.quadraticCurveTo(baseX, baseY - height, baseX + width / 2, baseY);
ctx.fill();
// Highlight
ctx.fillStyle = "#6DC05A";
ctx.beginPath();
ctx.moveTo(baseX - width / 4, baseY);
ctx.quadraticCurveTo(baseX, baseY - height, baseX + width / 4, baseY);
ctx.fill();
}
function drawBrickBlock(bx, by) {
ctx.fillStyle = "#C84C09";
ctx.fillRect(bx, by, TILE, TILE);
// Brick pattern
ctx.strokeStyle = "#8B3000";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
ctx.beginPath();
ctx.moveTo(bx, by + TILE / 2);
ctx.lineTo(bx + TILE, by + TILE / 2);
ctx.moveTo(bx + TILE / 2, by);
ctx.lineTo(bx + TILE / 2, by + TILE / 2);
ctx.moveTo(bx + TILE / 4, by + TILE / 2);
ctx.lineTo(bx + TILE / 4, by + TILE);
ctx.moveTo(bx + TILE * 3 / 4, by + TILE / 2);
ctx.lineTo(bx + TILE * 3 / 4, by + TILE);
ctx.stroke();
// Highlight
ctx.fillStyle = "rgba(255,255,255,0.15)";
ctx.fillRect(bx + 1, by + 1, TILE - 2, 3);
}
function drawQuestionBlock(bx, by, hit) {
if (hit) {
ctx.fillStyle = "#8B6914";
ctx.fillRect(bx, by, TILE, TILE);
ctx.strokeStyle = "#6B4F10";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
} else {
// Animated shimmer
const shimmer = Math.sin(Date.now() / 300) * 0.15 + 0.85;
ctx.fillStyle = `rgb(${Math.floor(255 * shimmer)}, ${Math.floor(200 * shimmer)}, 0)`;
ctx.fillRect(bx, by, TILE, TILE);
ctx.strokeStyle = "#B8860B";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
// Question mark
ctx.fillStyle = "#FFF";
ctx.font = "bold 18px 'Courier New', monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", bx + TILE / 2, by + TILE / 2 + 1);
}
}
function drawPipe(bx, by) {
// Pipe body
ctx.fillStyle = "#2E8B2E";
ctx.fillRect(bx + 2, by, TILE - 4, TILE);
// Highlight
ctx.fillStyle = "#4CBB3D";
ctx.fillRect(bx + 4, by, 6, TILE);
// Outline
ctx.strokeStyle = "#1A5C1A";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
}
function drawPipeTop(bx, by) {
ctx.fillStyle = "#2E8B2E";
ctx.fillRect(bx - 2, by, TILE + 4, TILE);
ctx.fillStyle = "#4CBB3D";
ctx.fillRect(bx + 2, by, 8, TILE);
ctx.strokeStyle = "#1A5C1A";
ctx.lineWidth = 1;
ctx.strokeRect(bx - 2.5, by + 0.5, TILE + 3, TILE - 1);
// Highlight line
ctx.fillStyle = "#6DD66D";
ctx.fillRect(bx + 12, by + 2, 4, TILE - 4);
}
function drawPlatform(bx, by) {
ctx.fillStyle = "#A0522D";
ctx.fillRect(bx, by, TILE, TILE * 0.6);
ctx.fillStyle = "#6B8E23";
ctx.fillRect(bx, by, TILE, 4);
ctx.strokeStyle = "#654321";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE * 0.6 - 1);
}
function drawStone(bx, by) {
ctx.fillStyle = "#808080";
ctx.fillRect(bx, by, TILE, TILE);
ctx.strokeStyle = "#606060";
ctx.lineWidth = 1;
ctx.strokeRect(bx + 0.5, by + 0.5, TILE - 1, TILE - 1);
ctx.fillStyle = "rgba(255,255,255,0.1)";
ctx.fillRect(bx + 1, by + 1, TILE - 2, 3);
}
function drawFlagpole(bx, by) {
ctx.fillStyle = "#666";
ctx.fillRect(bx + TILE / 2 - 2, by, 4, TILE);
// Ball on top
if (by < TILE * 2) {
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(bx + TILE / 2, by + 3, 5, 0, Math.PI * 2);
ctx.fill();
}
}
function drawFlag(bx, by) {
// Already drawn flagpole below
// Draw the flag cloth
const wave = Math.sin(Date.now() / 400) * 2;
ctx.fillStyle = "#E03030";
ctx.beginPath();
ctx.moveTo(bx + 2, by + 4);
ctx.lineTo(bx + TILE - 4 + wave, by + 8);
ctx.lineTo(bx + TILE - 2 + wave, by + 18);
ctx.lineTo(bx + 2, by + TILE - 4);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFD700";
ctx.font = "12px serif";
ctx.textAlign = "center";
ctx.fillText("★", bx + TILE / 2 + wave, by + 18);
}
function drawCoin(cx, cy, offset, time) {
const bob = Math.sin(time + offset) * 3;
const stretch = Math.abs(Math.sin(time * 3 + offset * 0.7));
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.ellipse(cx + 8, cy + 12 + bob, 6 * stretch + 1, 10, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFF8DC";
ctx.beginPath();
ctx.ellipse(cx + 6, cy + 10 + bob, 2 * stretch + 0.5, 4, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawPlayer(px, py) {
const p = player;
const f = p.facing;
// Body
ctx.fillStyle = "#E03030"; // Red shirt
ctx.fillRect(px + 4, py + 10, 14, 12);
// Head
ctx.fillStyle = "#FFB880"; // Skin
ctx.fillRect(px + 5, py + 2, 12, 10);
// Hat
ctx.fillStyle = "#E03030";
ctx.fillRect(px + 3, py, 14, 5);
ctx.fillRect(px + (f > 0 ? 10 : 2), py + 2, 10, 3);
// Eyes
ctx.fillStyle = "#000";
const eyeX = px + (f > 0 ? 13 : 6);
ctx.fillRect(eyeX, py + 5, 2, 3);
// Mustache
ctx.fillStyle = "#4A2800";
ctx.fillRect(px + 6, py + 9, 10, 2);
// Overalls
ctx.fillStyle = "#2050D0";
ctx.fillRect(px + 3, py + 18, 16, 8);
// Overalls buttons
ctx.fillStyle = "#FFD700";
ctx.fillRect(px + 7, py + 20, 2, 2);
ctx.fillRect(px + 13, py + 20, 2, 2);
// Legs
const legOffset = p.onGround ? Math.sin(p.walkFrame * Math.PI / 2) * 3 : 2;
ctx.fillStyle = "#2050D0";
ctx.fillRect(px + 3, py + 26, 7, 4 + legOffset);
ctx.fillRect(px + 12, py + 26, 7, 4 - legOffset);
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(px + 2, py + 28 + legOffset, 8, 2);
ctx.fillRect(px + 12, py + 28 - legOffset, 8, 2);
// Arms
ctx.fillStyle = "#FFB880";
if (p.onGround) {
const armSwing = Math.sin(p.walkFrame * Math.PI / 2) * 4;
ctx.fillRect(px + (f > 0 ? 16 : -2), py + 12 + armSwing, 4, 6);
ctx.fillRect(px + (f > 0 ? -2 : 16), py + 12 - armSwing, 4, 6);
} else {
// Jumping pose - arms up
ctx.fillRect(px + 2, py + 6, 4, 6);
ctx.fillRect(px + 16, py + 6, 4, 6);
}
}
function drawGoomba(gx, gy) {
// Body (mushroom shape)
ctx.fillStyle = "#A0522D";
ctx.beginPath();
ctx.arc(gx + 16, gy + 12, 13, Math.PI, 0);
ctx.fill();
// Head bottom
ctx.fillStyle = "#DEB887";
ctx.fillRect(gx + 4, gy + 16, 24, 12);
// Eyes
ctx.fillStyle = "#FFF";
ctx.fillRect(gx + 7, gy + 10, 6, 7);
ctx.fillRect(gx + 19, gy + 10, 6, 7);
ctx.fillStyle = "#000";
const pupilOffset = Math.sin(Date.now() / 200) * 1;
ctx.fillRect(gx + 10, gy + 12 + pupilOffset, 3, 4);
ctx.fillRect(gx + 21, gy + 12 + pupilOffset, 3, 4);
// Angry eyebrows
ctx.fillStyle = "#000";
ctx.fillRect(gx + 7, gy + 8, 7, 2);
ctx.fillRect(gx + 18, gy + 8, 7, 2);
// Feet
const walkBob = Math.sin(Date.now() / 100) * 2;
ctx.fillStyle = "#000";
ctx.fillRect(gx + 2, gy + 26 + walkBob, 8, 6);
ctx.fillRect(gx + 22, gy + 26 - walkBob, 8, 6);
}
function drawSquishedGoomba(gx, gy) {
ctx.fillStyle = "#A0522D";
ctx.fillRect(gx + 2, gy + 26, 28, 6);
ctx.fillStyle = "#000";
ctx.fillRect(gx + 7, gy + 27, 3, 3);
ctx.fillRect(gx + 19, gy + 27, 3, 3);
}
function drawHUD() {
// Top bar background
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(0, 0, W, 36);
// Coins
ctx.fillStyle = "#FFD700";
ctx.font = "bold 18px 'Courier New', monospace";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("🪙 " + score, 16, 18);
// Level info
ctx.fillStyle = "#FFF";
ctx.textAlign = "center";
ctx.fillText("WASD / Arrow Keys to move — Space / Up to jump", W / 2, 18);
// Enemies left
const aliveEnemies = enemies.filter(e => e.alive).length;
ctx.textAlign = "right";
ctx.fillStyle = "#FF6B6B";
ctx.fillText(`👾 × ${aliveEnemies}`, W - 16, 18);
}
function drawGameOver() {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFF";
ctx.font = "bold 48px 'Courier New', monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("GAME OVER", W / 2, H / 2 - 20);
ctx.font = "20px 'Courier New', monospace";
ctx.fillStyle = "#CCC";
ctx.fillText("Score: " + score, W / 2, H / 2 + 20);
ctx.fillText("Press SPACE to restart", W / 2, H / 2 + 55);
if (keys["Space"]) {
buildLevel();
player.x = 80;
player.y = 200;
player.vx = 0;
player.vy = 0;
player.dead = false;
camera.x = 0;
}
}
function drawWinScreen() {
const t = player.winTimer;
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 52px 'Courier New', monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🎉 YOU WIN! 🎉", W / 2, H / 2 - 40);
ctx.fillStyle = "#FFF";
ctx.font = "bold 28px 'Courier New', monospace";
ctx.fillText("Score: " + score, W / 2, H / 2 + 10);
ctx.font = "20px 'Courier New', monospace";
ctx.fillStyle = "#CCC";
ctx.fillText("Press SPACE to play again", W / 2, H / 2 + 50);
if (keys["Space"]) {
buildLevel();
player.x = 80;
player.y = 200;
player.vx = 0;
player.vy = 0;
player.won = false;
player.winTimer = 0;
camera.x = 0;
}
}
function render() {
const time = Date.now() / 1000;
const camX = Math.round(camera.x);
// Sky
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// ─── Background parallax layers ───
// Far hills
ctx.save();
ctx.translate(-Math.floor(camX * 0.15) % W, 0);
for (let i = -1; i < 3; i++) {
drawHill(i * 350 + 100, H - 40, 300, 100);
drawHill(i * 350 + 300, H - 40, 200, 70);
}
ctx.restore();
// Near hills
ctx.save();
ctx.translate(-Math.floor(camX * 0.35) % W, 0);
for (let i = -1; i < 4; i++) {
drawHill(i * 280 + 50, H - 20, 220, 60);
}
ctx.restore();
// Clouds
ctx.save();
ctx.translate(-Math.floor(camX * 0.1), 0);
drawCloud(100, 60, 1.2);
drawCloud(400, 40, 0.8);
drawCloud(700, 70, 1.0);
drawCloud(1050, 50, 1.3);
drawCloud(1400, 65, 0.9);
drawCloud(1750, 45, 1.1);
drawCloud(2100, 55, 1.0);
drawCloud(2500, 70, 1.2);
drawCloud(2900, 40, 0.8);
ctx.restore();
// ─── World ───
ctx.save();
ctx.translate(-camX, 0);
// Ground
ctx.fillStyle = "#5DA84A";
ctx.fillRect(0, (ROWS - 2) * TILE, LEVEL_W, TILE * 2);
// Ground detail
ctx.fillStyle = "#4A8A3A";
ctx.fillRect(0, (ROWS - 2) * TILE, LEVEL_W, 6);
// Dirt layers
ctx.fillStyle = "#8B6914";
for (let r = ROWS - 1; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const bx = c * TILE;
const by = r * TILE;
// Random dirt texture
if ((c + r * 7) % 5 === 0) {
ctx.fillStyle = "#7A5A10";
ctx.fillRect(bx + 4, by + 4, 8, 6);
}
if ((c * 3 + r * 11) % 7 === 0) {
ctx.fillStyle = "#9A7A20";
ctx.fillRect(bx + 18, by + 16, 10, 8);
}
}
}
// Blocks
for (const b of blocks) {
const bx = b.x, by = b.y;
// Only draw if on screen
if (bx + TILE < camX - 32 || bx > camX + W + 32) continue;
if (b.type === "brick") drawBrickBlock(bx, by);
else if (b.type === "question") drawQuestionBlock(bx, by, b.hit);
else if (b.type === "pipe") drawPipe(bx, by);
else if (b.type === "pipeBody") drawPipe(bx, by);
else if (b.type === "platform") drawPlatform(bx, by);
else if (b.type === "stone") drawStone(bx, by);
else if (b.type === "flagpole") drawFlagpole(bx, by);
else if (b.type === "flag") drawFlag(bx, by);
}
// Coins
for (const c of coins) {
if (c.collected) continue;
if (c.x + c.w < camX - 32 || c.x > camX + W + 32) continue;
drawCoin(c.x, c.y, c.bobOffset, time);
}
// Enemies
for (const e of enemies) {
if (e.x + e.w < camX - 64 || e.x > camX + W + 64) continue;
if (e.alive) {
drawGoomba(e.x, e.y);
} else if (e.squishTimer > 0) {
drawSquishedGoomba(e.x, e.y);
}
}
// Particles
for (const p of particles) {
const alpha = p.life / p.maxLife;
if (p.text) {
ctx.fillStyle = p.color;
ctx.globalAlpha = alpha;
ctx.font = "bold 14px 'Courier New', monospace";
ctx.textAlign = "center";
ctx.fillText(p.text, p.x, p.y);
ctx.globalAlpha = 1;
} else {
ctx.fillStyle = p.color;
ctx.globalAlpha = alpha;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
ctx.globalAlpha = 1;
}
}
// Player
if (!player.dead) {
drawPlayer(player.x, player.y);
} else {
// Dead player flying up
ctx.globalAlpha = 0.7;
drawPlayer(player.x, player.y);
ctx.globalAlpha = 1;
}
ctx.restore();
// ─── HUD ───
drawHUD();
// ─── Overlays ───
if (player.dead && player.deathTimer > 89) {
drawGameOver();
}
if (player.won) {
drawWinScreen();
}
}
// ─── Game Loop ─────────────────────────────────────────────────
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// ─── Init ──────────────────────────────────────────────────────
buildLevel();
// Place player on ground
player.y = (ROWS - 3) * TILE;
gameLoop();
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Pixel Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #000;
display: flex; justify-content: center; align-items: center;
overflow: hidden;
font-family: 'Courier New', monospace;
}
canvas {
display: block;
image-rendering: pixelated;
width: 800px; height: 450px;
border: 2px solid #333;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script src="game.js"></script>
</body>
</html>