Skip to content

Instantly share code, notes, and snippets.

@lolpack
Last active April 11, 2025 12:37
Show Gist options
  • Select an option

  • Save lolpack/412a1009be427dbecc29a3ab4ce85f92 to your computer and use it in GitHub Desktop.

Select an option

Save lolpack/412a1009be427dbecc29a3ab4ce85f92 to your computer and use it in GitHub Desktop.
Flappy Bird Learning Game
<!-- View here https://gist.githack.com/lolpack/412a1009be427dbecc29a3ab4ce85f92/raw/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Flappy You+ Configurable</title>
<style>
body {
margin: 0;
display: flex;
height: 100vh;
font-family: sans-serif;
}
.code-pane, .game-pane {
width: 50%;
height: 100%;
}
.code-pane {
background: #1e1e1e;
color: #00ff90;
display: flex;
flex-direction: column;
padding: 10px;
}
#code-editor {
flex-grow: 1;
font-family: monospace;
font-size: 16px;
background: #1e1e1e;
color: #00ff90;
border: none;
padding: 10px;
resize: none;
}
button {
padding: 12px;
background: #00ff90;
color: #000;
font-weight: bold;
font-size: 16px;
cursor: pointer;
border: none;
margin-top: 10px;
}
.game-pane {
background: #87ceeb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
canvas {
border: 2px solid black;
margin-top: 10px;
}
video {
display: none;
}
.score {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
font-weight: bold;
color: white;
text-shadow: 1px 1px 3px black;
}
</style>
</head>
<body>
<div class="code-pane">
<textarea id="code-editor">
// Hello Commodore John Rodgers!
// Customize your game below and learn a little JavaScript!
const playerName = "YourNameHere";
const tubeColor = "green";
const tubeCapColor = "darkgreen";
const gravitySetting = 0.25;
const jumpForce = -6;
const hatEmoji = ""; // Try 🧢 👑 🎩 🎓 👒
</textarea>
<button id="start">Start</button>
</div>
<div class="game-pane">
<div class="score" id="score-display">Score: 0</div>
<video id="video" autoplay playsinline></video>
<canvas id="canvas" width="300" height="500"></canvas>
</div>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const video = document.getElementById("video");
const startBtn = document.getElementById("start");
const codeEditor = document.getElementById("code-editor");
const scoreDisplay = document.getElementById("score-display");
let playerName = "Player";
let tubeColor = "green";
let tubeCapColor = "darkgreen";
let gravity = 0.25;
let jumpForce = -6;
let faceImage = new Image();
let y = 200;
let velocity = 0;
let isPlaying = false;
let hasGameStarted = false;
let pipes = [];
let hatEmoji = "";
const pipeGap = 240;
const pipeSpeed = 2;
let frame = 0;
let animationId = null;
let score = 0;
startBtn.onclick = async () => {
if (hasGameStarted) return;
hasGameStarted = true;
try {
const code = codeEditor.value;
// Safely evaluate user code in its own scope
const userSettings = new Function(`
${code}
return {
playerName,
tubeColor,
tubeCapColor,
gravitySetting,
jumpForce,
hatEmoji
};
`)();
// Apply config
playerName = userSettings.playerName || "Player";
tubeColor = userSettings.tubeColor || "green";
tubeCapColor = userSettings.tubeCapColor || "darkgreen";
gravity = userSettings.gravitySetting ?? 0.25;
jumpForce = userSettings.jumpForce ?? -6;
hatEmoji = userSettings.hatEmoji || "";
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await new Promise(res => setTimeout(res, 1000));
const snap = document.createElement("canvas");
snap.width = video.videoWidth;
snap.height = video.videoHeight;
const snapCtx = snap.getContext("2d");
snapCtx.drawImage(video, 0, 0);
faceImage.src = snap.toDataURL();
stream.getTracks().forEach(t => t.stop());
y = 200;
velocity = 0;
pipes = [];
frame = 0;
score = 0;
scoreDisplay.textContent = "Score: 0";
isPlaying = true;
cancelAnimationFrame(animationId);
loop();
} catch (e) {
alert("Error: " + e.message);
hasGameStarted = false;
}
};
function loop() {
if (!isPlaying) return;
animationId = requestAnimationFrame(loop);
frame++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (frame > 60 && frame % 100 === 0) {
const top = Math.floor(Math.random() * (canvas.height - pipeGap - 80)) + 20;
pipes.push({ x: canvas.width, top, passed: false });
}
for (let i = pipes.length - 1; i >= 0; i--) {
pipes[i].x -= pipeSpeed;
drawPipe(pipes[i].x, 0, pipes[i].top, true);
drawPipe(pipes[i].x, pipes[i].top + pipeGap, canvas.height - pipes[i].top - pipeGap, false);
if (frame > 100) {
if (
120 < pipes[i].x + 60 &&
120 + 60 > pipes[i].x &&
(y < pipes[i].top || y + 60 > pipes[i].top + pipeGap)
) {
gameOver();
}
}
if (!pipes[i].passed && pipes[i].x + 60 < 120) {
pipes[i].passed = true;
score++;
scoreDisplay.textContent = "Score: " + score;
}
if (pipes[i].x < -60) pipes.splice(i, 1);
}
velocity += gravity;
velocity *= 0.98;
y += velocity;
if (y > canvas.height - 60) {
y = canvas.height - 60;
velocity = 0;
gameOver();
}
if (y < 0) {
y = 0;
velocity = 0;
}
if (faceImage.complete && faceImage.naturalWidth > 0) {
ctx.save();
ctx.translate(150, y + 30);
ctx.rotate(velocity * 0.03);
// Clip to circle for face
ctx.beginPath();
ctx.arc(0, 0, 30, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(faceImage, -30, -30, 60, 60);
ctx.restore();
// Draw hat emoji slightly above head
ctx.save();
ctx.translate(150, y + 60);
ctx.rotate(velocity * 0.03);
ctx.font = "50px serif";
ctx.textAlign = "center";
ctx.fillText(hatEmoji, 0, -40); // slightly above circle
ctx.restore();
}
ctx.fillStyle = "black";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.fillText(playerName, 150, y + 75);
}
function drawPipe(x, y, height, isTop) {
ctx.fillStyle = tubeColor;
ctx.fillRect(x, y, 60, height);
ctx.fillStyle = tubeCapColor;
if (isTop) {
ctx.fillRect(x - 5, y + height - 10, 70, 10);
} else {
ctx.fillRect(x - 5, y, 70, 10);
}
}
function gameOver() {
isPlaying = false;
hasGameStarted = false;
cancelAnimationFrame(animationId);
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#fff";
ctx.font = "24px Arial";
ctx.textAlign = "center";
ctx.fillText("Game Over", canvas.width / 2, canvas.height / 2);
ctx.font = "16px Arial";
ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2 + 30);
ctx.fillText("Click Start to try again", canvas.width / 2, canvas.height / 2 + 55);
}
document.addEventListener("keydown", (e) => {
if (e.code === "Space" && isPlaying && y > 0) {
velocity = jumpForce;
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment