Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created May 25, 2025 14:49
Show Gist options
  • Save shricodev/ebf7cd4eda69c3cbd8deaf7da5dc7588 to your computer and use it in GitHub Desktop.
Save shricodev/ebf7cd4eda69c3cbd8deaf7da5dc7588 to your computer and use it in GitHub Desktop.
Blog - Mario (OpenAI o3)
<!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&nbsp;OVER<br>Press&nbsp;R&nbsp;to&nbsp;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