screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Platformer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #2c2c2c;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
font-family: 'Segoe UI', Arial, sans-serif;
}
#game {
border: 3px solid #555;
border-radius: 4px;
display: block;
image-rendering: pixelated;
box-shadow: 0 0 30px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<script>
(function() {
"use strict";
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var W = canvas.width;
var H = canvas.height;
// ─── Constants ───────────────────────────────────────────
var GRAVITY = 0.55;
var FRICTION = 0.82;
var MOVE_ACCEL = 0.6;
var MAX_SPEED = 5.5;
var JUMP_FORCE = -11.5;
var TILE = 32;
var WORLD_W = 4800;
var WORLD_H = 600;
// ─── Input ───────────────────────────────────────────────
var keys = {};
window.addEventListener("keydown", function(e) {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
e.preventDefault();
}
});
window.addEventListener("keyup", function(e) {
keys[e.code] = false;
});
// ─── World objects ───────────────────────────────────────
// Platforms: {x, y, w, h}
var platforms = [];
// Coins: {x, y, collected}
var coins = [];
// Flag (goal)
var flag = { x: 0, y: 0 };
// Enemies: {x, y, w, h, speed, startX, range}
var enemies = [];
function buildLevel() {
platforms = [];
coins = [];
enemies = [];
// Ground segments
addGround(0, 850);
addGround(900, 600);
addGround(1600, 1200);
addGround(3000, 1800);
// Raised platforms
addPlatform(250, 340, 160, 20);
addPlatform(500, 270, 120, 20);
addPlatform(700, 200, 100, 20);
addPlatform(950, 310, 140, 20);
addPlatform(1150, 250, 100, 20);
addPlatform(1350, 190, 80, 20);
addPlatform(1700, 320, 150, 20);
addPlatform(1900, 250, 120, 20);
addPlatform(2100, 180, 100, 20);
addPlatform(2400, 300, 130, 20);
addPlatform(2600, 220, 100, 20);
addPlatform(2800, 160, 80, 20);
addPlatform(3100, 300, 150, 20);
addPlatform(3350, 230, 120, 20);
addPlatform(3600, 280, 100, 20);
addPlatform(3800, 200, 130, 20);
addPlatform(4000, 310, 100, 20);
// Coins scattered on platforms and ground
var coinPositions = [
// On raised platforms
270, 310, 290, 310, 310, 310,
520, 240, 540, 240, 560, 240,
720, 170, 740, 170,
970, 280, 990, 280, 1010, 280,
1170, 220, 1190, 220,
1370, 160,
1720, 290, 1740, 290, 1760, 290,
1920, 220, 1940, 220,
2120, 150,
2420, 270, 2440, 270, 2460, 270,
2620, 190,
2820, 130,
3120, 270, 3140, 270, 3160, 270,
3370, 200, 3390, 200,
3620, 250,
3820, 170, 3840, 170,
4020, 280,
// Ground coins
100, 410, 130, 410, 160, 410,
400, 410, 430, 410,
600, 410, 630, 410,
1000, 410, 1030, 410, 1060, 410,
1300, 410, 1330, 410,
1650, 410, 1680, 410, 1710, 410,
2000, 410, 2030, 410, 2060, 410,
2300, 410, 2330, 410,
2500, 410, 2530, 410, 2560, 410,
2900, 410, 2930, 410,
3200, 410, 3230, 410, 3260, 410,
3500, 410, 3530, 410,
3700, 410, 3730, 410, 3760, 410,
4100, 410, 4130, 410,
4400, 410, 4430, 410, 4460, 410,
];
for (var i = 0; i < coinPositions.length; i += 2) {
coins.push({ x: coinPositions[i], y: coinPositions[i+1], collected: false, w: 16, h: 16 });
}
// Flag at the end
flag = { x: 4550, y: 340 };
// Enemies (simple patrol bots)
enemies.push({ x: 400, y: 414, w: 28, h: 28, speed: 1.2, startX: 300, range: 200 });
enemies.push({ x: 1000, y: 414, w: 28, h: 28, speed: 1.5, startX: 920, range: 250 });
enemies.push({ x: 1800, y: 414, w: 28, h: 28, speed: 1.0, startX: 1650, range: 300 });
enemies.push({ x: 2200, y: 414, w: 28, h: 28, speed: 1.8, startX: 2100, range: 200 });
enemies.push({ x: 3200, y: 414, w: 28, h: 28, speed: 1.3, startX: 3050, range: 350 });
enemies.push({ x: 3900, y: 414, w: 28, h: 28, speed: 1.6, startX: 3750, range: 280 });
enemies.push({ x: 4300, y: 414, w: 28, h: 28, speed: 1.4, startX: 4150, range: 250 });
}
function addGround(sx, w) {
platforms.push({ x: sx, y: 420, w: w, h: 180 });
}
function addPlatform(x, y, w, h) {
platforms.push({ x: x, y: y, w: w, h: h });
}
// ─── Player ──────────────────────────────────────────────
var player = {
x: 80, y: 350,
w: 24, h: 32,
vx: 0, vy: 0,
onGround: false,
facing: 1,
score: 0,
lives: 3,
dead: false,
win: false,
respawnTimer: 0,
animFrame: 0,
animTimer: 0,
};
// ─── Camera ──────────────────────────────────────────────
var cam = { x: 0, y: 0 };
// ─── Particles ───────────────────────────────────────────
var particles = [];
function spawnParticles(x, y, color, count) {
for (var i = 0; i < count; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color: color,
size: 2 + Math.random() * 3,
});
}
}
// ─── Collision 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;
}
// ─── Physics update ──────────────────────────────────────
function updatePlayer() {
if (player.dead) {
player.respawnTimer--;
if (player.respawnTimer <= 0) {
respawnPlayer();
}
return;
}
if (player.win) return;
// Horizontal input
var moveLeft = keys["ArrowLeft"] || keys["KeyA"];
var moveRight = keys["ArrowRight"] || keys["KeyD"];
var jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
if (moveLeft) {
player.vx -= MOVE_ACCEL;
player.facing = -1;
}
if (moveRight) {
player.vx += MOVE_ACCEL;
player.facing = 1;
}
// Clamp horizontal speed
if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
// Friction when no input
if (!moveLeft && !moveRight) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Jump
if (jumpKey && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 14) player.vy = 14;
// Move X
player.x += player.vx;
player.onGround = false;
// Collide X with platforms
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
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;
}
}
// Move Y
player.y += player.vy;
// Collide Y with platforms
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (aabb(player, p)) {
if (player.vy > 0) {
player.y = p.y - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// World bounds
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x + player.w > WORLD_W) { player.x = WORLD_W - player.w; player.vx = 0; }
// Fall off screen
if (player.y > H + 50) {
killPlayer();
}
// Animation timer
player.animTimer++;
if (player.animTimer > 8) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
// Coin collection
for (var i = 0; i < coins.length; i++) {
var c = coins[i];
if (!c.collected) {
var coinBox = { x: c.x - 8, y: c.y - 8, w: 16, h: 16 };
if (aabb(player, coinBox)) {
c.collected = true;
player.score++;
spawnParticles(c.x, c.y, "#FFD700", 8);
}
}
}
// Flag check (win)
var flagBox = { x: flag.x - 10, y: flag.y - 100, w: 60, h: 120 };
if (aabb(player, flagBox)) {
player.win = true;
player.score += 10;
spawnParticles(flag.x + 20, flag.y - 50, "#00FF00", 20);
spawnParticles(flag.x + 20, flag.y - 50, "#FFD700", 20);
}
// Enemy collision
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (aabb(player, e)) {
// Check if player is landing on top
if (player.vy > 0 && player.y + player.h - e.y < 16) {
// Stomp enemy
player.vy = JUMP_FORCE * 0.6;
e.dead = true;
player.score += 5;
spawnParticles(e.x + e.w/2, e.y + e.h/2, "#FF4444", 10);
} else {
// Player dies
killPlayer();
}
}
}
}
function killPlayer() {
player.dead = true;
player.lives--;
player.respawnTimer = 60;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#FF0000", 15);
}
function respawnPlayer() {
player.dead = false;
player.x = 80;
player.y = 350;
player.vx = 0;
player.vy = 0;
player.onGround = false;
}
function updateEnemies() {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (e.dead) continue;
e.x += e.speed;
if (e.x > e.startX + e.range || e.x < e.startX) {
e.speed = -e.speed;
}
}
// Remove dead enemies after a delay
enemies = enemies.filter(function(e) { return !e.dead || (e.dead && !e._remove); });
}
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life--;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
function updateCamera() {
var targetX = player.x - W / 2 + player.w / 2;
var targetY = player.y - H / 2 + player.h / 2;
cam.x += (targetX - cam.x) * 0.1;
cam.y += (targetY - cam.y) * 0.08;
if (cam.x < 0) cam.x = 0;
if (cam.x > WORLD_W - W) cam.x = WORLD_W - W;
if (cam.y > 0) cam.y = 0;
if (cam.y < WORLD_H - H) cam.y = WORLD_H - H;
}
// ─── Drawing ─────────────────────────────────────────────
function drawBackground() {
// Sky gradient
var grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#5C94FC");
grad.addColorStop(0.6, "#87CEEB");
grad.addColorStop(1, "#B0E0FF");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
drawClouds();
// Hills (parallax)
drawHills();
}
function drawClouds() {
ctx.fillStyle = "rgba(255,255,255,0.8)";
var cloudData = [
{ x: 100, y: 60, s: 1.0 },
{ x: 500, y: 40, s: 0.7 },
{ x: 900, y: 80, s: 1.2 },
{ x: 1400, y: 50, s: 0.8 },
{ x: 1900, y: 70, s: 1.1 },
{ x: 2400, y: 35, s: 0.9 },
{ x: 2900, y: 60, s: 1.0 },
{ x: 3400, y: 45, s: 0.7 },
{ x: 3900, y: 80, s: 1.3 },
{ x: 4400, y: 55, s: 0.8 },
];
for (var i = 0; i < cloudData.length; i++) {
var c = cloudData[i];
var px = c.x - cam.x * 0.2;
// Wrap
px = ((px % (W + 200)) + (W + 200)) % (W + 200) - 100;
var s = c.s;
ctx.beginPath();
ctx.arc(px, c.y, 20 * s, 0, Math.PI * 2);
ctx.arc(px + 18 * s, c.y - 8 * s, 16 * s, 0, Math.PI * 2);
ctx.arc(px + 36 * s, c.y, 20 * s, 0, Math.PI * 2);
ctx.arc(px + 18 * s, c.y + 4 * s, 14 * s, 0, Math.PI * 2);
ctx.fill();
}
}
function drawHills() {
ctx.fillStyle = "#4AAF4A";
var hillData = [
{ x: 0, w: 300, h: 120 },
{ x: 400, w: 200, h: 80 },
{ x: 800, w: 350, h: 140 },
{ x: 1300, w: 250, h: 100 },
{ x: 1700, w: 300, h: 130 },
{ x: 2200, w: 200, h: 90 },
{ x: 2600, w: 350, h: 150 },
{ x: 3100, w: 280, h: 110 },
{ x: 3600, w: 200, h: 80 },
{ x: 4000, w: 300, h: 120 },
{ x: 4400, w: 250, h: 100 },
];
for (var i = 0; i < hillData.length; i++) {
var h = hillData[i];
var px = h.x - cam.x * 0.4;
ctx.beginPath();
ctx.moveTo(px, 420);
ctx.quadraticCurveTo(px + h.w / 2, 420 - h.h, px + h.w, 420);
ctx.fill();
}
}
function drawPlatforms() {
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
var sx = p.x - cam.x;
var sy = p.y - cam.y;
if (sx + p.w < -50 || sx > W + 50) continue;
if (sy + p.h < -50 || sy > H + 50) continue;
if (p.h > 40) {
// Ground
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy, p.w, p.h);
// Grass top
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, p.w, 8);
ctx.fillStyle = "#66BB6A";
ctx.fillRect(sx, sy, p.w, 4);
// Dirt texture
ctx.fillStyle = "#7B3B0F";
for (var dx = 0; dx < p.w; dx += 20) {
for (var dy = 15; dy < p.h; dy += 18) {
ctx.fillRect(sx + dx + (dy % 3 === 0 ? 5 : 0), sy + dy, 8, 6);
}
}
} else {
// Floating platform
ctx.fillStyle = "#A0522D";
ctx.fillRect(sx, sy, p.w, p.h);
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy + p.h - 4, p.w, 4);
ctx.fillStyle = "#6B3410";
ctx.fillRect(sx + 2, sy + 2, p.w - 4, 4);
// Brick pattern
ctx.strokeStyle = "#5C2E0A";
ctx.lineWidth = 1;
for (var bx = 0; bx < p.w; bx += 16) {
ctx.strokeRect(sx + bx, sy, 16, p.h);
}
}
}
}
function drawCoins() {
for (var i = 0; i < coins.length; i++) {
var c = coins[i];
if (c.collected) continue;
var sx = c.x - cam.x;
var sy = c.y - cam.y;
if (sx < -20 || sx > W + 20) continue;
var bob = Math.sin(Date.now() / 300 + i) * 3;
var stretch = Math.abs(Math.cos(Date.now() / 400 + i));
ctx.save();
ctx.translate(sx, sy + bob);
ctx.scale(0.5 + stretch * 0.5, 1);
// Coin body
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFA500";
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFD700";
ctx.font = "bold 10px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 1);
ctx.restore();
}
}
function drawFlag() {
var sx = flag.x - cam.x;
var sy = flag.y - cam.y;
if (sx < -60 || sx > W + 60) return;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(sx + 18, sy - 100, 4, 110);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(sx + 20, sy - 102, 6, 0, Math.PI * 2);
ctx.fill();
// Flag cloth
var wave = Math.sin(Date.now() / 200) * 3;
ctx.fillStyle = "#E53935";
ctx.beginPath();
ctx.moveTo(sx + 22, sy - 95);
ctx.lineTo(sx + 55 + wave, sy - 85);
ctx.lineTo(sx + 22, sy - 65);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFD700";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText("★", sx + 36 + wave * 0.5, sy - 78);
// "GOAL" text
ctx.fillStyle = "#FFF";
ctx.font = "bold 10px Arial";
ctx.textAlign = "center";
ctx.fillText("GOAL", sx + 20, sy + 15);
}
function drawEnemies() {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (e.dead) continue;
var sx = e.x - cam.x;
var sy = e.y - cam.y;
if (sx < -40 || sx > W + 40) continue;
// Body
ctx.fillStyle = "#D32F2F";
ctx.beginPath();
ctx.arc(sx + e.w / 2, sy + e.h / 2, e.w / 2, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(sx + e.w / 2 - 5, sy + e.h / 2 - 3, 4, 0, Math.PI * 2);
ctx.arc(sx + e.w / 2 + 5, sy + e.h / 2 - 3, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#000";
ctx.beginPath();
ctx.arc(sx + e.w / 2 - 4, sy + e.h / 2 - 3, 2, 0, Math.PI * 2);
ctx.arc(sx + e.w / 2 + 6, sy + e.h / 2 - 3, 2, 0, Math.PI * 2);
ctx.fill();
// Angry eyebrows
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(sx + e.w / 2 - 8, sy + e.h / 2 - 9);
ctx.lineTo(sx + e.w / 2 - 2, sy + e.h / 2 - 7);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx + e.w / 2 + 8, sy + e.h / 2 - 9);
ctx.lineTo(sx + e.w / 2 + 2, sy + e.h / 2 - 7);
ctx.stroke();
// Feet
var walk = Math.sin(Date.now() / 150) * 3;
ctx.fillStyle = "#B71C1C";
ctx.fillRect(sx + 2, sy + e.h - 4, 8, 4);
ctx.fillRect(sx + e.w - 10, sy + e.h - 4, 8, 4);
}
}
function drawPlayer() {
if (player.dead) return;
var sx = player.x - cam.x;
var sy = player.y - cam.y;
ctx.save();
ctx.translate(sx + player.w / 2, sy + player.h / 2);
if (player.facing < 0) ctx.scale(-1, 1);
ctx.translate(-player.w / 2, -player.h / 2);
// Body
ctx.fillStyle = "#E53935";
ctx.fillRect(2, 4, 20, 16);
// Head
ctx.fillStyle = "#FFCC80";
ctx.fillRect(4, 0, 16, 12);
// Hat
ctx.fillStyle = "#E53935";
ctx.fillRect(0, 0, 20, 6);
ctx.fillRect(4, -3, 18, 5);
// Hat brim
ctx.fillStyle = "#B71C1C";
ctx.fillRect(0, 4, 22, 3);
// Eyes
ctx.fillStyle = "#000";
ctx.fillRect(14, 5, 3, 4);
// Mustache
ctx.fillStyle = "#5D4037";
ctx.fillRect(10, 9, 10, 2);
// Overalls
ctx.fillStyle = "#1565C0";
ctx.fillRect(2, 18, 20, 10);
// Belt
ctx.fillStyle = "#FFD700";
ctx.fillRect(8, 20, 8, 2);
// Legs
var legOffset = player.onGround && Math.abs(player.vx) > 0.5 ? Math.sin(Date.now() / 80) * 3 : 0;
ctx.fillStyle = "#1565C0";
ctx.fillRect(2, 28, 8, 4 + legOffset);
ctx.fillRect(14, 28, 8, 4 - legOffset);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(0, 30 + legOffset, 10, 3);
ctx.fillRect(14, 30 - legOffset, 10, 3);
ctx.restore();
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
var alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - cam.x, p.y - cam.y, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score panel
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(10, 10, 160, 40);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(10, 10, 160, 40);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 18px 'Segoe UI', Arial, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("★ " + player.score, 22, 25);
// Lives
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(W - 120, 10, 110, 40);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.strokeRect(W - 120, 10, 110, 40);
ctx.fillStyle = "#FF5252";
ctx.font = "bold 18px 'Segoe UI', Arial, sans-serif";
ctx.textAlign = "right";
ctx.fillText("♥ " + player.lives, W - 20, 25);
// Controls hint (fades out)
var elapsed = Date.now() / 1000;
if (elapsed < 12) {
var alpha = elapsed < 10 ? 0.7 : 0.7 * (12 - elapsed) / 2;
ctx.globalAlpha = alpha;
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(W / 2 - 180, H - 50, 360, 36);
ctx.fillStyle = "#FFF";
ctx.font = "13px 'Segoe UI', Arial, sans-serif";
ctx.textAlign = "center";
ctx.fillText("Arrow Keys / WASD to move • Space / Up / W to jump", W / 2, H - 30);
ctx.globalAlpha = 1;
}
}
function drawWinScreen() {
if (!player.win) return;
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px 'Segoe UI', Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🏆 YOU WIN! 🏆", W / 2, H / 2 - 40);
ctx.fillStyle = "#FFF";
ctx.font = "24px 'Segoe UI', Arial, sans-serif";
ctx.fillText("Score: " + player.score, W / 2, H / 2 + 20);
ctx.fillStyle = "#AAA";
ctx.font = "16px 'Segoe UI', Arial, sans-serif";
ctx.fillText("Press R to play again", W / 2, H / 2 + 60);
}
function drawDeathScreen() {
if (!player.dead || player.respawnTimer > 30) return;
ctx.fillStyle = "rgba(255,0,0,0.3)";
ctx.fillRect(0, 0, W, H);
}
// ─── Main loop ───────────────────────────────────────────
function gameLoop() {
// Input: restart
if (keys["KeyR"] && player.win) {
resetGame();
}
updatePlayer();
updateEnemies();
updateParticles();
updateCamera();
// Draw
drawBackground();
drawPlatforms();
drawCoins();
drawEnemies();
drawFlag();
drawPlayer();
drawParticles();
drawHUD();
drawDeathScreen();
drawWinScreen();
requestAnimationFrame(gameLoop);
}
function resetGame() {
player.x = 80;
player.y = 350;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.score = 0;
player.lives = 3;
player.dead = false;
player.win = false;
player.respawnTimer = 0;
player.facing = 1;
cam.x = 0;
cam.y = 0;
particles = [];
buildLevel();
}
// ─── Start ───────────────────────────────────────────────
buildLevel();
requestAnimationFrame(gameLoop);
})();
</script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
// ============================================================
// SUPER PIXEL ADVENTURE — A Mario-style Platformer
// Vanilla JS, Canvas, Zero Dependencies
// ============================================================
(function () {
"use strict";
// ---- Constants ----
const CANVAS_W = 800;
const CANVAS_H = 450;
const TILE = 32;
const GRAVITY = 0.55;
const FRICTION = 0.82;
const MAX_SPEED = 5.2;
const JUMP_FORCE = -11.5;
const ACCEL = 0.65;
const LEVEL_WIDTH = 220; // tiles wide
const LEVEL_HEIGHT = 14; // tiles high
// ---- Canvas Setup ----
const canvas = document.getElementById("game");
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const ctx = canvas.getContext("2d");
// ---- Input ----
const keys = {};
window.addEventListener("keydown", (e) => {
keys[e.code] = true;
// Prevent scrolling for game keys
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Space"].includes(
e.code
)
) {
e.preventDefault();
}
});
window.addEventListener("keyup", (e) => {
keys[e.code] = false;
});
// ---- Game State ----
let score = 0;
let coinsCollected = 0;
let totalCoins = 0;
let gameState = "playing"; // "playing" | "won" | "dead"
let deathTimer = 0;
let winTimer = 0;
let particles = [];
let frameCount = 0;
// ---- Camera ----
const camera = { x: 0, y: 0 };
// ---- Level Data ----
// 0 = air, 1 = ground, 2 = platform, 3 = coin, 4 = flag pole, 5 = question block
const levelMap = [];
function buildLevel() {
for (let y = 0; y < LEVEL_HEIGHT; y++) {
levelMap[y] = [];
for (let x = 0; x < LEVEL_WIDTH; x++) {
levelMap[y][x] = 0;
}
}
// Ground segments with gaps
const groundSegments = [
[0, 40],
[45, 70],
[75, 110],
[115, 160],
[165, LEVEL_WIDTH],
];
for (const [start, end] of groundSegments) {
for (let x = start; x < end; x++) {
levelMap[LEVEL_HEIGHT - 1][x] = 1;
levelMap[LEVEL_HEIGHT - 2][x] = 1;
}
}
// ---- Platforms ----
const platforms = [
// Low floating platforms early
{ x: 8, y: 10, w: 5 },
{ x: 16, y: 8, w: 4 },
{ x: 24, y: 10, w: 3 },
// Mid section
{ x: 35, y: 9, w: 4 },
{ x: 48, y: 7, w: 5 },
{ x: 55, y: 10, w: 3 },
// Higher section
{ x: 65, y: 8, w: 4 },
{ x: 72, y: 6, w: 3 },
{ x: 80, y: 9, w: 5 },
// Tricky jumps
{ x: 95, y: 10, w: 3 },
{ x: 100, y: 8, w: 4 },
{ x: 107, y: 6, w: 3 },
// Long section
{ x: 120, y: 9, w: 5 },
{ x: 130, y: 7, w: 4 },
{ x: 138, y: 10, w: 3 },
{ x: 145, y: 8, w: 5 },
// Final stretch
{ x: 155, y: 10, w: 4 },
{ x: 162, y: 7, w: 3 },
{ x: 170, y: 5, w: 4 },
{ x: 178, y: 8, w: 5 },
{ x: 188, y: 10, w: 3 },
];
for (const p of platforms) {
for (let px = 0; px < p.w; px++) {
if (p.x + px < LEVEL_WIDTH) {
levelMap[p.y][p.x + px] = 2;
}
}
}
// ---- Question blocks (bonus coins) ----
const questionBlocks = [
{ x: 12, y: 6 },
{ x: 37, y: 5 },
{ x: 50, y: 4 },
{ x: 82, y: 6 },
{ x: 122, y: 6 },
{ x: 147, y: 5 },
{ x: 172, y: 2 },
{ x: 180, y: 5 },
];
for (const qb of questionBlocks) {
if (qb.x < LEVEL_WIDTH && qb.y < LEVEL_HEIGHT) {
levelMap[qb.y][qb.x] = 5;
}
}
// ---- Coins ----
const coinPositions = [
// Early coins
{ x: 9, y: 8 },
{ x: 10, y: 8 },
{ x: 17, y: 6 },
{ x: 18, y: 6 },
{ x: 25, y: 8 },
// Mid coins
{ x: 36, y: 7 },
{ x: 37, y: 7 },
{ x: 49, y: 5 },
{ x: 50, y: 5 },
{ x: 51, y: 5 },
{ x: 66, y: 6 },
{ x: 67, y: 6 },
{ x: 73, y: 4 },
// Higher coins
{ x: 81, y: 7 },
{ x: 82, y: 7 },
{ x: 96, y: 8 },
{ x: 101, y: 6 },
{ x: 102, y: 6 },
{ x: 108, y: 4 },
// Late coins
{ x: 121, y: 7 },
{ x: 122, y: 7 },
{ x: 131, y: 5 },
{ x: 132, y: 5 },
{ x: 139, y: 8 },
{ x: 146, y: 6 },
{ x: 147, y: 6 },
// Final coins
{ x: 156, y: 8 },
{ x: 163, y: 5 },
{ x: 171, y: 3 },
{ x: 172, y: 3 },
{ x: 179, y: 6 },
{ x: 180, y: 6 },
{ x: 189, y: 8 },
];
for (const c of coinPositions) {
if (c.x < LEVEL_WIDTH && c.y < LEVEL_HEIGHT) {
levelMap[c.y][c.x] = 3;
}
}
// ---- Flag (goal) ----
const flagX = 200;
if (flagX < LEVEL_WIDTH) {
levelMap[LEVEL_HEIGHT - 3][flagX] = 4; // flag pole base
levelMap[LEVEL_HEIGHT - 4][flagX] = 4;
levelMap[LEVEL_HEIGHT - 5][flagX] = 4;
levelMap[LEVEL_HEIGHT - 6][flagX] = 4;
levelMap[LEVEL_HEIGHT - 7][flagX] = 4;
levelMap[LEVEL_HEIGHT - 8][flagX] = 4;
levelMap[LEVEL_HEIGHT - 9][flagX] = 4;
levelMap[LEVEL_HEIGHT - 10][flagX] = 4;
}
// Count total coins
totalCoins = 0;
for (let y = 0; y < LEVEL_HEIGHT; y++) {
for (let x = 0; x < LEVEL_WIDTH; x++) {
if (levelMap[y][x] === 3 || levelMap[y][x] === 5) {
totalCoins++;
}
}
}
}
buildLevel();
// ---- Player ----
const player = {
x: 3 * TILE,
y: (LEVEL_HEIGHT - 4) * TILE,
w: 22,
h: 28,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
animFrame: 0,
animTimer: 0,
jumpHeld: false,
coyoteTime: 0,
jumpBuffer: 0,
};
function resetPlayer() {
player.x = 3 * TILE;
player.y = (LEVEL_HEIGHT - 4) * TILE;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.facing = 1;
gameState = "playing";
deathTimer = 0;
winTimer = 0;
camera.x = 0;
camera.y = 0;
}
// ---- Tile Collision ----
function getTile(tx, ty) {
if (tx < 0 || tx >= LEVEL_WIDTH || ty < 0 || ty >= LEVEL_HEIGHT) return 0;
return levelMap[ty][tx];
}
function isSolid(type) {
return type === 1 || type === 2 || type === 5;
}
// ---- Particles ----
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.5) * 5 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color: color,
size: 2 + Math.random() * 3,
});
}
}
// ---- Update ----
function update() {
frameCount++;
if (gameState === "dead") {
deathTimer++;
if (deathTimer > 90) {
resetPlayer();
}
// Update particles
updateParticles();
return;
}
if (gameState === "won") {
winTimer++;
updateParticles();
return;
}
// ---- Player Input ----
let moveX = 0;
if (keys["ArrowLeft"] || keys["KeyA"]) moveX -= 1;
if (keys["ArrowRight"] || keys["KeyD"]) moveX += 1;
const jumpKey =
keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
// Horizontal movement with acceleration
if (moveX !== 0) {
player.vx += moveX * ACCEL;
if (player.vx > MAX_SPEED) player.vx = MAX_SPEED;
if (player.vx < -MAX_SPEED) player.vx = -MAX_SPEED;
player.facing = moveX;
} else {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Coyote time & jump buffer
if (player.onGround) {
player.coyoteTime = 8;
} else {
player.coyoteTime--;
}
if (jumpKey) {
player.jumpBuffer = 8;
} else {
player.jumpBuffer--;
}
// Jump (with coyote time and jump buffer)
if (player.jumpBuffer > 0 && player.coyoteTime > 0) {
player.vy = JUMP_FORCE;
player.onGround = false;
player.coyoteTime = 0;
player.jumpBuffer = 0;
spawnParticles(
player.x + player.w / 2,
player.y + player.h,
"#ddd",
5
);
}
// Variable jump height — release early to jump shorter
if (!jumpKey && player.vy < -3) {
player.vy *= 0.7;
}
// Gravity
player.vy += GRAVITY;
if (player.vy > 12) player.vy = 12;
// ---- Horizontal Collision ----
player.x += player.vx;
const left = Math.floor(player.x / TILE);
const right = Math.floor((player.x + player.w) / TILE);
const top = Math.floor(player.y / TILE);
const bottom = Math.floor((player.y + player.h) / TILE);
for (let ty = top; ty <= bottom; ty++) {
if (player.vx > 0) {
const tx = right;
if (isSolid(getTile(tx, ty))) {
player.x = tx * TILE - player.w;
player.vx = 0;
break;
}
} else if (player.vx < 0) {
const tx = left;
if (isSolid(getTile(tx, ty))) {
player.x = (tx + 1) * TILE;
player.vx = 0;
break;
}
}
}
// ---- Vertical Collision ----
player.y += player.vy;
player.onGround = false;
const newLeft = Math.floor(player.x / TILE);
const newRight = Math.floor((player.x + player.w) / TILE);
const newTop = Math.floor(player.y / TILE);
const newBottom = Math.floor((player.y + player.h) / TILE);
for (let tx = newLeft; tx <= newRight; tx++) {
if (player.vy > 0) {
const ty = newBottom;
if (isSolid(getTile(tx, ty))) {
player.y = ty * TILE - player.h;
player.vy = 0;
player.onGround = true;
break;
}
} else if (player.vy < 0) {
const ty = newTop;
const tile = getTile(tx, ty);
if (isSolid(tile)) {
player.y = (ty + 1) * TILE;
player.vy = 0;
// Hit question block from below
if (tile === 5) {
levelMap[ty][tx] = 2; // turn into solid block
score += 50;
spawnParticles(
tx * TILE + TILE / 2,
ty * TILE,
"#FFD700",
8
);
}
break;
}
}
}
// ---- Collectibles ----
const playerCX = Math.floor((player.x + player.w / 2) / TILE);
const playerCY = Math.floor((player.y + player.h / 2) / TILE);
// Check surrounding tiles for coins
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const tx = playerCX + dx;
const ty = playerCY + dy;
if (getTile(tx, ty) === 3) {
levelMap[ty][tx] = 0;
score += 100;
coinsCollected++;
spawnParticles(
tx * TILE + TILE / 2,
ty * TILE + TILE / 2,
"#FFD700",
10
);
}
}
}
// ---- Flag (goal) ----
const flagTileX = Math.floor((player.x + player.w / 2) / TILE);
const flagTileY = Math.floor((player.y + player.h / 2) / TILE);
if (getTile(flagTileX, flagTileY) === 4) {
gameState = "won";
winTimer = 0;
score += 1000;
spawnParticles(
flagTileX * TILE + TILE / 2,
flagTileY * TILE,
"#FF6B6B",
20
);
spawnParticles(
flagTileX * TILE + TILE / 2,
flagTileY * TILE,
"#4ECDC4",
20
);
spawnParticles(
flagTileX * TILE + TILE / 2,
flagTileY * TILE,
"#FFD700",
20
);
}
// ---- Fall off the map ----
if (player.y > LEVEL_HEIGHT * TILE + 100) {
gameState = "dead";
deathTimer = 0;
spawnParticles(
player.x + player.w / 2,
LEVEL_HEIGHT * TILE,
"#FF4444",
15
);
}
// ---- Clamp player to level bounds ----
if (player.x < 0) {
player.x = 0;
player.vx = 0;
}
// ---- Animation ----
if (Math.abs(player.vx) > 0.5 && player.onGround) {
player.animTimer++;
if (player.animTimer > 6) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
} else if (player.onGround) {
player.animFrame = 0;
player.animTimer = 0;
}
// ---- Camera ----
const targetX = player.x - CANVAS_W / 2 + player.w / 2;
const targetY = player.y - CANVAS_H / 2 + player.h / 2;
camera.x += (targetX - camera.x) * 0.1;
camera.y += (targetY - camera.y) * 0.08;
// Clamp camera
camera.x = Math.max(0, Math.min(camera.x, LEVEL_WIDTH * TILE - CANVAS_W));
camera.y = Math.max(
0,
Math.min(camera.y, LEVEL_HEIGHT * TILE - CANVAS_H)
);
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--;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
// ---- Drawing ----
function drawBackground() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, CANVAS_H);
grad.addColorStop(0, "#5C94FC");
grad.addColorStop(0.6, "#87CEEB");
grad.addColorStop(1, "#B0E0FF");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// Clouds (parallax)
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
drawCloud(
(100 - camera.x * 0.1) % (CANVAS_W + 200) - 100,
50 + Math.sin(frameCount * 0.005) * 5,
1.0
);
drawCloud(
(400 - camera.x * 0.15) % (CANVAS_W + 200) - 100,
80 + Math.sin(frameCount * 0.007 + 1) * 5,
0.8
);
drawCloud(
(700 - camera.x * 0.08) % (CANVAS_W + 200) - 100,
35 + Math.sin(frameCount * 0.006 + 2) * 5,
1.2
);
drawCloud(
(1000 - camera.x * 0.12) % (CANVAS_W + 200) - 100,
95 + Math.sin(frameCount * 0.004 + 3) * 5,
0.9
);
// Hills (parallax layer 2)
ctx.fillStyle = "#4CAF50";
drawHills(0, CANVAS_H - 60, 0.2);
ctx.fillStyle = "#388E3C";
drawHills(200, CANVAS_H - 40, 0.3);
}
function drawCloud(x, y, scale) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.arc(25, -5, 25, 0, Math.PI * 2);
ctx.arc(50, 0, 20, 0, Math.PI * 2);
ctx.arc(25, 5, 22, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHills(offsetY, baseY, parallax) {
for (let i = -1; i < 6; i++) {
const hx = i * 300 - (camera.x * parallax) % 300;
ctx.beginPath();
ctx.moveTo(hx - 100, baseY + 80);
ctx.quadraticCurveTo(hx, baseY - 60, hx + 100, baseY + 80);
ctx.fill();
}
}
function drawTile(tx, ty, type) {
const sx = tx * TILE - camera.x;
const sy = ty * TILE - camera.y;
// Skip if off screen
if (sx + TILE < 0 || sx > CANVAS_W || sy + TILE < 0 || sy > CANVAS_H)
return;
switch (type) {
case 1: // Ground
// Top layer with grass
const above = getTile(tx, ty - 1);
if (above === 0 || above === 3) {
// Grass top
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, TILE, 6);
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy + 6, TILE, TILE - 6);
// Dirt texture
ctx.fillStyle = "#7B3B0F";
ctx.fillRect(sx + 4, sy + 12, 6, 4);
ctx.fillRect(sx + 18, sy + 20, 8, 4);
ctx.fillRect(sx + 8, sy + 24, 5, 3);
} else {
ctx.fillStyle = "#8B4513";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#7B3B0F";
ctx.fillRect(sx + 4, sy + 8, 6, 4);
ctx.fillRect(sx + 20, sy + 18, 8, 4);
}
break;
case 2: // Platform / solid block
ctx.fillStyle = "#C84C09";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#E8731A";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
// Brick lines
ctx.strokeStyle = "#8B3000";
ctx.lineWidth = 1;
ctx.strokeRect(sx + 1, sy + 1, TILE - 2, TILE - 2);
ctx.beginPath();
ctx.moveTo(sx + TILE / 2, sy);
ctx.lineTo(sx + TILE / 2, sy + TILE);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx, sy + TILE / 2);
ctx.lineTo(sx + TILE, sy + TILE / 2);
ctx.stroke();
break;
case 3: // Coin
const bobY = Math.sin(frameCount * 0.08 + tx * 1.5) * 3;
const coinX = sx + TILE / 2;
const coinY = sy + TILE / 2 + bobY;
// Outer glow
ctx.fillStyle = "rgba(255, 215, 0, 0.3)";
ctx.beginPath();
ctx.arc(coinX, coinY, 14, 0, Math.PI * 2);
ctx.fill();
// Coin body
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(coinX, coinY, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFC107";
ctx.beginPath();
ctx.arc(coinX, coinY, 7, 0, Math.PI * 2);
ctx.fill();
// Shine
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
ctx.beginPath();
ctx.arc(coinX - 3, coinY - 3, 3, 0, Math.PI * 2);
ctx.fill();
break;
case 4: // Flag pole
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(sx + 14, sy, 4, TILE);
if (ty === LEVEL_HEIGHT - 3) {
// Flag at top
ctx.fillStyle = "#FF4444";
ctx.beginPath();
ctx.moveTo(sx + 18, sy);
ctx.lineTo(sx + 38, sy + 8);
ctx.lineTo(sx + 18, sy + 16);
ctx.fill();
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(sx + 16, sy - 2, 4, 0, Math.PI * 2);
ctx.fill();
}
break;
case 5: // Question block
const bounce = Math.sin(frameCount * 0.06 + tx * 2) * 1;
ctx.fillStyle = "#FFB800";
ctx.fillRect(sx, sy + bounce, TILE, TILE);
ctx.fillStyle = "#FFD700";
ctx.fillRect(sx + 2, sy + 2 + bounce, TILE - 4, TILE - 4);
// Question mark
ctx.fillStyle = "#8B6914";
ctx.font = "bold 18px Arial";
ctx.textAlign = "center";
ctx.fillText("?", sx + TILE / 2, sy + TILE / 2 + 6 + bounce);
// Corner dots
ctx.fillStyle = "#C89600";
ctx.fillRect(sx + 2, sy + 2 + bounce, 3, 3);
ctx.fillRect(sx + TILE - 5, sy + 2 + bounce, 3, 3);
ctx.fillRect(sx + 2, sy + TILE - 5 + bounce, 3, 3);
ctx.fillRect(sx + TILE - 5, sy + TILE - 5 + bounce, 3, 3);
break;
}
}
function drawPlayer() {
const sx = player.x - camera.x;
const sy = player.y - camera.y;
if (gameState === "dead") {
// Draw player as fading/spinning
const alpha = 1 - deathTimer / 90;
ctx.globalAlpha = alpha;
const rot = (deathTimer / 90) * Math.PI * 2;
ctx.save();
ctx.translate(sx + player.w / 2, sy + player.h / 2);
ctx.rotate(rot);
ctx.translate(-(sx + player.w / 2), -(sy + player.h / 2));
}
// Shadow
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.beginPath();
ctx.ellipse(
sx + player.w / 2,
sy + player.h + 2,
player.w / 2 + 2,
4,
0,
0,
Math.PI * 2
);
ctx.fill();
// Body (overalls)
ctx.fillStyle = "#E52521";
ctx.fillRect(sx + 3, sy + 4, 16, 12);
// Head
ctx.fillStyle = "#FFB366";
ctx.fillRect(sx + 5, sy, 12, 10);
// Hat
ctx.fillStyle = "#E52521";
ctx.fillRect(sx + 3, sy - 2, 16, 5);
ctx.fillRect(sx + (player.facing > 0 ? 12 : 1), sy, 10, 3);
// Eyes
ctx.fillStyle = "#000";
const eyeX = player.facing > 0 ? sx + 13 : sx + 7;
ctx.fillRect(eyeX, sy + 3, 3, 3);
// Mustache
ctx.fillStyle = "#4A2800";
ctx.fillRect(sx + 6, sy + 7, 10, 2);
// Legs
const legOffset = player.onGround && Math.abs(player.vx) > 0.5
? Math.sin(player.animFrame * Math.PI / 2) * 3
: 0;
ctx.fillStyle = "#2835A7";
ctx.fillRect(sx + 3, sy + 16, 6, 8 + legOffset);
ctx.fillRect(sx + 13, sy + 16, 6, 8 - legOffset);
// Shoes
ctx.fillStyle = "#6B3300";
ctx.fillRect(sx + 1, sy + 23 + Math.max(0, legOffset), 8, 4);
ctx.fillRect(sx + 13, sy + 23 + Math.max(0, -legOffset), 8, 4);
// Arms
ctx.fillStyle = "#FFB366";
if (!player.onGround) {
// Arms up when jumping
ctx.fillRect(sx - 2, sy + 2, 5, 8);
ctx.fillRect(sx + 17, sy + 2, 5, 8);
} else {
ctx.fillRect(sx - 2, sy + 8, 5, 8);
ctx.fillRect(sx + 17, sy + 8, 5, 8);
}
if (gameState === "dead") {
ctx.restore();
ctx.globalAlpha = 1;
}
}
function drawParticles() {
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(
p.x - camera.x - p.size / 2,
p.y - camera.y - p.size / 2,
p.size,
p.size
);
}
ctx.globalAlpha = 1;
}
function drawUI() {
// Score panel
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(10, 10, 220, 44);
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, 220, 44);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 16px 'Courier New', monospace";
ctx.textAlign = "left";
ctx.fillText("🪙 COINS: " + coinsCollected, 20, 30);
ctx.fillStyle = "#FFF";
ctx.fillText("SCORE: " + score, 20, 50);
// Progress indicator
const progress = Math.min(1, player.x / ((LEVEL_WIDTH - 5) * TILE));
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(CANVAS_W - 160, 14, 150, 16);
ctx.fillStyle = "#4CAF50";
ctx.fillRect(CANVAS_W - 158, 16, 146 * progress, 12);
ctx.fillStyle = "#FFD700";
ctx.font = "11px 'Courier New', monospace";
ctx.textAlign = "right";
ctx.fillText("GOAL", CANVAS_W - 12, 26);
}
function drawWinScreen() {
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.textAlign = "center";
// Title
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px 'Courier New', monospace";
ctx.fillText("🎉 YOU WIN! 🎉", CANVAS_W / 2, CANVAS_H / 2 - 40);
ctx.fillStyle = "#FFF";
ctx.font = "24px 'Courier New', monospace";
ctx.fillText("Score: " + score, CANVAS_W / 2, CANVAS_H / 2 + 10);
ctx.fillText(
"Coins: " + coinsCollected + " / " + totalCoins,
CANVAS_W / 2,
CANVAS_H / 2 + 45
);
if (winTimer > 60) {
ctx.fillStyle = "#4ECDC4";
ctx.font = "18px 'Courier New', monospace";
ctx.fillText(
"Press R to play again",
CANVAS_W / 2,
CANVAS_H / 2 + 85
);
}
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.textAlign = "center";
ctx.fillStyle = "#FF6B6B";
ctx.font = "bold 36px 'Courier New', monospace";
ctx.fillText("OOPS!", CANVAS_W / 2, CANVAS_H / 2);
}
function draw() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
drawBackground();
// Draw tiles
const startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
const endTX = Math.min(
LEVEL_WIDTH,
Math.ceil((camera.x + CANVAS_W) / TILE) + 1
);
const startTY = Math.max(0, Math.floor(camera.y / TILE) - 1);
const endTY = Math.min(
LEVEL_HEIGHT,
Math.ceil((camera.y + CANVAS_H) / TILE) + 1
);
for (let ty = startTY; ty < endTY; ty++) {
for (let tx = startTX; tx < endTX; tx++) {
const tile = getTile(tx, ty);
if (tile !== 0) {
drawTile(tx, ty, tile);
}
}
}
drawParticles();
drawPlayer();
drawUI();
if (gameState === "won") {
drawWinScreen();
}
if (gameState === "dead") {
drawDeathScreen();
}
}
// ---- Game Loop ----
function gameLoop() {
// Check restart
if ((gameState === "won" || gameState === "dead") && keys["KeyR"]) {
score = 0;
coinsCollected = 0;
buildLevel();
resetPlayer();
}
update();
draw();
requestAnimationFrame(gameLoop);
}
// ---- Start ----
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 Adventure</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Courier New', monospace;
overflow: hidden;
}
h1 {
color: #FFD700;
font-size: 24px;
margin-bottom: 12px;
text-shadow: 2px 2px 0 #8B6914;
letter-spacing: 2px;
}
#game {
border: 3px solid #FFD700;
border-radius: 4px;
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
display: block;
}
.controls {
color: #aaa;
font-size: 13px;
margin-top: 12px;
text-align: center;
line-height: 1.6;
}
.controls kbd {
background: #333;
color: #fff;
padding: 2px 7px;
border-radius: 3px;
border: 1px solid #555;
font-size: 12px;
}
</style>
</head>
<body>
<h1>🍄 SUPER PIXEL ADVENTURE 🍄</h1>
<canvas id="game"></canvas>
<div class="controls">
<kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> to move |
<kbd>Space</kbd> / <kbd>↑</kbd> / <kbd>W</kbd> to jump |
<kbd>R</kbd> to restart
</div>
<script src="game.js"></script>
</body>
</html>
screenshot

CHECK PASS: canvas #game exists and is sized CHECK PASS: no uncaught JS errors on load CHECK PASS: keyboard input does not throw score: 3/3 (100%)
/* ============================================================
Super Platformer — a Mario-style side-scroller
All game logic in a single file. 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
const hudEl = document.getElementById("hud");
const msgOverlay = document.getElementById("message-overlay");
const msgText = document.getElementById("message-text");
const msgSub = document.getElementById("message-sub");
// ─── Constants ─────────────────────────────────────────────
const GRAVITY = 1800;
const MOVE_SPEED = 280;
const ACCEL = 1200;
const DECEL = 1000;
const AIR_ACCEL = 800;
const JUMP_VEL = -620;
const JUMP_HOLD_TIME = 0.18; // seconds of jump hold for variable height
const COYOTE_TIME = 0.10; // seconds after leaving platform you can still jump
const JUMP_BUFFER = 0.10; // seconds before landing you can buffer a jump
const ENEMY_SPEED = 80;
const ENEMY_HURT_VEL_X = 250;
const ENEMY_HURT_VEL_Y = -400;
const LEVEL_WIDTH = 5200;
const TILE = 32;
const PLAYER_W = 24;
const PLAYER_H = 36;
// ─── Input ─────────────────────────────────────────────────
const keys = {};
window.addEventListener("keydown", (e) => {
keys[e.code] = true;
// Prevent 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"]; }
// ─── Level Data ────────────────────────────────────────────
// Ground segments: {x, y, w}
// Platforms: {x, y, w, h}
// Coins: {x, y}
// Enemies: {x, y, w, h, minX, maxX}
// Flag: {x, y}
const GROUND_Y = H - 40;
function buildLevel() {
const platforms = [];
const coins = [];
const enemies = [];
let totalCoins = 0;
// ── Ground segments (with gaps) ──
const groundSegments = [
{ x: 0, w: 560 },
{ x: 620, w: 480 },
{ x: 1160, w: 320 },
{ x: 1540, w: 600 },
{ x: 2240, w: 400 },
{ x: 2740, w: 800 },
{ x: 3640, w: 600 },
{ x: 4340, w: 860 },
];
groundSegments.forEach((s) => {
platforms.push({ x: s.x, y: GROUND_Y, w: s.w, h: 40 });
});
// ── Raised platforms ──
const plats = [
// Early area
{ x: 200, y: GROUND_Y - 100, w: 96, h: 16 },
{ x: 380, y: GROUND_Y - 170, w: 96, h: 16 },
{ x: 540, y: GROUND_Y - 100, w: 64, h: 16 },
// After first gap
{ x: 700, y: GROUND_Y - 90, w: 128, h: 16 },
{ x: 900, y: GROUND_Y - 160, w: 96, h: 16 },
{ x: 1050, y: GROUND_Y - 90, w: 64, h: 16 },
// Mid area
{ x: 1200, y: GROUND_Y - 120, w: 96, h: 16 },
{ x: 1360, y: GROUND_Y - 200, w: 96, h: 16 },
{ x: 1560, y: GROUND_Y - 100, w: 128, h: 16 },
{ x: 1760, y: GROUND_Y - 170, w: 96, h: 16 },
// Staircase section
{ x: 1960, y: GROUND_Y - 80, w: 64, h: 16 },
{ x: 2040, y: GROUND_Y - 120, w: 64, h: 16 },
{ x: 2120, y: GROUND_Y - 160, w: 64, h: 16 },
// After gap
{ x: 2300, y: GROUND_Y - 100, w: 96, h: 16 },
{ x: 2500, y: GROUND_Y - 160, w: 96, h: 16 },
// High area
{ x: 2800, y: GROUND_Y - 90, w: 128, h: 16 },
{ x: 3000, y: GROUND_Y - 160, w: 96, h: 16 },
{ x: 3200, y: GROUND_Y - 230, w: 96, h: 16 },
{ x: 3400, y: GROUND_Y - 160, w: 96, h: 16 },
// Final stretch
{ x: 3700, y: GROUND_Y - 100, w: 96, h: 16 },
{ x: 3900, y: GROUND_Y - 170, w: 96, h: 16 },
{ x: 4100, y: GROUND_Y - 100, w: 128, h: 16 },
];
plats.forEach((p) => platforms.push(p));
// ── Coins ──
function addCoins(x, y, count, spacing) {
for (let i = 0; i < count; i++) {
coins.push({ x: x + i * (spacing || 36), y: y });
totalCoins++;
}
}
// Ground coins
addCoins(120, GROUND_Y - 50, 3, 36);
addCoins(450, GROUND_Y - 50, 2, 36);
addCoins(720, GROUND_Y - 50, 3, 36);
// Platform coins
coins.push({ x: 230, y: GROUND_Y - 130 });
coins.push({ x: 410, y: GROUND_Y - 200 });
coins.push({ x: 560, y: GROUND_Y - 130 });
addCoins(730, GROUND_Y - 120, 3, 36);
coins.push({ x: 930, y: GROUND_Y - 190 });
addCoins(1230, GROUND_Y - 150, 2, 36);
coins.push({ x: 1390, y: GROUND_Y - 230 });
addCoins(1590, GROUND_Y - 130, 3, 36);
coins.push({ x: 1790, y: GROUND_Y - 200 });
// Staircase coins
coins.push({ x: 1990, y: GROUND_Y - 110 });
coins.push({ x: 2070, y: GROUND_Y - 150 });
coins.push({ x: 2150, y: GROUND_Y - 190 });
addCoins(2330, GROUND_Y - 130, 2, 36);
coins.push({ x: 2530, y: GROUND_Y - 190 });
addCoins(2830, GROUND_Y - 120, 3, 36);
coins.push({ x: 3030, y: GROUND_Y - 190 });
coins.push({ x: 3230, y: GROUND_Y - 260 });
addCoins(3430, GROUND_Y - 190, 2, 36);
addCoins(3730, GROUND_Y - 130, 2, 36);
coins.push({ x: 3930, y: GROUND_Y - 200 });
addCoins(4130, GROUND_Y - 130, 3, 36);
// ── Enemies ──
function addEnemy(x, y, range) {
enemies.push({
x, y, w: 28, h: 28,
minX: x - (range || 80),
maxX: x + (range || 80),
vx: ENEMY_SPEED,
vy: 0,
alive: true,
squishTimer: 0,
});
}
addEnemy(300, GROUND_Y - 28, 80);
addEnemy(500, GROUND_Y - 28, 60);
addEnemy(750, GROUND_Y - 28, 100);
addEnemy(1100, GROUND_Y - 28, 70);
addEnemy(1580, GROUND_Y - 28, 120);
addEnemy(1800, GROUND_Y - 28, 80);
addEnemy(2350, GROUND_Y - 28, 100);
addEnemy(2600, GROUND_Y - 28, 80);
addEnemy(2900, GROUND_Y - 28, 120);
addEnemy(3100, GROUND_Y - 28, 80);
addEnemy(3700, GROUND_Y - 28, 100);
addEnemy(4000, GROUND_Y - 28, 80);
addEnemy(4400, GROUND_Y - 28, 120);
// ── Flag (goal) ──
const flag = { x: 5000, y: GROUND_Y - 200 };
return { platforms, coins, enemies, flag, totalCoins };
}
// ─── Game State ────────────────────────────────────────────
let level, player, camera, gameState; // gameState: "playing" | "won" | "dead" | "intro"
function initGame() {
level = buildLevel();
player = {
x: 60,
y: GROUND_Y - PLAYER_H,
w: PLAYER_W,
h: PLAYER_H,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
coyoteTimer: 0,
jumpBufferTimer: 0,
jumpHoldTimer: 0,
alive: true,
lives: 3,
invincibleTimer: 0,
deathTimer: 0,
animFrame: 0,
animTimer: 0,
};
camera = { x: 0 };
gameState = "playing";
msgOverlay.style.display = "none";
hudEl.textContent = `COINS: 0 / ${level.totalCoins} LIVES: ${player.lives}`;
}
// ─── 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;
}
function lerp(a, b, t) { return a + (b - a) * t; }
// ─── Update ────────────────────────────────────────────────
let lastTime = 0;
let coinCount = 0;
function update(dt) {
if (gameState !== "playing") return;
// ── Player death animation ──
if (!player.alive) {
player.deathTimer -= dt;
player.vy += GRAVITY * dt;
player.y += player.vy * dt;
if (player.deathTimer <= 0) {
player.lives--;
if (player.lives <= 0) {
gameState = "dead";
showMessage("GAME OVER", "Press ENTER to restart");
return;
}
// Respawn
respawnPlayer();
}
hudEl.textContent = `COINS: ${coinCount} / ${level.totalCoins} LIVES: ${player.lives}`;
return;
}
// ── Invincibility timer ──
if (player.invincibleTimer > 0) {
player.invincibleTimer -= dt;
}
// ── Horizontal movement ──
const wantLeft = isLeft();
const wantRight = isRight();
const onGround = player.onGround;
if (wantLeft) {
player.vx -= (onGround ? ACCEL : AIR_ACCEL) * dt;
player.facing = -1;
} else if (wantRight) {
player.vx += (onGround ? ACCEL : AIR_ACCEL) * dt;
player.facing = 1;
} else {
// Decelerate
const decel = onGround ? DECEL : ACCEL * 0.5;
if (player.vx > 0) {
player.vx = Math.max(0, player.vx - decel * dt);
} else if (player.vx < 0) {
player.vx = Math.min(0, player.vx + decel * dt);
}
}
player.vx = Math.max(-MOVE_SPEED, Math.min(MOVE_SPEED, player.vx));
// ── Jump input ──
if (isJump()) {
player.jumpBufferTimer = JUMP_BUFFER;
} else {
player.jumpBufferTimer = Math.max(0, player.jumpBufferTimer - dt);
}
// ── Apply jump ──
if (player.jumpBufferTimer > 0 && (player.onGround || player.coyoteTimer > 0)) {
player.vy = JUMP_VEL;
player.onGround = false;
player.coyoteTimer = 0;
player.jumpBufferTimer = 0;
player.jumpHoldTimer = JUMP_HOLD_TIME;
}
// Variable jump height
if (isJump() && player.jumpHoldTimer > 0) {
player.vy += JUMP_VEL * 0.5 * dt / JUMP_HOLD_TIME;
player.jumpHoldTimer -= dt;
} else {
player.jumpHoldTimer = 0;
}
// ── Coyote time ──
if (player.onGround) {
player.coyoteTimer = COYOTE_TIME;
} else {
player.coyoteTimer = Math.max(0, player.coyoteTimer - dt);
}
// ── Gravity ──
player.vy += GRAVITY * dt;
if (player.vy > 800) player.vy = 800;
// ── Move X ──
player.x += player.vx * dt;
// World bounds
if (player.x < 0) { player.x = 0; player.vx = 0; }
if (player.x + player.w > LEVEL_WIDTH) { player.x = LEVEL_WIDTH - player.w; player.vx = 0; }
// ── Collide X with platforms ──
for (const p of level.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;
}
}
// ── Move Y ──
player.y += player.vy * dt;
player.onGround = false;
// ── Collide Y with platforms ──
for (const p of level.platforms) {
if (aabb(player, p)) {
if (player.vy > 0) {
player.y = p.y - player.h;
player.onGround = true;
player.vy = 0;
} else if (player.vy < 0) {
player.y = p.y + p.h;
player.vy = 0;
}
}
}
// ── Fall off screen ──
if (player.y > H + 50) {
killPlayer();
return;
}
// ── Animation ──
player.animTimer += dt;
if (player.animTimer > 0.12) {
player.animTimer = 0;
player.animFrame = (player.animFrame + 1) % 4;
}
// ── Coins ──
for (let i = level.coins.length - 1; i >= 0; i--) {
const c = level.coins[i];
if (!c.collected && c.x > player.x - 20 && c.x < player.x + player.w + 20 &&
c.y > player.y - 20 && c.y < player.y + player.h + 20) {
c.collected = true;
coinCount++;
hudEl.textContent = `COINS: ${coinCount} / ${level.totalCoins} LIVES: ${player.lives}`;
}
}
// ── Enemies ──
for (const e of level.enemies) {
if (!e.alive) {
e.squishTimer -= dt;
continue;
}
// Patrol
e.x += e.vx * dt;
if (e.x <= e.minX || e.x + e.w >= e.maxX) {
e.vx *= -1;
}
// Gravity for enemies
e.vy += GRAVITY * dt;
e.y += e.vy * dt;
// Ground collision for enemies
e.onGround = false;
for (const p of level.platforms) {
if (aabb(e, p)) {
if (e.vy > 0) {
e.y = p.y - e.h;
e.vy = 0;
e.onGround = true;
}
}
}
// Remove if fallen
if (e.y > H + 100) {
e.alive = false;
continue;
}
// Player collision
if (player.invincibleTimer <= 0 && aabb(player, e)) {
// Check if player is stomping (falling and above enemy center)
if (player.vy > 0 && player.y + player.h - 8 < e.y + e.h * 0.5) {
// Stomp!
e.alive = false;
e.squishTimer = 0.5;
player.vy = JUMP_VEL * 0.5; // bounce
} else {
// Hurt
killPlayer();
return;
}
}
}
// ── Flag (goal) ──
const flag = level.flag;
if (player.x + player.w > flag.x && player.x < flag.x + 20 &&
player.y + player.h > flag.y && player.y < flag.y + 200) {
gameState = "won";
showMessage(
`🎉 YOU WIN! 🎉`,
`Coins: ${coinCount} / ${level.totalCoins} Lives left: ${player.lives}`,
"Press ENTER to play again"
);
}
// ── Camera ──
const targetX = player.x - W / 3;
camera.x = lerp(camera.x, Math.max(0, Math.min(targetX, LEVEL_WIDTH - W)), 8 * dt);
}
function killPlayer() {
if (!player.alive) return;
player.alive = false;
player.vy = -400;
player.vx = 0;
player.deathTimer = 1.2;
}
function respawnPlayer() {
player.x = 60;
player.y = GROUND_Y - PLAYER_H;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.coyoteTimer = 0;
player.jumpBufferTimer = 0;
player.invincibleTimer = 2.0;
player.alive = true;
camera.x = 0;
}
function showMessage(title, sub, subSub) {
msgText.textContent = title;
msgSub.textContent = sub + "\n" + (subSub || "");
msgOverlay.style.display = "flex";
}
function hideMessage() {
msgOverlay.style.display = "none";
}
// ─── Drawing ───────────────────────────────────────────────
// Clouds (parallax)
const clouds = [];
for (let i = 0; i < 30; i++) {
clouds.push({
x: Math.random() * LEVEL_WIDTH,
y: 20 + Math.random() * 100,
w: 60 + Math.random() * 80,
h: 25 + Math.random() * 20,
speed: 0.1 + Math.random() * 0.2,
});
}
// Hills (parallax background)
const hills = [];
for (let i = 0; i < 20; i++) {
hills.push({
x: i * 300 + Math.random() * 100,
w: 120 + Math.random() * 100,
h: 50 + Math.random() * 60,
});
}
function drawBackground() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#5c94fc");
grad.addColorStop(0.7, "#87CEEB");
grad.addColorStop(1, "#b8e4ff");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Hills (parallax at 0.2x)
ctx.fillStyle = "#4aad4a";
for (const h of hills) {
const sx = h.x - camera.x * 0.2;
if (sx + h.w < -50 || sx > W + 50) continue;
ctx.beginPath();
ctx.ellipse(sx + h.w / 2, GROUND_Y - camera.y * 0 + 20, h.w / 2, h.h, 0, Math.PI, 0);
ctx.fill();
}
// Clouds (parallax at 0.15x)
ctx.fillStyle = "rgba(255,255,255,0.85)";
for (const c of clouds) {
const sx = c.x - camera.x * 0.15;
const wrapX = ((sx % (W + 200)) + (W + 200)) % (W + 200) - 100;
ctx.beginPath();
ctx.ellipse(wrapX, c.y, c.w / 2, c.h / 2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(wrapX - c.w * 0.25, c.y + 5, c.w * 0.3, c.h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(wrapX + c.w * 0.25, c.y + 3, c.w * 0.35, c.h * 0.45, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPlatform(p) {
const sx = p.x - camera.x;
const sy = p.y - camera.y;
if (sx + p.w < -10 || sx > W + 10) return;
if (p.h <= 20) {
// Raised platform (brick style)
ctx.fillStyle = "#c84c09";
ctx.fillRect(sx, sy, p.w, p.h);
// Brick lines
ctx.strokeStyle = "#a03800";
ctx.lineWidth = 1;
for (let bx = sx; bx < sx + p.w; bx += 16) {
ctx.beginPath();
ctx.moveTo(bx, sy);
ctx.lineTo(bx, sy + p.h);
ctx.stroke();
}
// Top highlight
ctx.fillStyle = "#e87040";
ctx.fillRect(sx, sy, p.w, 3);
// Bottom shadow
ctx.fillStyle = "#8b3000";
ctx.fillRect(sx, sy + p.h - 3, p.w, 3);
} else {
// Ground
ctx.fillStyle = "#c84c09";
ctx.fillRect(sx, sy, p.w, p.h);
// Grass top
ctx.fillStyle = "#4aad4a";
ctx.fillRect(sx, sy, p.w, 6);
// Grass detail
ctx.fillStyle = "#5cc85c";
for (let gx = sx; gx < sx + p.w; gx += 8) {
ctx.fillRect(gx, sy, 2, 8);
}
// Dirt texture
ctx.fillStyle = "#a03800";
for (let dx = sx + 4; dx < sx + p.w - 4; dx += 12) {
ctx.fillRect(dx, sy + 10, 4, 3);
ctx.fillRect(dx + 6, sy + 18, 4, 3);
}
}
}
function drawPlayer() {
const sx = player.x - camera.x;
const sy = player.y - camera.y;
// Blink when invincible
if (player.invincibleTimer > 0 && Math.floor(player.invincibleTimer * 10) % 2 === 0) return;
ctx.save();
ctx.translate(sx + player.w / 2, sy + player.h / 2);
ctx.scale(player.facing, 1);
ctx.translate(-player.w / 2, -player.h / 2);
// Death: flip upside down briefly
if (!player.alive) {
ctx.translate(player.w / 2, player.h / 2);
ctx.rotate(Math.PI);
ctx.translate(-player.w / 2, -player.h / 2);
}
const px = 0;
const py = 0;
// Hat
ctx.fillStyle = "#e44040";
ctx.fillRect(px + 2, py, 20, 8);
ctx.fillRect(px, py + 2, 24, 6);
// Face
ctx.fillStyle = "#ffcc88";
ctx.fillRect(px + 4, py + 8, 16, 12);
// Eyes
ctx.fillStyle = "#222";
ctx.fillRect(px + 14, py + 10, 4, 4);
// Mustache
ctx.fillStyle = "#5c3000";
ctx.fillRect(px + 10, py + 15, 12, 3);
// Body (overalls)
ctx.fillStyle = "#3060c8";
ctx.fillRect(px + 2, py + 20, 20, 8);
// Overalls buttons
ctx.fillStyle = "#ffd700";
ctx.fillRect(px + 8, py + 22, 3, 3);
ctx.fillRect(px + 14, py + 22, 3, 3);
// Legs
ctx.fillStyle = "#3060c8";
const legOffset = player.onGround && Math.abs(player.vx) > 10
? Math.sin(player.animFrame * Math.PI / 2) * 3
: 0;
ctx.fillRect(px + 2, py + 28, 8, 8);
ctx.fillRect(px + 14, py + 28, 8, 8);
// Shoes
ctx.fillStyle = "#8b4513";
ctx.fillRect(px + (legOffset > 0 ? 0 : 2), py + 33, 10, 4);
ctx.fillRect(px + (legOffset > 0 ? 12 : 14), py + 33, 10, 4);
ctx.restore();
}
function drawCoin(c) {
if (c.collected) return;
const sx = c.x - camera.x;
const sy = c.y - camera.y;
if (sx < -20 || sx > W + 20 || sy < -20 || sy > H + 20) return;
const t = performance.now() / 300;
const scaleX = Math.abs(Math.cos(t + c.x * 0.01));
ctx.save();
ctx.translate(sx, sy);
ctx.scale(scaleX, 1);
// Coin body
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.ellipse(0, 0, 10, 12, 0, 0, Math.PI * 2);
ctx.fill();
// Coin highlight
ctx.fillStyle = "#ffec80";
ctx.beginPath();
ctx.ellipse(-2, -3, 5, 7, -0.3, 0, Math.PI * 2);
ctx.fill();
// Dollar sign
ctx.fillStyle = "#c8a000";
ctx.font = "bold 12px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 1, 1);
ctx.restore();
}
function drawEnemy(e) {
const sx = e.x - camera.x;
const sy = e.y - camera.y;
if (sx < -40 || sx > W + 40) return;
if (!e.alive) {
// Squished
ctx.fillStyle = "#a0522d";
ctx.fillRect(sx, sy + e.h - 6, e.w, 6);
return;
}
// Body (Goomba-style)
ctx.fillStyle = "#a0522d";
ctx.beginPath();
ctx.ellipse(sx + e.w / 2, sy + e.h * 0.4, e.w / 2, e.h * 0.45, 0, Math.PI, 0);
ctx.fill();
ctx.fillRect(sx + 2, sy + e.h * 0.4, e.w - 4, e.h * 0.55);
// Feet
const walk = Math.sin(performance.now() / 150) * 2;
ctx.fillStyle = "#222";
ctx.fillRect(sx + 2 + walk, sy + e.h - 4, 8, 4);
ctx.fillRect(sx + e.w - 10 - walk, sy + e.h - 4, 8, 4);
// Eyes
ctx.fillStyle = "#fff";
ctx.fillRect(sx + 5, sy + 6, 7, 8);
ctx.fillRect(sx + e.w - 12, sy + 6, 7, 8);
ctx.fillStyle = "#222";
const dir = e.vx > 0 ? 2 : 0;
ctx.fillRect(sx + 6 + dir, sy + 8, 4, 5);
ctx.fillRect(sx + e.w - 11 + dir, sy + 8, 4, 5);
// Angry eyebrows
ctx.fillStyle = "#222";
ctx.fillRect(sx + 4, sy + 4, 8, 2);
ctx.fillRect(sx + e.w - 12, sy + 4, 8, 2);
}
function drawFlag() {
const f = level.flag;
const sx = f.x - camera.x;
const sy = f.y - camera.y;
if (sx < -50 || sx > W + 50) return;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(sx, sy, 4, 200);
// Ball on top
ctx.fillStyle = "#ffd700";
ctx.beginPath();
ctx.arc(sx + 2, sy, 6, 0, Math.PI * 2);
ctx.fill();
// Flag
const wave = Math.sin(performance.now() / 400) * 3;
ctx.fillStyle = "#e44040";
ctx.beginPath();
ctx.moveTo(sx + 4, sy + 8);
ctx.lineTo(sx + 44 + wave, sy + 20);
ctx.lineTo(sx + 4, sy + 40);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#ffd700";
ctx.font = "14px serif";
ctx.fillText("★", sx + 14 + wave * 0.5, sy + 30);
// Base
ctx.fillStyle = "#888";
ctx.fillRect(sx - 6, sy + 196, 16, 8);
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
ctx.save();
ctx.translate(0, camera.y);
// Platforms
for (const p of level.platforms) {
drawPlatform(p);
}
// Coins
for (const c of level.coins) {
drawCoin(c);
}
// Enemies
for (const e of level.enemies) {
drawEnemy(e);
}
// Flag
drawFlag();
// Player
drawPlayer();
ctx.restore();
}
// ─── Game Loop ─────────────────────────────────────────────
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
// Clamp dt to avoid spiral of death
if (dt > 0.05) dt = 0.05;
// Handle restart
if ((gameState === "won" || gameState === "dead") && keys["Enter"]) {
coinCount = 0;
initGame();
}
update(dt);
draw();
requestAnimationFrame(gameLoop);
}
// ─── Start ─────────────────────────────────────────────────
initGame();
requestAnimationFrame(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: #000;
display: flex; justify-content: center; align-items: center;
overflow: hidden;
font-family: 'Courier New', monospace;
}
canvas {
display: block;
image-rendering: pixelated;
border: 2px solid #333;
}
#ui-overlay {
position: absolute;
top: 10px; left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 18px;
font-weight: bold;
text-shadow: 2px 2px 0 #000;
pointer-events: none;
z-index: 10;
}
#message-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 28px;
font-weight: bold;
z-index: 20;
text-align: center;
line-height: 1.6;
}
#message-overlay .sub {
font-size: 16px;
margin-top: 10px;
font-weight: normal;
color: #ccc;
}
</style>
</head>
<body>
<div id="ui-overlay">
<span id="hud">COINS: 0 / 0 LIVES: 3</span>
</div>
<div id="message-overlay">
<div id="message-text"></div>
<div class="sub" id="message-sub"></div>
</div>
<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 Pixel Adventure — a Mario-style platformer
// ============================================================
(function () {
"use strict";
// ---- Canvas setup ----
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 MAX_FALL = 12;
const PLAYER_SPEED = 4.5;
const PLAYER_ACCEL = 0.6;
const PLAYER_DECEL = 0.75;
const JUMP_FORCE = -11.5;
const TILE = 32;
// ---- Input ----
const keys = {};
window.addEventListener("keydown", function (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", function (e) {
keys[e.code] = false;
});
// ---- World / Level ----
// World is defined in tile units (32px each).
// 0 = air, 1 = ground, 2 = platform, 3 = coin, 4 = flag pole, 5 = question block
const WORLD_WIDTH = 200; // tiles wide
const WORLD_HEIGHT = 14; // tiles tall
// Build the level map
const map = [];
for (let y = 0; y < WORLD_HEIGHT; y++) {
map[y] = [];
for (let x = 0; x < WORLD_WIDTH; x++) {
map[y][x] = 0;
}
}
// Ground — mostly solid with a couple of gaps
function setGround(y, startX, endX) {
for (let x = startX; x < endX; x++) {
map[y][x] = 1;
}
}
// Main ground with gaps
setGround(12, 0, 35);
setGround(12, 38, 65);
setGround(12, 68, 110);
setGround(12, 113, 150);
setGround(12, 153, WORLD_WIDTH);
// Extra ground layer for depth
setGround(13, 0, WORLD_WIDTH);
// Platforms at various heights
function setPlatform(y, startX, endX) {
for (let x = startX; x < endX; x++) {
map[y][x] = 2;
}
}
// Staircase section
setPlatform(10, 10, 13);
setPlatform(8, 13, 16);
setPlatform(6, 16, 19);
// Floating platforms
setPlatform(9, 28, 33);
setPlatform(7, 40, 45);
setPlatform(5, 43, 48);
// High platform
setPlatform(8, 55, 62);
setPlatform(6, 58, 65);
// Step-up after gap
setPlatform(10, 72, 75);
setPlatform(8, 75, 78);
setPlatform(6, 78, 81);
// Long floating platform
setPlatform(9, 90, 98);
setPlatform(7, 100, 106);
// Upper route
setPlatform(7, 118, 124);
setPlatform(5, 124, 130);
setPlatform(7, 130, 136);
// Final stretch
setPlatform(9, 140, 145);
setPlatform(7, 145, 150);
setPlatform(5, 150, 155);
// Question blocks (contain coins)
function setQuestion(y, x) {
map[y][x] = 5;
}
setQuestion(8, 11);
setQuestion(7, 44);
setQuestion(5, 60);
setQuestion(5, 126);
// Coins
function setCoin(y, x) {
map[y][x] = 3;
}
// Coins on the ground
setCoin(11, 5);
setCoin(11, 6);
setCoin(11, 7);
// Coins on platforms
setCoin(8, 29);
setCoin(8, 30);
setCoin(8, 31);
setCoin(6, 41);
setCoin(6, 42);
setCoin(4, 56);
setCoin(4, 57);
setCoin(4, 58);
setCoin(8, 73);
setCoin(8, 74);
setCoin(5, 92);
setCoin(5, 93);
setCoin(5, 94);
setCoin(5, 95);
setCoin(6, 102);
setCoin(6, 103);
// Coins after gap
setCoin(11, 70);
setCoin(11, 71);
setCoin(11, 72);
// High coins
setCoin(4, 120);
setCoin(4, 121);
setCoin(4, 122);
setCoin(4, 128);
setCoin(4, 129);
setCoin(4, 132);
setCoin(4, 133);
// Final coins
setCoin(8, 142);
setCoin(8, 143);
setCoin(8, 144);
setCoin(4, 152);
setCoin(4, 153);
setCoin(4, 154);
// Flag pole at the end
function setFlag(y, x) {
map[y][x] = 4;
}
// Flag pole — vertical column of flag tiles
for (let fy = 3; fy <= 11; fy++) {
map[fy][185] = 4;
}
// Flag top
map[2][185] = 4;
// Base
map[11][185] = 4;
map[12][185] = 1;
// ---- Player ----
const player = {
x: 3 * TILE,
y: 10 * TILE,
w: 24,
h: 30,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1 = right, -1 = left
frame: 0,
frameTimer: 0,
alive: true,
won: false,
winTimer: 0,
// Animation
walkFrame: 0,
walkTimer: 0,
jumpFrame: 0,
};
// ---- Camera ----
const camera = {
x: 0,
y: 0,
targetX: 0,
targetY: 0,
};
// ---- Coins (collected state) ----
let coins = [];
let score = 0;
let totalCoins = 0;
// Count total coins
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let x = 0; x < WORLD_WIDTH; x++) {
if (map[y][x] === 3) totalCoins++;
}
}
// ---- Particles ----
let particles = [];
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color: color,
size: 2 + Math.random() * 3,
});
}
}
// ---- Clouds (decoration) ----
let clouds = [];
for (let i = 0; i < 25; i++) {
clouds.push({
x: Math.random() * WORLD_WIDTH * TILE,
y: 20 + Math.random() * 100,
w: 60 + Math.random() * 80,
h: 25 + Math.random() * 20,
speed: 0.1 + Math.random() * 0.2,
});
}
// ---- Background hills ----
let hills = [];
for (let i = 0; i < 15; i++) {
hills.push({
x: i * 200 + Math.random() * 50,
w: 120 + Math.random() * 100,
h: 40 + Math.random() * 60,
color: `hsl(${120 + Math.random() * 30}, ${40 + Math.random() * 20}%, ${30 + Math.random() * 15}%)`,
});
}
// ---- Collision helpers ----
function getTile(px, py) {
const tx = Math.floor(px / TILE);
const ty = Math.floor(py / TILE);
if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) return 0;
return map[ty][tx];
}
function isSolid(tileType) {
return tileType === 1 || tileType === 2 || tileType === 5;
}
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
);
}
// ---- Game state ----
let gameState = "playing"; // "playing", "won"
let gameTime = 0;
// ---- Update ----
function update() {
gameTime++;
if (gameState === "won") {
player.winTimer++;
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.1;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
return;
}
// ---- Player input ----
let moveX = 0;
if (keys["ArrowLeft"] || keys["KeyA"]) moveX -= 1;
if (keys["ArrowRight"] || keys["KeyD"]) moveX += 1;
// Acceleration / deceleration
if (moveX !== 0) {
player.vx += moveX * PLAYER_ACCEL;
if (player.vx > PLAYER_SPEED) player.vx = PLAYER_SPEED;
if (player.vx < -PLAYER_SPEED) player.vx = -PLAYER_SPEED;
player.facing = moveX;
// Walking animation
player.walkTimer++;
if (player.walkTimer > 6) {
player.walkTimer = 0;
player.walkFrame = (player.walkFrame + 1) % 4;
}
} else {
// Deceleration
if (player.vx > 0) {
player.vx -= PLAYER_DECEL;
if (player.vx < 0) player.vx = 0;
} else if (player.vx < 0) {
player.vx += PLAYER_DECEL;
if (player.vx > 0) player.vx = 0;
}
player.walkFrame = 0;
player.walkTimer = 0;
}
// Jump
if ((keys["Space"] || keys["ArrowUp"] || keys["KeyW"]) && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
}
// Apply gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// ---- Horizontal collision ----
player.x += player.vx;
resolveCollisionX();
// ---- Vertical collision ----
player.y += player.vy;
player.onGround = false;
resolveCollisionY();
// ---- Coin collection ----
const pcx = player.x + player.w / 2;
const pcy = player.y + player.h / 2;
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let x = 0; x < WORLD_WIDTH; x++) {
if (map[y][x] === 3) {
const cx = x * TILE + TILE / 2;
const cy = y * TILE + TILE / 2;
const dx = pcx - cx;
const dy = pcy - cy;
if (Math.sqrt(dx * dx + dy * dy) < 24) {
map[y][x] = 0;
score++;
spawnParticles(cx, cy, "#FFD700", 8);
}
}
}
}
// ---- Question block hit (from below) ----
if (player.vy < 0) {
const headTileX = Math.floor((player.x + player.w / 2) / TILE);
const headTileY = Math.floor((player.y - 2) / TILE);
if (headTileY >= 0 && headTileY < WORLD_HEIGHT && headTileX >= 0 && headTileX < WORLD_WIDTH) {
if (map[headTileY][headTileX] === 5) {
map[headTileY][headTileX] = 2; // turn into solid block
score += 5;
spawnParticles(headTileX * TILE + TILE / 2, headTileY * TILE, "#FFD700", 12);
}
}
}
// ---- Flag / goal ----
const flagTileX = Math.floor(pcx / TILE);
const flagTileY = Math.floor(pcy / TILE);
if (flagTileX >= 0 && flagTileX < WORLD_WIDTH && flagTileY >= 0 && flagTileY < WORLD_HEIGHT) {
if (map[flagTileY][flagTileX] === 4) {
gameState = "won";
player.won = true;
player.winTimer = 0;
spawnParticles(pcx, pcy, "#FF6B6B", 20);
spawnParticles(pcx, pcy, "#4ECDC4", 20);
spawnParticles(pcx, pcy, "#FFE66D", 20);
}
}
// ---- Fall off screen ----
if (player.y > H + 50) {
// Respawn
player.x = 3 * TILE;
player.y = 10 * TILE;
player.vx = 0;
player.vy = 0;
}
// ---- Camera follow ----
camera.targetX = player.x - W / 3;
camera.targetY = Math.min(player.y - H / 2, 0);
// Clamp camera
if (camera.targetX < 0) camera.targetX = 0;
if (camera.targetX > WORLD_WIDTH * TILE - W) camera.targetX = WORLD_WIDTH * TILE - W;
if (camera.targetY > 0) camera.targetY = 0;
camera.x += (camera.targetX - camera.x) * 0.1;
camera.y += (camera.targetY - camera.y) * 0.08;
// ---- Update particles ----
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.1;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
// ---- Update clouds ----
for (let c of clouds) {
c.x += c.speed;
if (c.x > WORLD_WIDTH * TILE + 100) c.x = -c.w - 50;
}
}
function resolveCollisionX() {
const left = Math.floor(player.x / TILE);
const right = Math.floor((player.x + player.w - 1) / TILE);
const top = Math.floor(player.y / TILE);
const bottom = Math.floor((player.y + player.h - 1) / TILE);
for (let ty = top; ty <= bottom; ty++) {
for (let tx = left; tx <= right; tx++) {
if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) continue;
if (isSolid(map[ty][tx])) {
if (player.vx > 0) {
player.x = tx * TILE - player.w;
player.vx = 0;
} else if (player.vx < 0) {
player.x = (tx + 1) * TILE;
player.vx = 0;
}
}
}
}
}
function resolveCollisionY() {
const left = Math.floor(player.x / TILE);
const right = Math.floor((player.x + player.w - 1) / TILE);
const top = Math.floor(player.y / TILE);
const bottom = Math.floor((player.y + player.h - 1) / TILE);
for (let ty = top; ty <= bottom; ty++) {
for (let tx = left; tx <= right; tx++) {
if (tx < 0 || tx >= WORLD_WIDTH || ty < 0 || ty >= WORLD_HEIGHT) continue;
if (isSolid(map[ty][tx])) {
if (player.vy > 0) {
player.y = ty * TILE - player.h;
player.vy = 0;
player.onGround = true;
} else if (player.vy < 0) {
player.y = (ty + 1) * TILE;
player.vy = 0;
}
}
}
}
}
// ---- Drawing ----
function drawSky() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#4FC3F7");
grad.addColorStop(0.5, "#81D4FA");
grad.addColorStop(1, "#B3E5FC");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawClouds() {
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
for (let c of clouds) {
const sx = c.x - camera.x * 0.3;
const sy = c.y;
// Wrap for seamless scrolling
const wrappedX = ((sx % (WORLD_WIDTH * TILE + 200)) + WORLD_WIDTH * TILE + 200) % (WORLD_WIDTH * TILE + 200) - 100;
if (wrappedX > -c.w && wrappedX < W + c.w) {
drawCloud(wrappedX, sy, c.w, c.h);
}
}
}
function drawCloud(x, y, w, h) {
ctx.beginPath();
ctx.ellipse(x + w * 0.3, y + h * 0.5, w * 0.3, h * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + w * 0.6, y + h * 0.35, w * 0.35, h * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + w * 0.75, y + h * 0.55, w * 0.25, h * 0.45, 0, 0, Math.PI * 2);
ctx.fill();
}
function drawHills() {
for (let h of hills) {
const sx = h.x - camera.x * 0.5;
if (sx > -h.w && sx < W + h.w) {
ctx.fillStyle = h.color;
ctx.beginPath();
ctx.moveTo(sx - h.w / 2, 12 * TILE);
ctx.quadraticCurveTo(sx, 12 * TILE - h.h, sx + h.w / 2, 12 * TILE);
ctx.fill();
}
}
}
function drawTile(tx, ty, type) {
const sx = tx * TILE - camera.x;
const sy = ty * TILE - camera.y;
// Skip if off screen
if (sx < -TILE || sx > W + TILE || sy < -TILE || sy > H + TILE) return;
if (type === 1) {
// Ground block
ctx.fillStyle = "#8B5E3C";
ctx.fillRect(sx, sy, TILE, TILE);
// Grass top
ctx.fillStyle = "#4CAF50";
ctx.fillRect(sx, sy, TILE, 6);
ctx.fillStyle = "#388E3C";
ctx.fillRect(sx, sy, TILE, 3);
// Dirt texture
ctx.fillStyle = "#795548";
ctx.fillRect(sx + 4, sy + 12, 6, 4);
ctx.fillRect(sx + 18, sy + 20, 8, 4);
ctx.fillRect(sx + 8, sy + 24, 5, 4);
} else if (type === 2) {
// Platform (brick)
ctx.fillStyle = "#C0392B";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#E74C3C";
ctx.fillRect(sx + 1, sy + 1, TILE - 2, TILE / 2 - 2);
// Brick lines
ctx.fillStyle = "#922B21";
ctx.fillRect(sx, sy + TILE / 2 - 1, TILE, 2);
ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE / 2);
ctx.fillRect(sx + TILE / 4, sy + TILE / 2, 2, TILE / 2);
ctx.fillRect(sx + TILE * 3 / 4, sy + TILE / 2, 2, TILE / 2);
// Highlight
ctx.fillStyle = "#FF6B6B";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, 2);
} else if (type === 3) {
// Coin
const coinX = sx + TILE / 2;
const coinY = sy + TILE / 2;
const bob = Math.sin(gameTime * 0.08 + tx * 0.5) * 3;
const stretch = Math.abs(Math.cos(gameTime * 0.06 + tx * 0.3));
ctx.save();
ctx.translate(coinX, coinY + bob);
ctx.scale(stretch, 1);
// Coin body
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2);
ctx.fill();
// Coin highlight
ctx.fillStyle = "#FFF176";
ctx.beginPath();
ctx.arc(-2, -3, 5, 0, Math.PI * 2);
ctx.fill();
// Coin border
ctx.strokeStyle = "#F57F17";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
} else if (type === 4) {
// Flag pole
if (ty === 2) {
// Flag top (ball)
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(sx + TILE / 2, sy + TILE / 2, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#FFF176";
ctx.beginPath();
ctx.arc(sx + TILE / 2 - 2, sy + TILE / 2 - 2, 3, 0, Math.PI * 2);
ctx.fill();
} else if (ty === 3) {
// Flag cloth
const wave = Math.sin(gameTime * 0.1) * 3;
ctx.fillStyle = "#E94560";
ctx.beginPath();
ctx.moveTo(sx + TILE / 2, sy);
ctx.lineTo(sx + TILE * 1.5 + wave, sy + 8);
ctx.lineTo(sx + TILE / 2, sy + 16);
ctx.fill();
ctx.fillStyle = "#FF6B6B";
ctx.beginPath();
ctx.moveTo(sx + TILE / 2, sy + 3);
ctx.lineTo(sx + TILE * 1.3 + wave * 0.7, sy + 8);
ctx.lineTo(sx + TILE / 2, sy + 13);
ctx.fill();
} else if (ty >= 4 && ty <= 10) {
// Pole
ctx.fillStyle = "#9E9E9E";
ctx.fillRect(sx + TILE / 2 - 2, sy, 4, TILE);
ctx.fillStyle = "#BDBDBD";
ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE);
} else {
// Flag base
ctx.fillStyle = "#7F8C8D";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#95A5A6";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE - 4);
}
} else if (type === 5) {
// Question block
const pulse = Math.sin(gameTime * 0.05) * 0.1;
ctx.fillStyle = "#FFB300";
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = "#FFD54F";
ctx.fillRect(sx + 2, sy + 2, TILE - 4, TILE / 2 - 3);
// Question mark
ctx.fillStyle = "#E65100";
ctx.font = "bold 18px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", sx + TILE / 2, sy + TILE / 2 + pulse * 5);
// Border
ctx.strokeStyle = "#E65100";
ctx.lineWidth = 2;
ctx.strokeRect(sx + 1, sy + 1, TILE - 2, TILE - 2);
}
}
function drawPlayer() {
const sx = player.x - camera.x;
const sy = player.y - camera.y;
const f = player.facing;
// Shadow
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.beginPath();
ctx.ellipse(sx + player.w / 2, sy + player.h + 2, player.w / 2 + 2, 4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.save();
if (f === -1) {
ctx.translate(sx + player.w / 2, 0);
ctx.scale(-1, 1);
ctx.translate(-(sx + player.w / 2), 0);
}
// Body
ctx.fillStyle = "#E94560";
ctx.fillRect(sx + 4, sy + 10, 16, 14);
// Overalls
ctx.fillStyle = "#2C3E50";
ctx.fillRect(sx + 4, sy + 18, 16, 8);
// Head
ctx.fillStyle = "#FDBCB4";
ctx.fillRect(sx + 4, sy, 16, 12);
// Hat
ctx.fillStyle = "#E94560";
ctx.fillRect(sx + 2, sy - 4, 20, 6);
ctx.fillRect(sx + 6, sy - 8, 14, 6);
// Hat brim
ctx.fillStyle = "#C0392B";
ctx.fillRect(sx + 2, sy - 1, 22, 3);
// Eyes
ctx.fillStyle = "#2C3E50";
ctx.fillRect(sx + 12, sy + 3, 3, 4);
// Mustache
ctx.fillStyle = "#5D4037";
ctx.fillRect(sx + 10, sy + 8, 8, 2);
// Legs animation
if (!player.onGround) {
// Jumping pose
ctx.fillStyle = "#2C3E50";
ctx.fillRect(sx + 2, sy + 24, 6, 6);
ctx.fillRect(sx + 16, sy + 24, 6, 6);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(sx, sy + 28, 8, 4);
ctx.fillRect(sx + 16, sy + 28, 8, 4);
} else if (Math.abs(player.vx) > 0.5) {
// Walking
const legOffset = Math.sin(player.walkFrame * Math.PI / 2) * 4;
ctx.fillStyle = "#2C3E50";
ctx.fillRect(sx + 4 + legOffset, sy + 24, 6, 4);
ctx.fillRect(sx + 14 - legOffset, sy + 24, 6, 4);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(sx + 2 + legOffset, sy + 26, 8, 4);
ctx.fillRect(sx + 14 - legOffset, sy + 26, 8, 4);
} else {
// Standing
ctx.fillStyle = "#2C3E50";
ctx.fillRect(sx + 4, sy + 24, 6, 4);
ctx.fillRect(sx + 14, sy + 24, 6, 4);
// Shoes
ctx.fillStyle = "#5D4037";
ctx.fillRect(sx + 2, sy + 26, 8, 4);
ctx.fillRect(sx + 14, sy + 26, 8, 4);
}
// Arms
ctx.fillStyle = "#FDBCB4";
if (!player.onGround) {
ctx.fillRect(sx - 2, sy + 10, 6, 4);
ctx.fillRect(sx + 20, sy + 10, 6, 4);
} else {
const armSwing = Math.sin(player.walkFrame * Math.PI / 2) * 3;
ctx.fillRect(sx - 2, sy + 14 + armSwing, 6, 4);
ctx.fillRect(sx + 20, sy + 14 - armSwing, 6, 4);
}
ctx.restore();
}
function drawParticles() {
for (let p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - camera.x, p.y - camera.y, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score background
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(10, 10, 160, 40);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(10, 10, 160, 40);
// Coins
ctx.fillStyle = "#FFD700";
ctx.font = "bold 16px monospace";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("🪙 × " + score, 20, 28);
// Total coins
ctx.fillStyle = "#BDBDBD";
ctx.font = "12px monospace";
ctx.fillText("Total: " + score + " / " + totalCoins, 20, 44);
// Controls hint (fades out)
if (gameTime < 300) {
const alpha = Math.max(0, 1 - gameTime / 300);
ctx.globalAlpha = alpha;
ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
ctx.fillRect(W / 2 - 180, H - 50, 360, 36);
ctx.fillStyle = "#FFFFFF";
ctx.font = "14px monospace";
ctx.textAlign = "center";
ctx.fillText("← → or A D to move | SPACE or ↑ to jump | Reach the flag!", W / 2, H - 32);
ctx.globalAlpha = 1;
}
}
function drawWinScreen() {
// Darken
ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
ctx.fillRect(0, 0, W, H);
// Win box
const boxW = 400;
const boxH = 200;
const boxX = (W - boxW) / 2;
const boxY = (H - boxH) / 2;
ctx.fillStyle = "#2C3E50";
ctx.fillRect(boxX, boxY, boxW, boxH);
ctx.strokeStyle = "#E94560";
ctx.lineWidth = 3;
ctx.strokeRect(boxX, boxY, boxW, boxH);
// Title
ctx.fillStyle = "#FFD700";
ctx.font = "bold 36px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🏆 YOU WIN! 🏆", W / 2, boxY + 50);
// Score
ctx.fillStyle = "#FFFFFF";
ctx.font = "20px monospace";
ctx.fillText("Coins collected: " + score + " / " + totalCoins, W / 2, boxY + 100);
// Rating
let rating = "⭐";
if (score >= totalCoins) rating = "⭐⭐⭐";
else if (score >= totalCoins * 0.7) rating = "⭐⭐";
ctx.font = "28px monospace";
ctx.fillText(rating, W / 2, boxY + 140);
// Restart
if (player.winTimer > 60) {
ctx.fillStyle = "#4ECDC4";
ctx.font = "16px monospace";
ctx.fillText("Press SPACE to play again", W / 2, boxY + 180);
if (keys["Space"]) {
resetGame();
}
}
}
function resetGame() {
// Reset player
player.x = 3 * TILE;
player.y = 10 * TILE;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.alive = true;
player.won = false;
player.winTimer = 0;
// Reset camera
camera.x = 0;
camera.y = 0;
// Reset score
score = 0;
gameState = "playing";
gameTime = 0;
particles = [];
// Reset map coins and question blocks
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let x = 0; x < WORLD_WIDTH; x++) {
if (map[y][x] === 0) {
// Check original level for coins and question blocks
// We need to rebuild — let's store originals
}
}
}
rebuildLevel();
}
function rebuildLevel() {
// Rebuild the entire map
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let x = 0; x < WORLD_WIDTH; x++) {
map[y][x] = 0;
}
}
setGround(12, 0, 35);
setGround(12, 38, 65);
setGround(12, 68, 110);
setGround(12, 113, 150);
setGround(12, 153, WORLD_WIDTH);
setGround(13, 0, WORLD_WIDTH);
setPlatform(10, 10, 13);
setPlatform(8, 13, 16);
setPlatform(6, 16, 19);
setPlatform(9, 28, 33);
setPlatform(7, 40, 45);
setPlatform(5, 43, 48);
setPlatform(8, 55, 62);
setPlatform(6, 58, 65);
setPlatform(10, 72, 75);
setPlatform(8, 75, 78);
setPlatform(6, 78, 81);
setPlatform(9, 90, 98);
setPlatform(7, 100, 106);
setPlatform(7, 118, 124);
setPlatform(5, 124, 130);
setPlatform(7, 130, 136);
setPlatform(9, 140, 145);
setPlatform(7, 145, 150);
setPlatform(5, 150, 155);
setQuestion(8, 11);
setQuestion(7, 44);
setQuestion(5, 60);
setQuestion(4, 126);
setCoin(11, 5);
setCoin(11, 6);
setCoin(11, 7);
setCoin(8, 29);
setCoin(8, 30);
setCoin(8, 31);
setCoin(6, 41);
setCoin(6, 42);
setCoin(4, 56);
setCoin(4, 57);
setCoin(4, 58);
setCoin(8, 73);
setCoin(8, 74);
setCoin(5, 92);
setCoin(5, 93);
setCoin(5, 94);
setCoin(5, 95);
setCoin(6, 102);
setCoin(6, 103);
setCoin(11, 70);
setCoin(11, 71);
setCoin(11, 72);
setCoin(4, 120);
setCoin(4, 121);
setCoin(4, 122);
setCoin(4, 128);
setCoin(4, 129);
setCoin(4, 132);
setCoin(4, 133);
setCoin(8, 142);
setCoin(8, 143);
setCoin(8, 144);
setCoin(4, 152);
setCoin(4, 153);
setCoin(4, 154);
for (let fy = 3; fy <= 11; fy++) {
map[fy][185] = 4;
}
map[2][185] = 4;
map[11][185] = 4;
map[12][185] = 1;
}
// ---- Main render ----
function draw() {
ctx.clearRect(0, 0, W, H);
drawSky();
drawClouds();
drawHills();
// Draw tiles
const startTX = Math.max(0, Math.floor(camera.x / TILE) - 1);
const endTX = Math.min(WORLD_WIDTH, Math.ceil((camera.x + W) / TILE) + 1);
const startTY = Math.max(0, Math.floor(camera.y / TILE) - 1);
const endTY = Math.min(WORLD_HEIGHT, Math.ceil((camera.y + H) / TILE) + 1);
for (let ty = startTY; ty < endTY; ty++) {
for (let tx = startTX; tx < endTX; tx++) {
if (map[ty][tx] > 0) {
drawTile(tx, ty, map[ty][tx]);
}
}
}
drawParticles();
drawPlayer();
drawHUD();
if (gameState === "won") {
drawWinScreen();
}
}
// ---- Game loop ----
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ---- Start ----
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 Adventure</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
font-family: monospace;
}
canvas#game {
border: 3px solid #e94560;
border-radius: 4px;
image-rendering: pixelated;
image-rendering: crisp-edges;
box-shadow: 0 0 30px rgba(233, 69, 96, 0.3);
}
</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%)
<!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: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
canvas {
border: 2px solid #333;
display: block;
image-rendering: pixelated;
}
#info {
color: #aaa;
margin-top: 10px;
font-size: 14px;
text-align: center;
}
</style>
</head>
<body>
<canvas id="game" width="800" height="450"></canvas>
<div id="info">Arrow Keys / WASD to move • Space / Up to jump • R to restart</div>
<script>
(function() {
"use strict";
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var W = canvas.width;
var H = canvas.height;
// ─── Constants ───────────────────────────────────────────
var GRAVITY = 0.6;
var MOVE_SPEED = 4.5;
var JUMP_FORCE = -12;
var FRICTION = 0.82;
var MAX_FALL = 14;
var TILE = 32;
var LEVEL_W = 200; // tiles wide
var LEVEL_H = Math.ceil(H / TILE) + 1;
// ─── Game State ──────────────────────────────────────────
var state = "playing"; // "playing" | "win" | "dead"
var score = 0;
var coinsCollected = 0;
var totalCoins = 0;
var cameraX = 0;
var particles = [];
var deathTimer = 0;
var winTimer = 0;
var frameCount = 0;
// ─── Input ───────────────────────────────────────────────
var keys = {};
document.addEventListener("keydown", function(e) {
keys[e.code] = true;
if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) !== -1) {
e.preventDefault();
}
if (e.code === "KeyR") restart();
});
document.addEventListener("keyup", function(e) {
keys[e.code] = false;
});
// ─── Level Data ──────────────────────────────────────────
// 0=air, 1=ground, 2=brick, 3=question block, 4=pipe_L, 5=pipe_T, 6=pipe_R
var levelTiles = [];
for (var y = 0; y < LEVEL_H; y++) {
levelTiles[y] = [];
for (var x = 0; x < LEVEL_W; x++) {
levelTiles[y][x] = 0;
}
}
// Ground (rows near bottom)
var groundRow = LEVEL_H - 2;
for (var x = 0; x < LEVEL_W; x++) {
// Gaps in ground
if ((x >= 28 && x <= 30) || (x >= 55 && x <= 58) || (x >= 90 && x <= 93) || (x >= 130 && x <= 133)) {
continue;
}
levelTiles[groundRow][x] = 1;
levelTiles[groundRow + 1][x] = 1;
}
// Platforms (floating)
var platforms = [
{x:8, y:groundRow-4, w:5, type:2},
{x:16, y:groundRow-5, w:3, type:3},
{x:22, y:groundRow-3, w:4, type:2},
{x:35, y:groundRow-5, w:6, type:2},
{x:37, y:groundRow-5, w:1, type:3}, // question block in platform
{x:45, y:groundRow-4, w:3, type:2},
{x:50, y:groundRow-7, w:4, type:2},
{x:62, y:groundRow-5, w:5, type:2},
{x:64, y:groundRow-5, w:1, type:3},
{x:70, y:groundRow-3, w:3, type:2},
{x:76, y:groundRow-6, w:4, type:2},
{x:82, y:groundRow-4, w:3, type:2},
{x:96, y:groundRow-5, w:6, type:2},
{x:98, y:groundRow-5, w:1, type:3},
{x:105, y:groundRow-3, w:4, type:2},
{x:112, y:groundRow-6, w:5, type:2},
{x:120, y:groundRow-4, w:3, type:2},
{x:136, y:groundRow-5, w:5, type:2},
{x:145, y:groundRow-3, w:4, type:2},
{x:155, y:groundRow-6, w:6, type:2},
{x:165, y:groundRow-4, w:4, type:2},
{x:175, y:groundRow-5, w:5, type:2},
];
for (var p = 0; p < platforms.length; p++) {
var pl = platforms[p];
for (var px = 0; px < pl.w; px++) {
var tx = pl.x + px;
var ty = pl.y;
if (tx < LEVEL_W && ty >= 0) {
levelTiles[ty][tx] = pl.type;
}
}
}
// Stairs near the end
var stairStart = 170;
for (var s = 0; s < 7; s++) {
for (var s2 = 0; s2 <= s; s2++) {
var stairX = stairStart + s;
var stairY = groundRow - 1 - s2;
if (stairX < LEVEL_W && stairY >= 0) {
levelTiles[stairY][stairX] = 2;
}
}
}
// ─── Coins ───────────────────────────────────────────────
var coins = [];
function addCoin(tx, ty) {
coins.push({
x: tx * TILE + 8,
y: ty * TILE + 6,
w: 16,
h: 20,
collected: false,
bobOffset: Math.random() * Math.PI * 2
});
totalCoins++;
}
// Coin placements
var coinPositions = [
{x:10,y:groundRow-5},{x:11,y:groundRow-5},{x:12,y:groundRow-5},
{x:17,y:groundRow-6},{x:18,y:groundRow-6},
{x:23,y:groundRow-4},{x:24,y:groundRow-4},
{x:37,y:groundRow-6}, // in question block area
{x:37,y:groundRow-9},{x:38,y:groundRow-9},
{x:46,y:groundRow-5},{x:47,y:groundRow-5},
{x:51,y:groundRow-8},{x:52,y:groundRow-8},
{x:63,y:groundRow-6},{x:64,y:groundRow-6},{x:65,y:groundRow-6},
{x:71,y:groundRow-4},{x:72,y:groundRow-4},
{x:77,y:groundRow-7},{x:78,y:groundRow-7},
{x:83,y:groundRow-5},{x:84,y:groundRow-5},
{x:97,y:groundRow-6},{x:98,y:groundRow-6},{x:99,y:groundRow-6},
{x:106,y:groundRow-4},{x:107,y:groundRow-4},
{x:113,y:groundRow-7},{x:114,y:groundRow-7},
{x:121,y:groundRow-5},{x:122,y:groundRow-5},
{x:137,y:groundRow-6},{x:138,y:groundRow-6},{x:139,y:groundRow-6},
{x:146,y:groundRow-4},{x:147,y:groundRow-4},
{x:156,y:groundRow-7},{x:157,y:groundRow-7},{x:158,y:groundRow-7},
{x:166,y:groundRow-5},{x:167,y:groundRow-5},
{x:176,y:groundRow-6},{x:177,y:groundRow-6},
];
for (var ci = 0; ci < coinPositions.length; ci++) {
addCoin(coinPositions[ci].x, coinPositions[ci].y);
}
// ─── Goal Flag ───────────────────────────────────────────
var flagX = 190 * TILE;
var flagY = groundRow - 8;
var flagReached = false;
// ─── Player ──────────────────────────────────────────────
var player = {
x: 3 * TILE,
y: (groundRow - 3) * TILE,
w: 24,
h: 30,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1=right, -1=left
animFrame: 0,
animTimer: 0,
walkCycle: 0
};
// ─── Enemies (simple goombas) ────────────────────────────
var enemies = [];
function addEnemy(tx, patrol) {
enemies.push({
x: tx * TILE,
y: (groundRow - 1) * TILE - 28,
w: 28,
h: 28,
vx: patrol || 1,
alive: true,
squishTimer: 0,
groundY: (groundRow - 1) * TILE - 28
});
}
var enemyPositions = [
{x:15, p:1}, {x:25, p:-1}, {x:40, p:1}, {x:48, p:-1},
{x:65, p:1}, {x:73, p:-1}, {x:88, p:1}, {x:100, p:-1},
{x:115, p:1}, {x:125, p:-1}, {x:140, p:1}, {x:150, p:-1},
{x:160, p:1}, {x:170, p:-1}
];
for (var ei = 0; ei < enemyPositions.length; ei++) {
addEnemy(enemyPositions[ei].x, enemyPositions[ei].p * 1.2);
}
// ─── Helpers ─────────────────────────────────────────────
function getTile(tx, ty) {
if (tx < 0 || tx >= LEVEL_W || ty < 0 || ty >= LEVEL_H) return 0;
return levelTiles[ty][tx];
}
function isSolid(type) {
return type === 1 || type === 2 || type === 3 || type === 4 || type === 5 || type === 6;
}
function rectCollide(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function spawnParticles(x, y, color, count) {
for (var i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.8) * 8,
life: 30 + Math.random() * 20,
maxLife: 50,
color: color,
size: 2 + Math.random() * 3
});
}
}
// ─── Restart ─────────────────────────────────────────────
function restart() {
player.x = 3 * TILE;
player.y = (groundRow - 3) * TILE;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.facing = 1;
score = 0;
coinsCollected = 0;
cameraX = 0;
particles = [];
deathTimer = 0;
winTimer = 0;
flagReached = false;
state = "playing";
for (var c = 0; c < coins.length; c++) {
coins[c].collected = false;
}
for (var e = 0; e < enemies.length; e++) {
var en = enemies[e];
en.alive = true;
en.squishTimer = 0;
en.vx = enemyPositions[e] ? enemyPositions[e].p * 1.2 : 1;
}
}
// ─── Update ──────────────────────────────────────────────
function update() {
if (state === "dead") {
deathTimer--;
if (deathTimer <= 0) restart();
updateParticles();
return;
}
if (state === "win") {
winTimer++;
updateParticles();
return;
}
frameCount++;
// ── Player Movement ──
var moveLeft = keys["ArrowLeft"] || keys["KeyA"];
var moveRight = keys["ArrowRight"] || keys["KeyD"];
var jumpKey = keys["Space"] || keys["ArrowUp"] || keys["KeyW"];
if (moveLeft) {
player.vx -= 1.8;
player.facing = -1;
}
if (moveRight) {
player.vx += 1.8;
player.facing = 1;
}
if (!moveLeft && !moveRight) {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.1) player.vx = 0;
}
// Clamp speed
if (player.vx > MOVE_SPEED) player.vx = MOVE_SPEED;
if (player.vx < -MOVE_SPEED) player.vx = -MOVE_SPEED;
// Jump
if (jumpKey && player.onGround) {
player.vy = JUMP_FORCE;
player.onGround = false;
spawnParticles(player.x + player.w/2, player.y + player.h, "#ddd", 5);
}
// Gravity
player.vy += GRAVITY;
if (player.vy > MAX_FALL) player.vy = MAX_FALL;
// ── Horizontal Collision ──
player.x += player.vx;
resolveCollisionX(player);
// ── Vertical Collision ──
player.y += player.vy;
player.onGround = false;
resolveCollisionY(player);
// Keep player in bounds
if (player.x < 0) { player.x = 0; player.vx = 0; }
// Fall death
if (player.y > H + 100) {
state = "dead";
deathTimer = 60;
spawnParticles(player.x + player.w/2, H, "#f44", 15);
}
// ── Animation ──
if (Math.abs(player.vx) > 0.5) {
player.walkCycle += Math.abs(player.vx) * 0.08;
player.animFrame = Math.floor(player.walkCycle) % 4;
} else {
player.animFrame = 0;
player.walkCycle = 0;
}
// ── Camera ──
var targetCam = player.x - W / 3;
if (targetCam < 0) targetCam = 0;
var maxCam = LEVEL_W * TILE - W;
if (targetCam > maxCam) targetCam = maxCam;
cameraX += (targetCam - cameraX) * 0.1;
// ── Coins ──
for (var c = 0; c < coins.length; c++) {
var coin = coins[c];
if (coin.collected) continue;
if (rectCollide(player, coin)) {
coin.collected = true;
coinsCollected++;
score += 10;
spawnParticles(coin.x + coin.w/2, coin.y + coin.h/2, "#FFD700", 8);
}
}
// ── Enemies ──
for (var e = 0; e < enemies.length; e++) {
var en = enemies[e];
if (!en.alive) {
if (en.squishTimer > 0) en.squishTimer--;
continue;
}
en.x += en.vx;
// Enemy-ground collision
var etx1 = Math.floor(en.x / TILE);
var etx2 = Math.floor((en.x + en.w) / TILE);
var ety = Math.floor((en.y + en.h) / TILE);
// Check if on ground
var onBlock = false;
for (var eex = etx1; eex <= etx2; eex++) {
if (isSolid(getTile(eex, ety))) { onBlock = true; break; }
}
if (!onBlock) {
en.y += 2;
} else {
en.y = en.groundY;
}
// Wall collision for enemies
var frontX = en.vx > 0 ? en.x + en.w : en.x;
var ftx = Math.floor(frontX / TILE);
var fty = Math.floor((en.y + en.h * 0.5) / TILE);
if (isSolid(getTile(ftx, fty))) {
en.vx = -en.vx;
en.x += en.vx * 2;
}
// Edge detection - turn around at gaps
var belowX = en.vx > 0 ? en.x + en.w + 2 : en.x - 2;
var btx = Math.floor(belowX / TILE);
var bty = Math.floor((en.y + en.h + 2) / TILE);
if (!isSolid(getTile(btx, bty))) {
en.vx = -en.vx;
}
// Player-enemy collision
if (rectCollide(player, en)) {
// Stomp from above
if (player.vy > 0 && player.y + player.h - 8 < en.y + en.h * 0.5) {
en.alive = false;
en.squishTimer = 30;
player.vy = JUMP_FORCE * 0.6;
score += 20;
spawnParticles(en.x + en.w/2, en.y + en.h/2, "#8B4513", 8);
} else {
// Player dies
state = "dead";
deathTimer = 60;
spawnParticles(player.x + player.w/2, player.y + player.h/2, "#f44", 15);
}
}
}
// ── Flag / Goal ──
if (!flagReached && player.x + player.w > flagX && player.x < flagX + 20) {
flagReached = true;
state = "win";
winTimer = 0;
score += 100;
spawnParticles(flagX + 10, flagY + 60, "#00FF00", 20);
spawnParticles(flagX + 10, flagY + 60, "#FFD700", 15);
}
updateParticles();
}
function resolveCollisionX(obj) {
var left = Math.floor(obj.x / TILE);
var right = Math.floor((obj.x + obj.w - 1) / TILE);
var top = Math.floor(obj.y / TILE);
var bot = Math.floor((obj.y + obj.h - 1) / TILE);
for (var ty = top; ty <= bot; ty++) {
for (var tx = left; tx <= right; tx++) {
if (isSolid(getTile(tx, ty))) {
if (obj.vx > 0) {
obj.x = tx * TILE - obj.w;
obj.vx = 0;
} else if (obj.vx < 0) {
obj.x = (tx + 1) * TILE;
obj.vx = 0;
}
}
}
}
}
function resolveCollisionY(obj) {
var left = Math.floor(obj.x / TILE);
var right = Math.floor((obj.x + obj.w - 1) / TILE);
var top = Math.floor(obj.y / TILE);
var bot = Math.floor((obj.y + obj.h - 1) / TILE);
for (var ty = top; ty <= bot; ty++) {
for (var tx = left; tx <= right; tx++) {
if (isSolid(getTile(tx, ty))) {
if (obj.vy > 0) {
obj.y = ty * TILE - obj.h;
obj.vy = 0;
obj.onGround = true;
} else if (obj.vy < 0) {
obj.y = (ty + 1) * TILE;
obj.vy = 0;
}
}
}
}
}
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.2;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
// ─── Drawing ─────────────────────────────────────────────
function draw() {
// Sky gradient
var skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, "#5C94FC");
skyGrad.addColorStop(0.7, "#87CEEB");
skyGrad.addColorStop(1, "#B0E0FF");
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Clouds (parallax)
drawClouds();
// Hills (parallax)
drawHills();
ctx.save();
ctx.translate(-Math.round(cameraX), 0);
// Draw tiles
drawTiles();
// Draw coins
drawCoins();
// Draw enemies
drawEnemies();
// Draw flag
drawFlag();
// Draw player
drawPlayer();
// Draw particles
drawParticles();
ctx.restore();
// HUD
drawHUD();
// Overlays
if (state === "win") drawWinScreen();
if (state === "dead") drawDeathScreen();
}
function drawClouds() {
ctx.fillStyle = "rgba(255,255,255,0.8)";
var cloudPositions = [
{x:100,y:50,s:1.2},{x:400,y:30,s:0.8},{x:700,y:60,s:1},
{x:1100,y:40,s:1.3},{x:1500,y:55,s:0.9},{x:1900,y:35,s:1.1},
{x:2300,y:50,s:0.7},{x:2700,y:45,s:1.2},{x:3100,y:30,s:0.8},
{x:3500,y:60,s:1},{x:3900,y:40,s:1.3},{x:4300,y:50,s:0.9},
{x:4700,y:35,s:1.1},{x:5100,y:55,s:0.7},{x:5500,y:45,s:1.2},
];
for (var c = 0; c < cloudPositions.length; c++) {
var cl = cloudPositions[c];
var px = cl.x - cameraX * 0.3;
// Wrap
while (px < -200) px += 6000;
while (px > W + 200) px -= 6000;
drawCloud(px, cl.y, cl.s);
}
}
function drawCloud(x, y, s) {
ctx.save();
ctx.translate(x, y);
ctx.scale(s, s);
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.arc(22, -5, 25, 0, Math.PI * 2);
ctx.arc(45, 0, 20, 0, Math.PI * 2);
ctx.arc(22, 5, 18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHills() {
ctx.fillStyle = "#4AAF4A";
var hillData = [
{x:0,w:300,h:80},{x:350,w:200,h:60},{x:600,w:350,h:100},
{x:1000,w:250,h:70},{x:1300,w:300,h:90},{x:1700,w:200,h:65},
{x:2000,w:350,h:85},{x:2400,w:250,h:75},{x:2700,w:300,h:95},
{x:3100,w:200,h:60},{x:3400,w:350,h:100},{x:3800,w:250,h:70},
{x:4100,w:300,h:80},{x:4500,w:200,h:55},{x:4800,w:350,h:90},
{x:5200,w:250,h:75},{x:5500,w:300,h:85},
];
for (var h = 0; h < hillData.length; h++) {
var hill = hillData[h];
var hx = hill.x - cameraX * 0.5;
while (hx < -hill.w) hx += 5800;
while (hx > W + hill.w) hx -= 5800;
var baseY = groundRow * TILE - cameraY_offset();
ctx.beginPath();
ctx.moveTo(hx - hill.w/2, baseY);
ctx.quadraticCurveTo(hx, baseY - hill.h, hx + hill.w/2, baseY);
ctx.fill();
}
}
function cameraY_offset() { return 0; }
function drawTiles() {
var startCol = Math.floor(cameraX / TILE) - 1;
var endCol = startCol + Math.ceil(W / TILE) + 2;
for (var ty = 0; ty < LEVEL_H; ty++) {
for (var tx = startCol; tx <= endCol && tx < LEVEL_W; tx++) {
if (tx < 0) continue;
var t = levelTiles[ty][tx];
if (t === 0) continue;
var x = tx * TILE;
var y = ty * TILE;
if (t === 1) {
// Ground block
ctx.fillStyle = "#8B4513";
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = "#6B3410";
ctx.fillRect(x + 1, y + 1, TILE - 2, TILE - 2);
ctx.fillStyle = "#228B22";
ctx.fillRect(x, y, TILE, 4);
// Brick lines
ctx.strokeStyle = "#6B3410";
ctx.lineWidth = 1;
ctx.strokeRect(x, y, TILE, TILE);
} else if (t === 2) {
// Brick
ctx.fillStyle = "#C84C09";
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = "#8B3000";
ctx.lineWidth = 1;
ctx.strokeRect(x, y, TILE, TILE);
ctx.beginPath();
ctx.moveTo(x, y + TILE/2);
ctx.lineTo(x + TILE, y + TILE/2);
ctx.moveTo(x + TILE/2, y);
ctx.lineTo(x + TILE/2, y + TILE/2);
ctx.moveTo(x + TILE/4, y + TILE/2);
ctx.lineTo(x + TILE/4, y + TILE);
ctx.moveTo(x + TILE*3/4, y + TILE/2);
ctx.lineTo(x + TILE*3/4, y + TILE);
ctx.stroke();
} else if (t === 3) {
// Question block
var pulse = Math.sin(frameCount * 0.08) * 0.15 + 0.85;
ctx.fillStyle = "#FFB800";
ctx.fillRect(x, y, TILE, TILE);
ctx.strokeStyle = "#CC8800";
ctx.lineWidth = 2;
ctx.strokeRect(x + 1, y + 1, TILE - 2, TILE - 2);
ctx.fillStyle = "#FFF";
ctx.font = "bold 18px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", x + TILE/2, y + TILE/2 + 1);
}
}
}
}
function drawCoins() {
for (var c = 0; c < coins.length; c++) {
var coin = coins[c];
if (coin.collected) continue;
var bob = Math.sin(frameCount * 0.06 + coin.bobOffset) * 3;
var cx = coin.x + coin.w / 2;
var cy = coin.y + coin.h / 2 + bob;
// Glow
ctx.fillStyle = "rgba(255,215,0,0.3)";
ctx.beginPath();
ctx.arc(cx, cy, 12, 0, Math.PI * 2);
ctx.fill();
// Coin body
var scaleX = Math.abs(Math.cos(frameCount * 0.05 + coin.bobOffset));
ctx.save();
ctx.translate(cx, cy);
ctx.scale(scaleX, 1);
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "#DAA520";
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = "#DAA520";
ctx.font = "bold 10px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 1);
ctx.restore();
}
}
function drawEnemies() {
for (var e = 0; e < enemies.length; e++) {
var en = enemies[e];
if (!en.alive && en.squishTimer <= 0) continue;
var ex = en.x;
var ey = en.y;
if (!en.alive) {
// Squished
ctx.fillStyle = "#8B4513";
ctx.fillRect(ex, ey + en.h - 6, en.w, 6);
continue;
}
// Body
ctx.fillStyle = "#8B4513";
ctx.beginPath();
ctx.ellipse(ex + en.w/2, ey + en.h * 0.4, en.w * 0.45, en.h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
// Feet
var footOffset = Math.sin(frameCount * 0.15) * 2;
ctx.fillStyle = "#333";
ctx.fillRect(ex + 2, ey + en.h - 8 + footOffset, 8, 8);
ctx.fillRect(ex + en.w - 10, ey + en.h - 8 - footOffset, 8, 8);
// Eyes
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(ex + en.w * 0.35, ey + en.h * 0.35, 4, 0, Math.PI * 2);
ctx.arc(ex + en.w * 0.65, ey + en.h * 0.35, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#000";
ctx.beginPath();
ctx.arc(ex + en.w * 0.35 + (en.vx > 0 ? 1.5 : -1.5), ey + en.h * 0.35, 2, 0, Math.PI * 2);
ctx.arc(ex + en.w * 0.65 + (en.vx > 0 ? 1.5 : -1.5), ey + en.h * 0.35, 2, 0, Math.PI * 2);
ctx.fill();
// Angry eyebrows
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(ex + en.w * 0.25, ey + en.h * 0.2);
ctx.lineTo(ex + en.w * 0.45, ey + en.h * 0.25);
ctx.moveTo(ex + en.w * 0.75, ey + en.h * 0.2);
ctx.lineTo(ex + en.w * 0.55, ey + en.h * 0.25);
ctx.stroke();
}
}
function drawFlag() {
var fx = flagX;
var fy = flagY;
// Pole
ctx.fillStyle = "#888";
ctx.fillRect(fx + 8, fy, 4, (groundRow - flagY) * TILE);
// Ball on top
ctx.fillStyle = "#FFD700";
ctx.beginPath();
ctx.arc(fx + 10, fy, 6, 0, Math.PI * 2);
ctx.fill();
// Flag
var wave = Math.sin(frameCount * 0.08) * 3;
ctx.fillStyle = "#FF3333";
ctx.beginPath();
ctx.moveTo(fx + 12, fy + 5);
ctx.lineTo(fx + 45 + wave, fy + 15);
ctx.lineTo(fx + 12, fy + 35);
ctx.closePath();
ctx.fill();
// Star on flag
ctx.fillStyle = "#FFD700";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.fillText("★", fx + 28 + wave * 0.5, fy + 24);
// "GOAL" text
ctx.fillStyle = "#FFF";
ctx.font = "bold 11px Arial";
ctx.textAlign = "center";
ctx.fillText("GOAL", fx + 10, fy - 8);
}
function drawPlayer() {
if (state === "dead") return;
var px = player.x;
var py = player.y;
var f = player.facing;
ctx.save();
ctx.translate(px + player.w / 2, py + player.h / 2);
ctx.scale(f, 1);
ctx.translate(-player.w / 2, -player.h / 2);
// Hat
ctx.fillStyle = "#E52521";
ctx.fillRect(2, 0, 20, 8);
ctx.fillRect(0, 2, 24, 6);
// Face
ctx.fillStyle = "#FBBE7C";
ctx.fillRect(2, 8, 20, 10);
// Eyes
ctx.fillStyle = "#000";
ctx.fillRect(14, 10, 3, 3);
// Mustache
ctx.fillStyle = "#4A2800";
ctx.fillRect(10, 14, 12, 2);
// Body (overalls)
ctx.fillStyle = "#0055D4";
ctx.fillRect(4, 18, 16, 8);
// Overall buttons
ctx.fillStyle = "#FFD700";
ctx.fillRect(8, 19, 2, 2);
ctx.fillRect(14, 19, 2, 2);
// Legs
var legAnim = player.onGround ? Math.sin(player.walkCycle * 0.8) * 3 : 2;
ctx.fillStyle = "#0055D4";
if (player.onGround && Math.abs(player.vx) > 0.5) {
ctx.fillRect(3, 26, 7, 4 + legAnim);
ctx.fillRect(14, 26, 7, 4 - legAnim);
} else {
ctx.fillRect(3, 26, 7, 4);
ctx.fillRect(14, 26, 7, 4);
}
// Shoes
ctx.fillStyle = "#8B4513";
ctx.fillRect(1, 29, 9, 3);
ctx.fillRect(14, 29, 9, 3);
// Arm
ctx.fillStyle = "#FBBE7C";
var armY = player.onGround ? 18 + Math.sin(player.walkCycle * 0.8) * 2 : 14;
ctx.fillRect(18, armY, 5, 8);
ctx.restore();
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
var alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score background
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fillRect(10, 8, 200, 36);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(10, 8, 200, 36);
// Coins
ctx.fillStyle = "#FFD700";
ctx.font = "bold 16px 'Segoe UI', Arial";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("🪙 " + coinsCollected + " / " + totalCoins, 20, 22);
// Score
ctx.fillStyle = "#FFF";
ctx.font = "bold 14px 'Segoe UI', Arial";
ctx.fillText("Score: " + score, 140, 22);
}
function drawWinScreen() {
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, W, H);
var bounce = Math.min(winTimer * 0.05, 1);
var scale = 0.5 + bounce * 0.5;
ctx.save();
ctx.translate(W/2, H/2 - 20);
ctx.scale(scale, scale);
ctx.fillStyle = "#FFD700";
ctx.font = "bold 48px 'Segoe UI', Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🏆 YOU WIN! 🏆", 0, -40);
ctx.fillStyle = "#FFF";
ctx.font = "24px 'Segoe UI', Arial";
ctx.fillText("Score: " + score, 0, 20);
ctx.fillText("Coins: " + coinsCollected + " / " + totalCoins, 0, 55);
if (winTimer > 60) {
ctx.fillStyle = "#AAA";
ctx.font = "16px 'Segoe UI', Arial";
ctx.fillText("Press R to play again", 0, 100);
}
ctx.restore();
}
function drawDeathScreen() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#FF4444";
ctx.font = "bold 42px 'Segoe UI', Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("GAME OVER", W/2, H/2 - 20);
ctx.fillStyle = "#FFF";
ctx.font = "18px 'Segoe UI', Arial";
ctx.fillText("Press R to try again", W/2, H/2 + 30);
}
// ─── Game Loop ───────────────────────────────────────────
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Start
gameLoop();
})();
</script>
</body>
</html>