-
-
Save shricodev/0604cb50cee0599e08e337522378715f to your computer and use it in GitHub Desktop.
Blog - Mario (Claude Opus 4)
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" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Super Mario Bros Clone</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
background: #5c94fc; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
font-family: Arial, sans-serif; | |
} | |
#gameContainer { | |
position: relative; | |
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
} | |
canvas { | |
display: block; | |
background: #5c94fc; | |
image-rendering: pixelated; | |
} | |
#gameUI { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: white; | |
font-size: 20px; | |
font-weight: bold; | |
text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.5); | |
} | |
#gameOver { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
color: white; | |
font-size: 36px; | |
font-weight: bold; | |
text-align: center; | |
text-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); | |
display: none; | |
} | |
#gameOver button { | |
margin-top: 20px; | |
padding: 10px 20px; | |
font-size: 20px; | |
background: #e45826; | |
color: white; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
box-shadow: 0 4px 0 #b73e1d; | |
} | |
#gameOver button:active { | |
transform: translateY(2px); | |
box-shadow: 0 2px 0 #b73e1d; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<canvas id="gameCanvas"></canvas> | |
<div id="gameUI"> | |
<div>SCORE: <span id="score">0</span></div> | |
<div>COINS: <span id="coins">0</span></div> | |
<div>LIVES: <span id="lives">3</span></div> | |
<div>TIME: <span id="timer">300</span></div> | |
</div> | |
<div id="gameOver"> | |
<div id="gameOverText">GAME OVER!</div> | |
<button onclick="restartGame()">RESTART</button> | |
</div> | |
</div> | |
<script> | |
// Canvas setup | |
const canvas = document.getElementById("gameCanvas"); | |
const ctx = canvas.getContext("2d"); | |
canvas.width = 800; | |
canvas.height = 400; | |
// Game state | |
let gameState = "playing"; // playing, gameOver, levelComplete | |
let score = 0; | |
let coins = 0; | |
let lives = 3; | |
let timer = 300; | |
let lastTimerUpdate = Date.now(); | |
// Camera | |
let camera = { x: 0, y: 0 }; | |
// Input handling | |
const keys = {}; | |
window.addEventListener("keydown", (e) => { | |
keys[e.key.toLowerCase()] = true; | |
// Prevent space from scrolling | |
if (e.key === " ") e.preventDefault(); | |
}); | |
window.addEventListener("keyup", (e) => { | |
keys[e.key.toLowerCase()] = false; | |
}); | |
// Player object | |
const player = { | |
x: 100, | |
y: 200, | |
width: 24, | |
height: 32, | |
velocityX: 0, | |
velocityY: 0, | |
speed: 4, | |
jumpPower: 12, | |
isGrounded: false, | |
isDead: false, | |
animFrame: 0, | |
animTimer: 0, | |
facing: 1, // 1 for right, -1 for left | |
invulnerable: 0, | |
update: function () { | |
if (this.isDead) return; | |
// Handle invulnerability frames | |
if (this.invulnerable > 0) { | |
this.invulnerable--; | |
} | |
// Input handling | |
if (keys["arrowleft"] || keys["a"]) { | |
this.velocityX = -this.speed; | |
this.facing = -1; | |
} else if (keys["arrowright"] || keys["d"]) { | |
this.velocityX = this.speed; | |
this.facing = 1; | |
} else { | |
this.velocityX *= 0.8; // Friction | |
} | |
// Jumping | |
if ((keys[" "] || keys["w"]) && this.isGrounded) { | |
this.velocityY = -this.jumpPower; | |
this.isGrounded = false; | |
} | |
// Apply gravity | |
this.velocityY += 0.6; | |
if (this.velocityY > 15) this.velocityY = 15; | |
// Update position | |
this.x += this.velocityX; | |
this.y += this.velocityY; | |
// Animation | |
if (Math.abs(this.velocityX) > 0.5) { | |
this.animTimer++; | |
if (this.animTimer > 8) { | |
this.animTimer = 0; | |
this.animFrame = (this.animFrame + 1) % 2; | |
} | |
} else { | |
this.animFrame = 0; | |
} | |
// Check if fallen off the world | |
if (this.y > canvas.height + 100) { | |
this.die(); | |
} | |
}, | |
draw: function () { | |
ctx.save(); | |
// Flash when invulnerable | |
if (this.invulnerable % 10 < 5) { | |
ctx.globalAlpha = 0.5; | |
} | |
// Draw Mario | |
ctx.translate(this.x - camera.x, this.y); | |
// Body | |
ctx.fillStyle = "#E45826"; | |
ctx.fillRect( | |
-this.width / 2, | |
-this.height, | |
this.width, | |
this.height * 0.6, | |
); | |
// Overalls | |
ctx.fillStyle = "#3B48E0"; | |
ctx.fillRect( | |
-this.width / 2, | |
-this.height * 0.4, | |
this.width, | |
this.height * 0.4, | |
); | |
// Face | |
ctx.fillStyle = "#FDB383"; | |
ctx.fillRect( | |
-this.width / 2 + 2, | |
-this.height + 2, | |
this.width - 4, | |
this.height * 0.3, | |
); | |
// Hat | |
ctx.fillStyle = "#E45826"; | |
ctx.fillRect(-this.width / 2, -this.height - 4, this.width, 6); | |
// Eye | |
ctx.fillStyle = "#000"; | |
ctx.fillRect(this.facing > 0 ? 2 : -6, -this.height + 8, 3, 3); | |
// Mustache | |
ctx.fillRect(this.facing > 0 ? -2 : -8, -this.height + 12, 8, 2); | |
// Arms & legs animation | |
if (this.animFrame === 1) { | |
// Leg movement | |
ctx.fillStyle = "#3B48E0"; | |
ctx.fillRect(-this.width / 2 - 2, -4, 4, 4); | |
ctx.fillRect(this.width / 2 - 2, -8, 4, 4); | |
} | |
ctx.restore(); | |
}, | |
die: function () { | |
if (this.invulnerable > 0) return; | |
lives--; | |
if (lives <= 0) { | |
gameState = "gameOver"; | |
document.getElementById("gameOver").style.display = "block"; | |
} else { | |
// Reset position | |
this.x = 100; | |
this.y = 200; | |
this.velocityX = 0; | |
this.velocityY = 0; | |
this.invulnerable = 120; // 2 seconds of invulnerability | |
} | |
updateUI(); | |
}, | |
takeDamage: function () { | |
this.die(); | |
}, | |
}; | |
// Enemy class | |
class Enemy { | |
constructor(x, y) { | |
this.x = x; | |
this.y = y; | |
this.width = 28; | |
this.height = 28; | |
this.velocityX = -1; | |
this.velocityY = 0; | |
this.isDead = false; | |
this.animFrame = 0; | |
this.animTimer = 0; | |
} | |
update() { | |
if (this.isDead) return; | |
// Apply gravity | |
this.velocityY += 0.6; | |
if (this.velocityY > 15) this.velocityY = 15; | |
// Update position | |
this.x += this.velocityX; | |
this.y += this.velocityY; | |
// Animation | |
this.animTimer++; | |
if (this.animTimer > 15) { | |
this.animTimer = 0; | |
this.animFrame = (this.animFrame + 1) % 2; | |
} | |
// Simple AI - reverse direction at edges | |
const tileBelow = getTileAt( | |
this.x + this.velocityX * 20, | |
this.y + this.height / 2 + 10, | |
); | |
if (!tileBelow || tileBelow.type === "empty") { | |
this.velocityX *= -1; | |
} | |
} | |
draw() { | |
if (this.isDead) return; | |
ctx.save(); | |
ctx.translate(this.x - camera.x, this.y); | |
// Shell | |
ctx.fillStyle = "#4B8B3B"; | |
ctx.fillRect( | |
-this.width / 2, | |
-this.height / 2, | |
this.width, | |
this.height / 2, | |
); | |
ctx.fillStyle = "#6BAA5C"; | |
ctx.fillRect( | |
-this.width / 2 + 2, | |
-this.height / 2 + 2, | |
this.width - 4, | |
this.height / 2 - 4, | |
); | |
// Body | |
ctx.fillStyle = "#FFD93D"; | |
ctx.fillRect( | |
-this.width / 2 + 4, | |
0, | |
this.width - 8, | |
this.height / 2 - 4, | |
); | |
// Feet animation | |
if (this.animFrame === 0) { | |
ctx.fillRect(-this.width / 2 - 2, this.height / 2 - 4, 6, 4); | |
ctx.fillRect(this.width / 2 - 4, this.height / 2 - 4, 6, 4); | |
} else { | |
ctx.fillRect(-this.width / 2 + 2, this.height / 2 - 4, 6, 4); | |
ctx.fillRect(this.width / 2 - 8, this.height / 2 - 4, 6, 4); | |
} | |
// Eyes | |
ctx.fillStyle = "#000"; | |
ctx.fillRect(this.velocityX > 0 ? 4 : -8, -4, 3, 3); | |
ctx.restore(); | |
} | |
die() { | |
this.isDead = true; | |
score += 100; | |
updateUI(); | |
} | |
} | |
// Coin class | |
class Coin { | |
constructor(x, y) { | |
this.x = x; | |
this.y = y; | |
this.width = 20; | |
this.height = 20; | |
this.collected = false; | |
this.animTimer = 0; | |
} | |
update() { | |
if (!this.collected) { | |
this.animTimer += 0.1; | |
} | |
} | |
draw() { | |
if (this.collected) return; | |
ctx.save(); | |
ctx.translate(this.x - camera.x, this.y); | |
// Animated coin | |
const scale = Math.abs(Math.sin(this.animTimer)); | |
ctx.scale(scale, 1); | |
ctx.fillStyle = "#FFD700"; | |
ctx.beginPath(); | |
ctx.arc(0, 0, this.width / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.fillStyle = "#FFA500"; | |
ctx.beginPath(); | |
ctx.arc(0, 0, this.width / 2 - 3, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.restore(); | |
} | |
collect() { | |
if (!this.collected) { | |
this.collected = true; | |
coins++; | |
score += 10; | |
updateUI(); | |
} | |
} | |
} | |
// Level data | |
const TILE_SIZE = 32; | |
const level = [ | |
" ", | |
" ", | |
" $ $$$ ", | |
" === === ", | |
" $ === $ $ ", | |
" === === $ ", | |
" = E ===== E === ", | |
" E ===== E $ | ", | |
"============== ================ ========================= =======================", | |
"============== ================ ========================= =======================", | |
]; | |
// Parse level and create objects | |
const platforms = []; | |
const enemies = []; | |
const coinsList = []; | |
let flagPole = null; | |
for (let row = 0; row < level.length; row++) { | |
for (let col = 0; col < level[row].length; col++) { | |
const char = level[row][col]; | |
const x = col * TILE_SIZE; | |
const y = row * TILE_SIZE; | |
switch (char) { | |
case "=": | |
platforms.push({ | |
x, | |
y, | |
width: TILE_SIZE, | |
height: TILE_SIZE, | |
type: "ground", | |
}); | |
break; | |
case "E": | |
enemies.push(new Enemy(x + TILE_SIZE / 2, y)); | |
break; | |
case "$": | |
coinsList.push(new Coin(x + TILE_SIZE / 2, y + TILE_SIZE / 2)); | |
break; | |
case "|": | |
flagPole = { x: x + TILE_SIZE / 2, y: y }; | |
break; | |
} | |
} | |
} | |
// Get tile at position | |
function getTileAt(x, y) { | |
const col = Math.floor(x / TILE_SIZE); | |
const row = Math.floor(y / TILE_SIZE); | |
if ( | |
row < 0 || | |
row >= level.length || | |
col < 0 || | |
col >= level[row].length | |
) { | |
return { type: "empty" }; | |
} | |
const char = level[row][col]; | |
if (char === "=") { | |
return { | |
type: "ground", | |
x: col * TILE_SIZE, | |
y: row * TILE_SIZE, | |
width: TILE_SIZE, | |
height: TILE_SIZE, | |
}; | |
} | |
return { type: "empty" }; | |
} | |
// Collision detection | |
function checkCollision(rect1, rect2) { | |
return ( | |
rect1.x - rect1.width / 2 < rect2.x + rect2.width / 2 && | |
rect1.x + rect1.width / 2 > rect2.x - rect2.width / 2 && | |
rect1.y - rect1.height < rect2.y + rect2.height / 2 && | |
rect1.y > rect2.y - rect2.height / 2 | |
); | |
} | |
function checkPlatformCollision(entity) { | |
entity.isGrounded = false; | |
// Check all platforms | |
for (let platform of platforms) { | |
const platRect = { | |
x: platform.x + platform.width / 2, | |
y: platform.y + platform.height / 2, | |
width: platform.width, | |
height: platform.height, | |
}; | |
if (checkCollision(entity, platRect)) { | |
// Top collision (landing) | |
if (entity.velocityY > 0 && entity.y - entity.height < platform.y) { | |
entity.y = platform.y; | |
entity.velocityY = 0; | |
entity.isGrounded = true; | |
} | |
// Bottom collision (hitting head) | |
else if ( | |
entity.velocityY < 0 && | |
entity.y > platform.y + platform.height | |
) { | |
entity.y = platform.y + platform.height + entity.height; | |
entity.velocityY = 0; | |
} | |
// Side collisions | |
else if (entity.x < platform.x + platform.width / 2) { | |
entity.x = platform.x - entity.width / 2; | |
entity.velocityX = 0; | |
} else { | |
entity.x = platform.x + platform.width + entity.width / 2; | |
entity.velocityX = 0; | |
} | |
} | |
} | |
} | |
// Update camera | |
function updateCamera() { | |
camera.x = player.x - canvas.width / 2; | |
if (camera.x < 0) camera.x = 0; | |
if (camera.x > level[0].length * TILE_SIZE - canvas.width) { | |
camera.x = level[0].length * TILE_SIZE - canvas.width; | |
} | |
} | |
// Update UI | |
function updateUI() { | |
document.getElementById("score").textContent = score; | |
document.getElementById("coins").textContent = coins; | |
document.getElementById("lives").textContent = lives; | |
document.getElementById("timer").textContent = timer; | |
} | |
// Draw background | |
function drawBackground() { | |
// Sky gradient | |
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); | |
gradient.addColorStop(0, "#5C94FC"); | |
gradient.addColorStop(1, "#8ED2FF"); | |
ctx.fillStyle = gradient; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// Clouds | |
ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; | |
for (let i = 0; i < 5; i++) { | |
const x = (i * 200 - camera.x * 0.3) % (canvas.width + 100); | |
const y = 50 + (i % 2) * 30; | |
// Simple cloud shape | |
ctx.beginPath(); | |
ctx.arc(x, y, 25, 0, Math.PI * 2); | |
ctx.arc(x + 25, y, 35, 0, Math.PI * 2); | |
ctx.arc(x + 50, y, 25, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
// Hills in background | |
ctx.fillStyle = "#3EAD3E"; | |
for (let i = 0; i < 3; i++) { | |
const x = (i * 300 - camera.x * 0.5) % (canvas.width + 200); | |
const y = canvas.height - 100; | |
ctx.beginPath(); | |
ctx.arc(x, y, 80, 0, Math.PI, true); | |
ctx.fill(); | |
} | |
} | |
// Draw platforms | |
function drawPlatforms() { | |
platforms.forEach((platform) => { | |
const x = platform.x - camera.x; | |
// Only draw visible platforms | |
if (x + platform.width > 0 && x < canvas.width) { | |
// Ground block | |
ctx.fillStyle = "#C84C0C"; | |
ctx.fillRect(x, platform.y, platform.width, platform.height); | |
// Inner detail | |
ctx.fillStyle = "#FC9838"; | |
ctx.fillRect( | |
x + 2, | |
platform.y + 2, | |
platform.width - 4, | |
platform.height - 4, | |
); | |
// Grid pattern | |
ctx.strokeStyle = "#C84C0C"; | |
ctx.lineWidth = 2; | |
ctx.strokeRect( | |
x + 2, | |
platform.y + 2, | |
platform.width - 4, | |
platform.height - 4, | |
); | |
// Brick lines | |
ctx.beginPath(); | |
ctx.moveTo(x + platform.width / 2, platform.y + 2); | |
ctx.lineTo( | |
x + platform.width / 2, | |
platform.y + platform.height - 2, | |
); | |
ctx.moveTo(x + 2, platform.y + platform.height / 2); | |
ctx.lineTo( | |
x + platform.width - 2, | |
platform.y + platform.height / 2, | |
); | |
ctx.stroke(); | |
} | |
}); | |
} | |
// Draw flag | |
function drawFlag() { | |
if (!flagPole) return; | |
const x = flagPole.x - camera.x; | |
// Pole | |
ctx.fillStyle = "#4B8B3B"; | |
ctx.fillRect(x - 2, flagPole.y, 4, 200); | |
// Flag | |
ctx.fillStyle = "#FFFFFF"; | |
ctx.beginPath(); | |
ctx.moveTo(x + 2, flagPole.y); | |
ctx.lineTo(x + 40, flagPole.y + 20); | |
ctx.lineTo(x + 2, flagPole.y + 40); | |
ctx.closePath(); | |
ctx.fill(); | |
// Flag detail | |
ctx.fillStyle = "#FF0000"; | |
ctx.beginPath(); | |
ctx.moveTo(x + 2, flagPole.y + 10); | |
ctx.lineTo(x + 25, flagPole.y + 20); | |
ctx.lineTo(x + 2, flagPole.y + 30); | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
// Game loop | |
function gameLoop() { | |
if (gameState === "playing") { | |
// Update timer | |
const now = Date.now(); | |
if (now - lastTimerUpdate > 1000) { | |
timer--; | |
lastTimerUpdate = now; | |
updateUI(); | |
if (timer <= 0) { | |
player.die(); | |
} | |
} | |
// Update player | |
player.update(); | |
checkPlatformCollision(player); | |
// Update enemies | |
enemies.forEach((enemy) => { | |
enemy.update(); | |
checkPlatformCollision(enemy); | |
// Check collision with player | |
if (!enemy.isDead && checkCollision(player, enemy)) { | |
// Check if player is jumping on enemy | |
if (player.velocityY > 0 && player.y < enemy.y) { | |
enemy.die(); | |
player.velocityY = -8; // Bounce | |
} else { | |
player.takeDamage(); | |
} | |
} | |
}); | |
// Update coins | |
coinsList.forEach((coin) => { | |
coin.update(); | |
if (!coin.collected && checkCollision(player, coin)) { | |
coin.collect(); | |
} | |
}); | |
// Check for level complete | |
if (flagPole && Math.abs(player.x - flagPole.x) < 20) { | |
gameState = "levelComplete"; | |
score += timer * 10; | |
document.getElementById("gameOverText").textContent = | |
"LEVEL COMPLETE!"; | |
document.getElementById("gameOver").style.display = "block"; | |
} | |
// Update camera | |
updateCamera(); | |
} | |
// Render | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw everything | |
drawBackground(); | |
drawPlatforms(); | |
drawFlag(); | |
// Draw coins | |
coinsList.forEach((coin) => coin.draw()); | |
// Draw enemies | |
enemies.forEach((enemy) => enemy.draw()); | |
// Draw player | |
player.draw(); | |
requestAnimationFrame(gameLoop); | |
} | |
// Restart game | |
function restartGame() { | |
// Reset game state | |
gameState = "playing"; | |
score = 0; | |
coins = 0; | |
lives = 3; | |
timer = 300; | |
lastTimerUpdate = Date.now(); | |
// Reset player | |
player.x = 100; | |
player.y = 200; | |
player.velocityX = 0; | |
player.velocityY = 0; | |
player.isDead = false; | |
player.invulnerable = 0; | |
// Reset enemies | |
enemies.forEach((enemy, index) => { | |
enemy.isDead = false; | |
enemy.x = | |
platforms.find( | |
(p) => | |
level[Math.floor(enemy.y / TILE_SIZE)][ | |
Math.floor(enemy.x / TILE_SIZE) | |
] === "E", | |
).x + | |
TILE_SIZE / 2; | |
}); | |
// Reset coins | |
coinsList.forEach((coin) => (coin.collected = false)); | |
// Hide game over screen | |
document.getElementById("gameOver").style.display = "none"; | |
// Update UI | |
updateUI(); | |
} | |
// Start game | |
updateUI(); | |
gameLoop(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment