Night has fallen and a little bat 🦇 is flying through the graveyard. Tap, click or press Space to flap its wings and slip through the gaps between the haunted gravestones. Every gap you pass scores a point, and the night gets faster as you go. One touch and the game is over. How far can you fly under the full moon?
.bat-game {
text-align: center;
}
.instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.bat-info {
margin-bottom: 10px;
font-size: 1.2em;
}
#batCanvas {
display: block;
margin: 0 auto;
max-width: 100%;
border: 3px solid #ff7518;
border-radius: 10px;
background: #120a1f;
touch-action: manipulation;
cursor: pointer;
}
.bat-message {
margin-top: 15px;
font-weight: bold;
}
.start-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #ff7518;
color: #1a1025;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.start-btn:hover {
background-color: #ff8f45;
}
.start-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
<div class="bat-game">
<h1>🦇 Bat Flap 🌕</h1>
<div class="instructions">
Tap, click or press Space to flap your wings. Fly through the gaps between the haunted gravestones and don't touch anything!
</div>
<div class="bat-info">Score: <span id="batScore">0</span></div>
<canvas id="batCanvas" width="320" height="400"></canvas>
<div class="bat-message" id="batMessage">Press "Start" to fly into the night...</div>
<button class="start-btn" id="batStart">Start</button>
</div>
const canvas = document.getElementById('batCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('batScore');
const message = document.getElementById('batMessage');
const startButton = document.getElementById('batStart');
const hiddenScore = document.getElementById('hiddenScore');
const W = canvas.width;
const H = canvas.height;
const GRAVITY = 0.22; // per 1/60s step
const FLAP = -4.6;
const PIPE_SPACING = 190;
const PIPE_WIDTH = 50;
const GAP = 125;
const BAT_X = 70;
const BAT_SIZE = 28;
let batY, velocity, pipes, score, running, waiting, lastTime, animationId, distance;
function reset() {
batY = H / 2;
velocity = 0;
pipes = [];
score = 0;
distance = 0;
scoreDisplay.textContent = 0;
hiddenScore.value = 0;
}
function flap() {
if (!running) {
return;
}
if (waiting) {
// The first flap starts the flight
waiting = false;
message.textContent = 'Keep flapping!';
lastTime = performance.now();
}
velocity = FLAP;
}
function addPipe() {
const top = 60 + Math.random() * (H - GAP - 120);
pipes.push({ x: W, top: top, passed: false });
}
function drawBackground() {
ctx.fillStyle = '#120a1f';
ctx.fillRect(0, 0, W, H);
ctx.font = '50px serif';
ctx.textAlign = 'center';
ctx.fillText('🌕', W - 50, 60);
}
function drawPipe(pipe) {
ctx.fillStyle = '#4a4458';
ctx.fillRect(pipe.x, 0, PIPE_WIDTH, pipe.top);
ctx.fillRect(pipe.x, pipe.top + GAP, PIPE_WIDTH, H - pipe.top - GAP);
ctx.fillStyle = '#6b6580';
ctx.fillRect(pipe.x - 4, pipe.top - 14, PIPE_WIDTH + 8, 14);
ctx.fillRect(pipe.x - 4, pipe.top + GAP, PIPE_WIDTH + 8, 14);
}
function drawBat() {
ctx.font = BAT_SIZE + 'px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🦇', BAT_X, batY);
}
function hits(pipe) {
const r = BAT_SIZE / 2 - 4;
const overlapsX = BAT_X + r > pipe.x && BAT_X - r < pipe.x + PIPE_WIDTH;
return overlapsX && (batY - r < pipe.top || batY + r > pipe.top + GAP);
}
function endGame() {
running = false;
cancelAnimationFrame(animationId);
message.textContent = 'You crashed! Final score: ' + score;
startButton.disabled = false;
document.querySelector('.share-buttons').style.display = 'flex'; // Show share buttons
}
function loop(now) {
if (waiting) {
// Hover gently until the player flaps for the first time
batY = H / 2 + Math.sin(now / 200) * 6;
drawBackground();
drawBat();
animationId = requestAnimationFrame(loop);
return;
}
// Frame-rate independent step (1 = one frame at 60fps)
const step = Math.min((now - lastTime) / (1000 / 60), 3);
lastTime = now;
velocity += GRAVITY * step;
batY += velocity * step;
const speed = 2 + Math.min(score * 0.05, 1.5);
distance += speed * step;
if (pipes.length === 0 || distance >= PIPE_SPACING) {
addPipe();
distance = 0;
}
pipes.forEach(pipe => pipe.x -= speed * step);
pipes = pipes.filter(pipe => pipe.x > -PIPE_WIDTH - 10);
drawBackground();
pipes.forEach(drawPipe);
drawBat();
for (const pipe of pipes) {
if (hits(pipe)) {
endGame();
return;
}
if (!pipe.passed && pipe.x + PIPE_WIDTH < BAT_X) {
pipe.passed = true;
score++;
scoreDisplay.textContent = score;
hiddenScore.value = score; // Update hidden score field
}
}
if (batY < 0 || batY > H) {
endGame();
return;
}
animationId = requestAnimationFrame(loop);
}
function startGame() {
reset();
running = true;
waiting = true;
startButton.disabled = true;
message.textContent = 'Tap, click or press Space to start flying!';
document.querySelector('.share-buttons').style.display = 'none';
animationId = requestAnimationFrame(loop);
}
canvas.addEventListener('mousedown', flap);
canvas.addEventListener('touchstart', event => {
event.preventDefault();
flap();
}, { passive: false });
document.addEventListener('keydown', event => {
if (event.code === 'Space' && running) {
event.preventDefault();
flap();
}
});
startButton.addEventListener('click', startGame);
running = false;
waiting = false;
reset();
drawBackground();
drawBat();