Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created May 25, 2025 14:48
Show Gist options
  • Save shricodev/e5e5951aa3f748caee901d90e4fe8feb to your computer and use it in GitHub Desktop.
Save shricodev/e5e5951aa3f748caee901d90e4fe8feb to your computer and use it in GitHub Desktop.
Blog - Mario (Gemini 2.5.Pro)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Mario-Style Platformer</title>
<style>
body {
margin: 0;
background-color: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: "Courier New", Courier, monospace;
}
canvas {
border: 2px solid #fff;
background-color: #70c5ce; /* Sky blue */
display: block;
}
#controlsInfo {
color: #fff;
text-align: center;
margin-top: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<div>
<canvas id="gameCanvas"></canvas>
<div id="controlsInfo">
Controls: Arrow Keys or A/D to Move, Space or W to Jump. R to Restart.
</div>
</div>
<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// Game constants
const CANVAS_WIDTH = 640;
const CANVAS_HEIGHT = 360;
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
const GRAVITY = 0.6;
const PLAYER_MAX_FALL_SPEED = 10;
const PLAYER_SPEED = 3.5;
const PLAYER_JUMP_STRENGTH = 13;
const ENEMY_SPEED = 0.8;
const PLAYER_WIDTH = 20;
const PLAYER_HEIGHT = 30;
const ENEMY_WIDTH = 22;
const ENEMY_HEIGHT = 22;
const TILE_SIZE = 20; // For visual consistency
// Game state
let gameState = "PLAYING"; // PLAYING, GAME_OVER, LEVEL_COMPLETE
let score = 0;
let lives = 3;
let coins = 0; // Simple coin counter
let gameTime = 400; // Timer like in Mario
let gameTimeInterval;
// Player object
let player = {
x: 50,
y: CANVAS_HEIGHT - 40 - PLAYER_HEIGHT, // Start on ground
width: PLAYER_WIDTH,
height: PLAYER_HEIGHT,
dx: 0,
dy: 0,
onGround: false,
isJumping: false,
facingRight: true,
isDead: false,
deathTimer: 0, // For death animation/delay
walkFrameTimer: 0,
walkFrame: 0,
};
// Level data: Platforms
// type: 'ground', 'brick', 'sky-brick'
let platforms = [
// Ground
{
x: 0,
y: CANVAS_HEIGHT - TILE_SIZE * 2,
width: CANVAS_WIDTH + 200,
height: TILE_SIZE * 2,
type: "ground",
}, // Extend ground for more space
// Floating platforms
{
x: 150,
y: CANVAS_HEIGHT - TILE_SIZE * 6,
width: TILE_SIZE * 5,
height: TILE_SIZE,
type: "brick",
},
{
x: 280,
y: CANVAS_HEIGHT - TILE_SIZE * 9,
width: TILE_SIZE * 4,
height: TILE_SIZE,
type: "brick",
},
{
x: 400,
y: CANVAS_HEIGHT - TILE_SIZE * 6,
width: TILE_SIZE * 5,
height: TILE_SIZE,
type: "brick",
},
// Some higher ones
{
x: 550,
y: CANVAS_HEIGHT - TILE_SIZE * 10,
width: TILE_SIZE * 3,
height: TILE_SIZE,
type: "sky-brick",
},
{
x: 50,
y: CANVAS_HEIGHT - TILE_SIZE * 9,
width: TILE_SIZE * 2,
height: TILE_SIZE,
type: "sky-brick",
}, // Near start
// A wall
{
x: CANVAS_WIDTH - TILE_SIZE * 3,
y: CANVAS_HEIGHT - TILE_SIZE * 7,
width: TILE_SIZE,
height: TILE_SIZE * 5,
type: "brick",
},
];
// Goal object (flagpole)
const goal = {
x: CANVAS_WIDTH + 150, // Place it off initial screen if camera was implemented, for now place it at end of extended ground
y: CANVAS_HEIGHT - TILE_SIZE * 2 - TILE_SIZE * 3, // On the ground platform
width: TILE_SIZE / 2,
height: TILE_SIZE * 3, // Actual collision height
poleHeight: TILE_SIZE * 3, // Visual pole height
type: "flagpole",
};
platforms.push(goal); // Add goal to platforms for drawing, collision handled separately
// Enemy data
const initialEnemyData = [
{
x: 250,
y: CANVAS_HEIGHT - TILE_SIZE * 2 - ENEMY_HEIGHT,
type: "koopa",
patrolMinX: 200,
patrolMaxX: 350,
},
{
x: 450,
y: CANVAS_HEIGHT - TILE_SIZE * 2 - ENEMY_HEIGHT,
type: "koopa",
patrolMinX: 400,
patrolMaxX: 520,
},
{
x: 160,
y: CANVAS_HEIGHT - TILE_SIZE * 6 - ENEMY_HEIGHT,
type: "koopa",
patrolMinX: 150,
patrolMaxX: 150 + TILE_SIZE * 5 - ENEMY_WIDTH,
}, // On a platform
];
let enemies = [];
// Input handling
const keys = {
ArrowLeft: false,
ArrowRight: false,
Space: false,
KeyA: false,
KeyD: false,
KeyW: false,
KeyR: false,
};
window.addEventListener("keydown", (e) => {
if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
if (
["Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(
e.code,
)
) {
e.preventDefault();
}
});
window.addEventListener("keyup", (e) => {
if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
});
function handleInput() {
if (player.isDead) return;
// Horizontal movement
if (keys.ArrowLeft || keys.KeyA) {
player.dx = -PLAYER_SPEED;
player.facingRight = false;
} else if (keys.ArrowRight || keys.KeyD) {
player.dx = PLAYER_SPEED;
player.facingRight = true;
} else {
player.dx = 0; // No explicit momentum/friction, direct stop
}
// Jumping
if ((keys.Space || keys.KeyW) && player.onGround && !player.isJumping) {
player.dy = -PLAYER_JUMP_STRENGTH;
player.onGround = false;
player.isJumping = true;
}
// Variable jump height (cut jump short if key released)
if (
!(keys.Space || keys.KeyW) &&
player.isJumping &&
player.dy < -PLAYER_JUMP_STRENGTH / 2.5
) {
player.dy = -PLAYER_JUMP_STRENGTH / 2.5;
}
}
function updatePlayer(deltaTime) {
if (player.isDead) {
player.deathTimer -= deltaTime;
player.y += player.dy; // Death hop
player.dy += GRAVITY; // Fall through stage
if (player.deathTimer <= 0) {
if (lives > 0) {
respawnPlayer();
} else {
gameState = "GAME_OVER";
}
}
return;
}
// Apply gravity
if (!player.onGround) {
player.dy += GRAVITY;
player.dy = Math.min(player.dy, PLAYER_MAX_FALL_SPEED);
}
let oldX = player.x;
let oldY = player.y;
let newX = player.x + player.dx;
let newY = player.y + player.dy;
// Horizontal collision
player.x = newX;
platforms.forEach((platform) => {
if (platform.type === "flagpole") return; // Flagpole doesn't block horizontal movement
if (checkCollision(player, platform)) {
if (player.dx > 0) {
player.x = platform.x - player.width;
} else if (player.dx < 0) {
player.x = platform.x + platform.width;
}
}
});
// Vertical collision
player.y = newY;
player.onGround = false;
platforms.forEach((platform) => {
if (platform.type === "flagpole") return;
if (checkCollision(player, platform)) {
if (player.dy > 0 && oldY + player.height <= platform.y) {
// Landing on top
player.y = platform.y - player.height;
player.dy = 0;
player.onGround = true;
player.isJumping = false;
} else if (player.dy < 0 && oldY >= platform.y + platform.height) {
// Hitting bottom (ceiling)
player.y = platform.y + platform.height;
player.dy = 0;
}
}
});
// Fall off map
if (player.y > CANVAS_HEIGHT + player.height) {
playerLosesLife();
}
// Animation timer
if (player.dx !== 0 && player.onGround) {
player.walkFrameTimer += deltaTime;
if (player.walkFrameTimer > 80) {
// Adjust for walk speed animation
player.walkFrame = (player.walkFrame + 1) % 2;
player.walkFrameTimer = 0;
}
} else if (player.dx === 0) {
player.walkFrame = 0; // Standing frame
}
// Check goal collision
if (checkCollision(player, goal)) {
gameState = "LEVEL_COMPLETE";
// Could add animation of sliding down pole here
player.dx = 0;
}
}
function updateEnemies(deltaTime) {
enemies.forEach((enemy) => {
if (!enemy.isAlive) return;
if (enemy.isStomped) {
enemy.stompTimer -= deltaTime;
if (enemy.stompTimer <= 0) {
enemy.isAlive = false; // Remove after stomp animation
// No points for koopa shells here, just defeat
}
return; // No movement if stomped
}
enemy.x += enemy.speed * enemy.direction;
// Patrol logic
if (
enemy.direction === 1 &&
enemy.x + ENEMY_WIDTH > enemy.patrolMaxX
) {
enemy.x = enemy.patrolMaxX - ENEMY_WIDTH;
enemy.direction = -1;
} else if (enemy.direction === -1 && enemy.x < enemy.patrolMinX) {
enemy.x = enemy.patrolMinX;
enemy.direction = 1;
}
// Basic "turn at edge" if on a platform (simplified: needs platform reference or raycast)
// For now, patrolMinX/MaxX must be set correctly for the platform it's on.
// Example: if an enemy is on platforms[i], patrolMinX = platforms[i].x, patrolMaxX = platforms[i].x + platforms[i].width - ENEMY_WIDTH
});
}
function checkCollisions() {
if (player.isDead) return;
// Player-Enemy collisions
enemies.forEach((enemy) => {
if (!enemy.isAlive || enemy.isStomped) return;
if (checkCollision(player, enemy)) {
// Player jumps on enemy
if (
player.dy > 0 &&
player.y + player.height - player.dy <= enemy.y
) {
// Player was above and is falling
enemy.isStomped = true;
enemy.stompTimer = 500; // 0.5 seconds
enemy.originalHeight = ENEMY_HEIGHT; // Store for stomp visual
enemy.height = ENEMY_HEIGHT / 2;
enemy.y += ENEMY_HEIGHT / 2;
player.dy = -PLAYER_JUMP_STRENGTH / 2; // Bounce
player.isJumping = true; // Allow further jump control if key held
player.onGround = false;
score += 100;
} else {
// Player hit by enemy from side/below
playerLosesLife();
}
}
});
}
function playerLosesLife() {
if (player.isDead) return; // Already dying
lives--;
player.isDead = true;
player.deathTimer = 1500; // 1.5 seconds for death animation
player.dx = 0;
player.dy = -PLAYER_JUMP_STRENGTH / 2; // Small hop animation
if (lives <= 0) {
// Game over will be set after deathTimer
}
}
function respawnPlayer() {
player.x = 50;
player.y = CANVAS_HEIGHT - TILE_SIZE * 2 - PLAYER_HEIGHT;
player.dx = 0;
player.dy = 0;
player.onGround = true; // Start on ground
player.isJumping = false;
player.isDead = false;
player.deathTimer = 0;
player.facingRight = true;
gameTime = 400; // Reset timer on respawn
}
function checkCollision(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
function drawPlayer() {
if (player.isDead && player.deathTimer > 0) {
// Simple death visual: flashing
if (Math.floor(player.deathTimer / 100) % 2 === 0) {
ctx.fillStyle = "rgba(200, 0, 0, 0.5)";
ctx.fillRect(player.x, player.y, player.width, player.height);
}
return;
}
if (player.isDead && player.deathTimer <= 0) return; // Don't draw if fully dead and processed
// Body
ctx.fillStyle = player.facingRight ? "#E04040" : "#D03030"; // Red for Mario
ctx.fillRect(player.x, player.y, player.width, player.height);
// Simple eyes (dots) based on direction
ctx.fillStyle = "white";
const eyeXOffset = player.facingRight
? player.width * 0.6
: player.width * 0.2;
ctx.fillRect(
player.x + eyeXOffset,
player.y + player.height * 0.2,
4,
4,
);
ctx.fillStyle = "black";
ctx.fillRect(
player.x + eyeXOffset + 1,
player.y + player.height * 0.2 + 1,
2,
2,
);
// "Legs" part for walking animation
if (player.onGround && player.dx !== 0) {
// Simple leg movement: shorten body slightly, draw "stepping" legs
ctx.fillStyle = "#8B4513"; // Brown for boots
const legWidth = player.width / 2.5;
if (player.walkFrame === 0) {
ctx.fillRect(
player.x,
player.y + player.height * 0.7,
legWidth,
player.height * 0.3,
);
ctx.fillRect(
player.x + player.width - legWidth,
player.y + player.height * 0.75,
legWidth,
player.height * 0.25,
);
} else {
ctx.fillRect(
player.x,
player.y + player.height * 0.75,
legWidth,
player.height * 0.25,
);
ctx.fillRect(
player.x + player.width - legWidth,
player.y + player.height * 0.7,
legWidth,
player.height * 0.3,
);
}
} else {
// Standing or jumping
ctx.fillStyle = "#A0522D"; // Brown for boots
ctx.fillRect(
player.x,
player.y + player.height * 0.7,
player.width,
player.height * 0.3,
);
}
}
function drawEnemies() {
enemies.forEach((enemy) => {
if (!enemy.isAlive) return;
ctx.fillStyle = "#2E8B57"; // Green for Koopa
if (enemy.isStomped) {
ctx.fillRect(enemy.x, enemy.y, ENEMY_WIDTH, ENEMY_HEIGHT / 2); // Squashed
} else {
ctx.fillRect(enemy.x, enemy.y, ENEMY_WIDTH, ENEMY_HEIGHT);
// Simple eyes for enemy
ctx.fillStyle = "white";
const eyeX =
enemy.direction === 1
? enemy.x + ENEMY_WIDTH * 0.6
: enemy.x + ENEMY_WIDTH * 0.1;
ctx.fillRect(eyeX, enemy.y + ENEMY_HEIGHT * 0.2, 5, 5);
ctx.fillStyle = "black";
ctx.fillRect(eyeX + 1, enemy.y + ENEMY_HEIGHT * 0.2 + 1, 3, 3);
}
});
}
function drawPlatforms() {
platforms.forEach((platform) => {
if (platform.type === "ground") {
ctx.fillStyle = "#D2B48C"; // Tan
} else if (platform.type === "brick") {
ctx.fillStyle = "#B22222"; // Firebrick Red
} else if (platform.type === "sky-brick") {
ctx.fillStyle = "#87CEEB"; // Sky blue brick (lighter)
} else if (platform.type === "flagpole") {
// Draw flagpole
ctx.fillStyle = "silver"; // Pole
ctx.fillRect(
platform.x,
platform.y,
platform.width,
platform.poleHeight,
);
// Flag
ctx.fillStyle = "green";
ctx.beginPath();
ctx.moveTo(platform.x + platform.width, platform.y + 5);
ctx.lineTo(
platform.x + platform.width + TILE_SIZE,
platform.y + TILE_SIZE / 2 + 5,
);
ctx.lineTo(platform.x + platform.width, platform.y + TILE_SIZE + 5);
ctx.closePath();
ctx.fill();
return; // Skip generic rect draw
} else {
ctx.fillStyle = "gray";
}
ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
// Add brick pattern for 'brick' type
if (platform.type === "brick" || platform.type === "sky-brick") {
ctx.strokeStyle = platform.type === "brick" ? "#800000" : "#4682B4"; // Darker lines
ctx.lineWidth = 1;
for (let i = 0; i < platform.width; i += TILE_SIZE / 2) {
ctx.beginPath();
ctx.moveTo(platform.x + i, platform.y);
ctx.lineTo(platform.x + i, platform.y + platform.height);
ctx.stroke();
}
for (let j = 0; j < platform.height; j += TILE_SIZE / 2) {
ctx.beginPath();
ctx.moveTo(platform.x, platform.y + j);
ctx.lineTo(platform.x + platform.width, platform.y + j);
ctx.stroke();
}
}
});
}
function drawHUD() {
ctx.fillStyle = "white";
ctx.font = '18px "Courier New", Courier, monospace';
// Score
ctx.textAlign = "left";
ctx.fillText(`SCORE: ${score}`, 10, 25);
// Coins (Not fully implemented, just counter)
ctx.fillText(`COINS: ${coins}`, 10, 50);
// Lives
ctx.textAlign = "center";
ctx.fillText(`LIVES: ${lives}`, CANVAS_WIDTH / 2, 25);
// Time
ctx.textAlign = "right";
ctx.fillText(`TIME: ${gameTime}`, CANVAS_WIDTH - 10, 25);
}
function drawGameOverScreen() {
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.fillStyle = "white";
ctx.font = '40px "Courier New", Courier, monospace';
ctx.textAlign = "center";
ctx.fillText("GAME OVER", CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 20);
ctx.font = '20px "Courier New", Courier, monospace';
ctx.fillText(
`Final Score: ${score}`,
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 + 20,
);
ctx.fillText(
"Press R to Restart",
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 + 60,
);
}
function drawLevelCompleteScreen() {
ctx.fillStyle = "rgba(0, 100, 0, 0.7)"; // Greenish tint
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.fillStyle = "white";
ctx.font = '40px "Courier New", Courier, monospace';
ctx.textAlign = "center";
ctx.fillText(
"LEVEL COMPLETE!",
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 - 20,
);
ctx.font = '20px "Courier New", Courier, monospace';
ctx.fillText(
`Final Score: ${score}`,
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 + 20,
);
ctx.fillText(
`Time Left: ${gameTime}`,
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 + 50,
);
ctx.fillText(
"Press R to Restart",
CANVAS_WIDTH / 2,
CANVAS_HEIGHT / 2 + 90,
);
}
function startGameTimer() {
if (gameTimeInterval) clearInterval(gameTimeInterval);
gameTimeInterval = setInterval(() => {
if (gameState === "PLAYING" && !player.isDead) {
gameTime--;
if (gameTime <= 0) {
playerLosesLife(); // Time up
gameTime = 0; // Prevent negative time display
}
}
}, 1000); // Approx 1 sec, Mario time is faster
}
function initGame() {
player.x = 50;
player.y = CANVAS_HEIGHT - TILE_SIZE * 2 - PLAYER_HEIGHT;
player.dx = 0;
player.dy = 0;
player.onGround = true;
player.isJumping = false;
player.isDead = false;
player.deathTimer = 0;
player.facingRight = true;
score = 0;
lives = 3;
coins = 0;
gameTime = 400;
enemies = initialEnemyData.map((data) => ({
...data,
width: ENEMY_WIDTH,
height: ENEMY_HEIGHT,
speed: ENEMY_SPEED,
direction: Math.random() > 0.5 ? 1 : -1,
isAlive: true,
isStomped: false,
stompTimer: 0,
}));
gameState = "PLAYING";
startGameTimer();
}
let lastTime = 0;
function gameLoop(timestamp) {
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// Game state logic
if (gameState === "PLAYING") {
handleInput();
updatePlayer(deltaTime);
updateEnemies(deltaTime);
checkCollisions();
} else if (
gameState === "GAME_OVER" ||
gameState === "LEVEL_COMPLETE"
) {
if (keys.KeyR) {
initGame();
}
} else if (gameState === "START_MENU") {
// If you add a start menu
// Handle start menu logic
}
// Drawing
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // Clear canvas
drawPlatforms();
drawEnemies();
drawPlayer();
drawHUD();
if (gameState === "GAME_OVER") {
drawGameOverScreen();
} else if (gameState === "LEVEL_COMPLETE") {
drawLevelCompleteScreen();
}
requestAnimationFrame(gameLoop);
}
// Start game
initGame();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment