Skip to content

Instantly share code, notes, and snippets.

@igorcosta
Created December 29, 2025 08:38
Show Gist options
  • Select an option

  • Save igorcosta/b9f5d53d30391cba36d0dbf18a953e0e to your computer and use it in GitHub Desktop.

Select an option

Save igorcosta/b9f5d53d30391cba36d0dbf18a953e0e to your computer and use it in GitHub Desktop.
Simple Arrow game with balloons
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Balloon Popper</title>
<style>
body {
margin: 0;
background: linear-gradient(to bottom, #87CEEB, #E0F6FF);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
user-select: none;
}
#ui-layer {
position: absolute;
top: 20px;
left: 20px;
z-index: 10;
display: flex;
gap: 20px;
color: #333;
font-weight: bold;
font-size: 1.2rem;
text-shadow: 1px 1px 2px white;
}
.stat-box {
background: rgba(255, 255, 255, 0.7);
padding: 8px 15px;
border-radius: 10px;
border: 2px solid #fff;
}
#game-area {
position: relative;
width: 800px;
height: 600px;
background: rgba(255, 255, 255, 0.2);
margin-top: 20px;
border: 2px solid #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* Elements */
.archer {
position: absolute;
bottom: 50px;
left: 50px;
width: 60px;
height: 100px;
background: #333;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.archer::before {
content: '🏹';
font-size: 40px;
position: absolute;
top: -20px;
left: 10px;
transform: rotate(-45deg);
}
.balloon {
position: absolute;
width: 40px;
height: 50px;
border-radius: 50% 50% 50% 50% / 40% 40% 60% 60%;
z-index: 2;
transition: transform 0.1s;
}
.balloon::after {
content: '';
position: absolute;
bottom: -15px;
left: 19px;
width: 2px;
height: 15px;
background: rgba(0,0,0,0.2);
}
/* Colors for balloons */
.color-0 { background: radial-gradient(circle at 30% 30%, #ff4d4d, #b30000); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.color-1 { background: radial-gradient(circle at 30% 30%, #4dff4d, #00b300); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.color-2 { background: radial-gradient(circle at 30% 30%, #4d4dff, #0000b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.color-3 { background: radial-gradient(circle at 30% 30%, #ffff4d, #b3b300); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.color-4 { background: radial-gradient(circle at 30% 30%, #ff4dff, #b300b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.color-5 { background: radial-gradient(circle at 30% 30%, #4dffff, #00b3b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); }
.arrow {
position: absolute;
width: 40px;
height: 6px;
background: #5d4037;
border-radius: 3px;
z-index: 3;
transform-origin: center;
}
.arrow::after {
content: '';
position: absolute;
right: -10px;
top: -3px;
width: 0;
height: 0;
border-left: 10px solid #5d4037;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
}
.arrow::before {
content: '';
position: absolute;
left: -5px;
top: 0;
width: 6px;
height: 6px;
background: #8d6e63;
border-radius: 50%;
}
.pop-effect {
position: absolute;
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
animation: pop 0.3s ease-out forwards;
pointer-events: none;
z-index: 5;
}
@keyframes pop {
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
#game-over {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.8);
color: white;
display: none; /* hidden by default */
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 20;
}
#game-over h1 { font-size: 3rem; margin-bottom: 10px; }
#game-over p { font-size: 1.5rem; margin-bottom: 20px; }
button {
padding: 15px 30px;
font-size: 1.2rem;
background: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
font-weight: bold;
}
button:hover { background: #45a049; }
.instruction {
margin-top: 10px;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px black;
}
</style>
</head>
<body>
<div class="instruction">Click anywhere in the game area to Shoot!</div>
<div id="ui-layer">
<div class="stat-box">Popped: <span id="score">0</span> / 100</div>
<div class="stat-box">Arrows: <span id="arrows-left">100</span></div>
<div class="stat-box">Balloons: <span id="balloons-left">100</span></div>
</div>
<div id="game-area">
<!-- The Archer -->
<div class="archer"></div>
<!-- Game Over Screen -->
<div id="game-over">
<h1 id="result-title">Game Over</h1>
<p id="result-score">Score: 0</p>
<button onclick="resetGame()">Play Again</button>
</div>
</div>
<script>
// Configuration
const CONFIG = {
maxArrows: 100,
maxBalloons: 100,
balloonSpeedMin: 1.5,
balloonSpeedMax: 3.5,
arrowSpeed: 12,
spawnRate: 800, // ms between balloons
colors: ['color-0', 'color-1', 'color-2', 'color-3', 'color-4', 'color-5']
};
// State
let gameState = {
playing: false,
score: 0,
arrowsLeft: 0,
balloonsLeft: 0,
balloonsOnScreen: [],
arrowsOnScreen: [],
lastSpawn: 0,
animId: null
};
// DOM Elements
const gameArea = document.getElementById('game-area');
const scoreEl = document.getElementById('score');
const arrowsEl = document.getElementById('arrows-left');
const balloonsEl = document.getElementById('balloons-left');
const gameOverScreen = document.getElementById('game-over');
const resultTitle = document.getElementById('result-title');
const resultScore = document.getElementById('result-score');
// --- Input Handling ---
// Use mousedown for responsiveness (vs click which waits for mouse up)
gameArea.addEventListener('mousedown', (e) => {
// Only shoot if playing and not clicking UI buttons
if (gameState.playing && e.target.tagName !== 'BUTTON') {
shootArrow(e);
}
});
// --- Game Logic ---
function resetGame() {
// Clear any existing game loop
if (gameState.animId) cancelAnimationFrame(gameState.animId);
// Reset State
gameState = {
playing: true,
score: 0,
arrowsLeft: CONFIG.maxArrows,
balloonsLeft: CONFIG.maxBalloons,
balloonsOnScreen: [],
arrowsOnScreen: [],
lastSpawn: performance.now(),
animId: null
};
// Clear DOM
const elements = document.querySelectorAll('.balloon, .arrow, .pop-effect');
elements.forEach(el => el.remove());
// Update UI
updateUI();
gameOverScreen.style.display = 'none';
// Start Loop
requestAnimationFrame(gameLoop);
}
function shootArrow(e) {
if (gameState.arrowsLeft <= 0) return; // No arrows left
gameState.arrowsLeft--;
updateUI();
// Calculate spawn position (Archer position)
const startX = 110; // Left of archer + offset
const startY = 460; // Middle height of archer area
// Calculate angle towards click
const rect = gameArea.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const angle = Math.atan2(clickY - startY, clickX - startX);
const arrow = {
el: document.createElement('div'),
x: startX,
y: startY,
vx: Math.cos(angle) * CONFIG.arrowSpeed,
vy: Math.sin(angle) * CONFIG.arrowSpeed,
width: 40,
height: 6
};
arrow.el.classList.add('arrow');
// Visual rotation based on angle
arrow.el.style.transform = `rotate(${angle}rad)`;
gameArea.appendChild(arrow.el);
gameState.arrowsOnScreen.push(arrow);
}
function spawnBalloon(timestamp) {
if (gameState.balloonsLeft <= 0) return;
if (timestamp - gameState.lastSpawn > CONFIG.spawnRate) {
gameState.balloonsLeft--;
updateUI();
const balloon = {
el: document.createElement('div'),
x: 650, // Start right side
y: Math.random() * (500 - 50) + 20, // Random Y
width: 40,
height: 50,
speed: Math.random() * (CONFIG.balloonSpeedMax - CONFIG.balloonSpeedMin) + CONFIG.balloonSpeedMin,
color: CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)]
};
balloon.el.classList.add('balloon', balloon.color);
gameArea.appendChild(balloon.el);
gameState.balloonsOnScreen.push(balloon);
gameState.lastSpawn = timestamp;
}
}
function updateEntities() {
// Update Arrows
for (let i = gameState.arrowsOnScreen.length - 1; i >= 0; i--) {
const arrow = gameState.arrowsOnScreen[i];
arrow.x += arrow.vx;
arrow.y += arrow.vy;
// Render
arrow.el.style.left = arrow.x + 'px';
arrow.el.style.top = arrow.y + 'px';
// Check bounds (remove if off screen)
if (arrow.x > 820 || arrow.x < -50 || arrow.y > 620 || arrow.y < -50) {
arrow.el.remove();
gameState.arrowsOnScreen.splice(i, 1);
}
}
// Update Balloons
for (let i = gameState.balloonsOnScreen.length - 1; i >= 0; i--) {
const balloon = gameState.balloonsOnScreen[i];
balloon.x -= balloon.speed; // Move left
// Render
balloon.el.style.left = balloon.x + 'px';
balloon.el.style.top = balloon.y + 'px';
// Check bounds (missed)
if (balloon.x < -50) {
balloon.el.remove();
gameState.balloonsOnScreen.splice(i, 1);
}
}
}
function checkCollisions() {
// Optimization: Iterate over arrows (usually fewer than balloons potentially)
for (let i = gameState.arrowsOnScreen.length - 1; i >= 0; i--) {
const arrow = gameState.arrowsOnScreen[i];
// Simple Circle/Rect collision logic
// Treat Arrow as a small point for hit detection accuracy or a small rect
const arrowRect = {
x: arrow.x + 20, // tip
y: arrow.y,
w: 10,
h: 6
};
for (let j = gameState.balloonsOnScreen.length - 1; j >= 0; j--) {
const balloon = gameState.balloonsOnScreen[j];
// Check AABB overlap
// Balloon dimensions
const bRect = {
x: balloon.x,
y: balloon.y,
w: balloon.width,
h: balloon.height
};
// Hit detection
if (arrowRect.x < bRect.x + bRect.w &&
arrowRect.x + arrowRect.w > bRect.x &&
arrowRect.y < bRect.y + bRect.h &&
arrowRect.y + arrowRect.h > bRect.y) {
// POP!
createPopEffect(balloon.x, balloon.y);
// Remove balloon
balloon.el.remove();
gameState.balloonsOnScreen.splice(j, 1);
// Remove Arrow
arrow.el.remove();
gameState.arrowsOnScreen.splice(i, 1);
// Update Score
gameState.score++;
updateUI();
// Stop checking this specific arrow
break;
}
}
}
}
function createPopEffect(x, y) {
const effect = document.createElement('div');
effect.classList.add('pop-effect');
effect.style.left = x + 'px';
effect.style.top = y + 'px';
gameArea.appendChild(effect);
// Remove after animation
setTimeout(() => effect.remove(), 300);
}
function updateUI() {
scoreEl.textContent = gameState.score;
arrowsEl.textContent = gameState.arrowsLeft;
balloonsEl.textContent = gameState.balloonsLeft;
}
function checkEndCondition() {
// Conditions to end game
// 1. Balloons are all popped (Victory)
// 2. Arrows are 0 AND no balloons on screen (Defeat)
const noBalloonsLeft = gameState.balloonsLeft === 0 && gameState.balloonsOnScreen.length === 0;
const outOfArrows = gameState.arrowsLeft === 0 && gameState.arrowsOnScreen.length === 0;
if (noBalloonsLeft) {
endGame(true);
return true;
}
if (outOfArrows) {
endGame(false);
return true;
}
return false;
}
function endGame(victory) {
gameState.playing = false;
cancelAnimationFrame(gameState.animId);
resultTitle.textContent = victory ? "VICTORY!" : "GAME OVER";
resultTitle.style.color = victory ? "#4CAF50" : "#ff4d4d";
resultScore.textContent = `Score: ${gameState.score} / ${CONFIG.maxBalloons}`;
gameOverScreen.style.display = 'flex';
}
// --- Main Loop ---
function gameLoop(timestamp) {
if (!gameState.playing) return;
// 1. Spawn
spawnBalloon(timestamp);
// 2. Update positions
updateEntities();
// 3. Check physics (collisions)
checkCollisions();
// 4. Check End
if (checkEndCondition()) return;
// 5. Continue
gameState.animId = requestAnimationFrame(gameLoop);
}
// Initialize on load
// We wait for the user to click "Play Again" which calls resetGame,
// but we can show the over screen initially to prompt start.
window.onload = () => {
// Initialize with 0 stats visually
updateUI();
// Start immediately
resetGame();
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment