-
-
Save shricodev/ebf7cd4eda69c3cbd8deaf7da5dc7588 to your computer and use it in GitHub Desktop.
Blog - Mario (OpenAI o3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>Mini-Mario – HTML 5 Canvas Demo</title> | |
<style> | |
html, | |
body { | |
margin: 0; | |
height: 100%; | |
background: #000; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
canvas { | |
background: #5c94fc; /* sky */ | |
image-rendering: pixelated; /* chunky NES pixels */ | |
} | |
#hud { | |
position: fixed; | |
top: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
font: 20px/20px monospace; | |
color: #fff; | |
text-shadow: 1px 1px 2px #000; | |
user-select: none; | |
} | |
#msg { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
font: 32px/40px monospace; | |
color: #fff; | |
text-align: center; | |
text-shadow: 2px 2px 4px #000; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="game"></canvas> | |
<div id="hud"></div> | |
<div id="msg"></div> | |
<script> | |
/************************************************************ | |
* Simple Super-Mario-style platformer in a single file | |
* ---------------------------------------------------------- | |
* Arrow ← → / A D : move | |
* Space / W : jump | |
* R : restart (from Game-Over) | |
************************************************************/ | |
/* ==== CONSTANTS ========================================= */ | |
const TILE = 16; // one “pixel-tile” in the internal world | |
const VIEW_W = 24 * TILE; // internal viewport: 384 × 240 (NES 4:3) | |
const VIEW_H = 15 * TILE; | |
const GRAVITY = 0.4; | |
const MAX_RUN = 2.6; | |
const JUMP_VEL = -7.5; | |
const ENEMY_V = 1.1; | |
const COLORS = { | |
bg: "#5c94fc", | |
ground: "#8b4513", | |
brick: "#c87137", | |
bar: "#d8a657", | |
player: "#ff0000", | |
enemy: "#2ecc71", | |
}; | |
/* ==== VERY SMALL LEVEL ================================== */ | |
const LEVEL = [ | |
"................................................................", | |
"................................................................", | |
"................................................................", | |
"................................................................", | |
".................---...............................K............", | |
"..................................................###..........", | |
".....###..............................###..................---..", | |
".............M..................................K...............", | |
"###############################################################", | |
]; | |
/* ==== SIMPLE VECTOR ===================================== */ | |
class Vec { | |
constructor(x = 0, y = 0) { | |
this.x = x; | |
this.y = y; | |
} | |
} | |
/* ==== BASE ENTITY (rect) ================================ */ | |
class Entity { | |
constructor(x, y, w, h) { | |
this.pos = new Vec(x, y); | |
this.vel = new Vec(); | |
this.size = new Vec(w, h); | |
this.onGround = false; | |
this.dead = false; | |
} | |
get l() { | |
return this.pos.x; | |
} | |
get r() { | |
return this.pos.x + this.size.x; | |
} | |
get t() { | |
return this.pos.y; | |
} | |
get b() { | |
return this.pos.y + this.size.y; | |
} | |
} | |
/* ==== INPUT ============================================= */ | |
const keys = {}; | |
addEventListener("keydown", (e) => { | |
keys[e.key.toLowerCase()] = true; | |
if (["ArrowLeft", "ArrowRight", " ", "ArrowUp"].includes(e.key)) | |
e.preventDefault(); | |
}); | |
addEventListener("keyup", (e) => (keys[e.key.toLowerCase()] = false)); | |
/* ==== TILE HELPERS ====================================== */ | |
function tileAt(tx, ty) { | |
if (ty < 0 || ty >= LEVEL.length) return "#"; // top / bottom: solid | |
if (tx < 0 || tx >= LEVEL[0].length) return "#"; // sides: solid | |
return LEVEL[ty][tx]; | |
} | |
function solid(c) { | |
return c === "#" || c === "-" || c === "?"; | |
} | |
/* ==== PLAYER ============================================ */ | |
class Player extends Entity { | |
constructor(x, y) { | |
super(x, y, 14, 16); | |
this.lives = 3; | |
this.score = 0; | |
this.inv = 0; | |
} | |
update() { | |
/* horizontal input */ | |
const accel = | |
(keys["arrowleft"] || keys["a"] ? -0.4 : 0) + | |
(keys["arrowright"] || keys["d"] ? 0.4 : 0); | |
this.vel.x += accel; | |
if (!accel) this.vel.x *= 0.8; // friction | |
this.vel.x = Math.max(-MAX_RUN, Math.min(MAX_RUN, this.vel.x)); | |
/* jump */ | |
if ((keys["w"] || keys[" "]) && this.onGround) { | |
this.vel.y = JUMP_VEL; | |
this.onGround = false; | |
} | |
/* gravity */ | |
this.vel.y += GRAVITY; | |
/* move & collide */ | |
this.move(this.vel.x, 0); | |
this.move(0, this.vel.y); | |
if (this.inv > 0) this.inv--; | |
} | |
move(dx, dy) { | |
/* horizontal sweep */ | |
if (dx) { | |
let nx = this.pos.x + dx, | |
dir = dx > 0 ? 1 : -1, | |
edge = dir > 0 ? nx + this.size.x : nx, | |
tx = Math.floor(edge / TILE), | |
y0 = Math.floor(this.t / TILE), | |
y1 = Math.floor((this.b - 1) / TILE); | |
for (let ty = y0; ty <= y1; ty++) { | |
if (solid(tileAt(tx, ty))) { | |
nx = dir > 0 ? tx * TILE - this.size.x : (tx + 1) * TILE; | |
this.vel.x = 0; | |
break; | |
} | |
} | |
this.pos.x = nx; | |
} | |
/* vertical sweep */ | |
if (dy) { | |
let ny = this.pos.y + dy, | |
dir = dy > 0 ? 1 : -1, | |
edge = dir > 0 ? ny + this.size.y : ny, | |
ty = Math.floor(edge / TILE), | |
x0 = Math.floor(this.l / TILE), | |
x1 = Math.floor((this.r - 1) / TILE); | |
for (let tx = x0; tx <= x1; tx++) { | |
if (solid(tileAt(tx, ty))) { | |
ny = dir > 0 ? ty * TILE - this.size.y : (ty + 1) * TILE; | |
if (dir > 0) this.onGround = true; // landed | |
this.vel.y = 0; | |
break; | |
} | |
} | |
this.pos.y = ny; | |
if (dy > 0 && this.vel.y !== 0) | |
this.onGround = false; /* walking off edge */ | |
} | |
} | |
} | |
/* ==== SIMPLE KOOPA ====================================== */ | |
class Koopa extends Entity { | |
constructor(x, y) { | |
super(x, y, 14, 16); | |
this.vel.x = ENEMY_V; | |
} | |
update() { | |
/* gravity */ | |
this.vel.y += GRAVITY; | |
/* horizontal */ | |
let nx = this.pos.x + this.vel.x, | |
dir = this.vel.x > 0 ? 1 : -1, | |
edge = dir > 0 ? nx + this.size.x : nx, | |
tx = Math.floor(edge / TILE), | |
y0 = Math.floor(this.t / TILE), | |
y1 = Math.floor((this.b - 1) / TILE); | |
let hit = false; | |
for (let ty = y0; ty <= y1; ty++) { | |
if (solid(tileAt(tx, ty))) { | |
hit = true; | |
break; | |
} | |
} | |
if (hit) { | |
this.vel.x *= -1; | |
nx = this.pos.x + this.vel.x; | |
} | |
this.pos.x = nx; | |
/* vertical */ | |
let ny = this.pos.y + this.vel.y, | |
vdir = this.vel.y > 0 ? 1 : -1, | |
yEdge = vdir > 0 ? ny + this.size.y : ny, | |
ty = Math.floor(yEdge / TILE), | |
x0 = Math.floor(this.l / TILE), | |
x1 = Math.floor((this.r - 1) / TILE); | |
for (let tx = x0; tx <= x1; tx++) { | |
if (solid(tileAt(tx, ty))) { | |
ny = vdir > 0 ? ty * TILE - this.size.y : (ty + 1) * TILE; | |
this.vel.y = 0; | |
break; | |
} | |
} | |
this.pos.y = ny; | |
} | |
} | |
/* ==== CANVAS & RESIZE =================================== */ | |
const cvs = document.getElementById("game"), | |
ctx = cvs.getContext("2d"); | |
ctx.imageSmoothingEnabled = false; | |
let SCALE = 1; | |
function resize() { | |
SCALE = Math.floor(Math.min(innerWidth / VIEW_W, innerHeight / VIEW_H)); | |
cvs.width = VIEW_W * SCALE; | |
cvs.height = VIEW_H * SCALE; | |
} | |
addEventListener("resize", resize); | |
resize(); | |
/* ==== WORLD / STATE ===================================== */ | |
let player, | |
enemies, | |
state = "play"; | |
const hud = document.getElementById("hud"), | |
msg = document.getElementById("msg"); | |
function init() { | |
enemies = []; | |
for (let y = 0; y < LEVEL.length; y++) { | |
for (let x = 0; x < LEVEL[0].length; x++) { | |
const c = LEVEL[y][x]; | |
if (c === "K") enemies.push(new Koopa(x * TILE, y * TILE - 0.1)); | |
if (c === "M") player = new Player(x * TILE, y * TILE - 16); | |
} | |
} | |
state = "play"; | |
msg.textContent = ""; | |
} | |
init(); | |
/* ==== MAIN LOOP ========================================= */ | |
function gameLoop() { | |
requestAnimationFrame(gameLoop); | |
if (state === "play") update(); | |
draw(); | |
} | |
requestAnimationFrame(gameLoop); | |
/* ---- UPDATE ------------------------------------------- */ | |
function update() { | |
player.update(); | |
/* enemies */ | |
enemies.forEach((e) => { | |
e.update(); | |
if (e.dead) return; | |
if (hit(player, e)) { | |
/* stomp from above */ | |
if (player.vel.y > 0 && player.b - e.t < 10) { | |
e.dead = true; | |
player.vel.y = JUMP_VEL * 0.7; | |
player.score += 100; | |
} else if (player.inv === 0) { | |
player.lives--; | |
player.inv = 120; // brief invulnerability | |
if (player.lives <= 0) lose(); | |
} | |
} | |
}); | |
enemies = enemies.filter((e) => !e.dead); | |
/* fall death */ | |
if (player.t > VIEW_H + 100) { | |
player.lives--; | |
if (player.lives > 0) init(); | |
else lose(); | |
} | |
/* manual restart */ | |
if (keys["r"]) init(); | |
} | |
/* ---- GAME-OVER ---------------------------------------- */ | |
function lose() { | |
state = "over"; | |
msg.innerHTML = "GAME OVER<br>Press R to Restart"; | |
} | |
/* ---- INTERSECTION ------------------------------------- */ | |
const hit = (a, b) => | |
!(a.r <= b.l || a.l >= b.r || a.b <= b.t || a.t >= b.b); | |
/* ---- DRAW --------------------------------------------- */ | |
function draw() { | |
ctx.fillStyle = COLORS.bg; | |
ctx.fillRect(0, 0, cvs.width, cvs.height); | |
ctx.save(); | |
ctx.scale(SCALE, SCALE); | |
/* camera follows Mario */ | |
const camX = Math.max( | |
0, | |
Math.min(player.pos.x - VIEW_W / 2, LEVEL[0].length * TILE - VIEW_W), | |
); | |
ctx.translate(-camX, 0); | |
/* tiles */ | |
for (let y = 0; y < LEVEL.length; y++) { | |
for (let x = 0; x < LEVEL[0].length; x++) { | |
const c = LEVEL[y][x]; | |
if (c === "#") { | |
// ground | |
ctx.fillStyle = COLORS.ground; | |
ctx.fillRect(x * TILE, y * TILE, TILE, TILE); | |
} else if (c === "-" || c === "?") { | |
// floating bricks / bars | |
ctx.fillStyle = c === "-" ? COLORS.bar : COLORS.brick; | |
ctx.fillRect(x * TILE + 2, y * TILE + 2, TILE - 4, TILE - 4); | |
} | |
} | |
} | |
/* enemies */ | |
enemies.forEach((e) => { | |
ctx.fillStyle = COLORS.enemy; | |
ctx.fillRect(e.pos.x, e.pos.y, e.size.x, e.size.y); | |
ctx.fillStyle = "#006400"; // shell stripe | |
ctx.fillRect(e.pos.x + 2, e.pos.y + 4, e.size.x - 4, 4); | |
}); | |
/* player (blink while invulnerable) */ | |
if (!(player.inv > 0 && Math.floor(player.inv / 6) % 2)) { | |
ctx.fillStyle = COLORS.player; | |
ctx.fillRect( | |
player.pos.x, | |
player.pos.y, | |
player.size.x, | |
player.size.y, | |
); | |
} | |
ctx.restore(); | |
/* HUD */ | |
hud.textContent = `Score: ${player.score} Lives: ${player.lives}`; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment