The first snow has fallen and the whole street has built snow forts ❄️. Kids keep popping up behind them, snowball in hand. Tap or click each one to hit them with your snowball before they throw theirs at you. Every hit scores a point, but a kid you miss gets you, and you only have three lives. Watch out for the penguins 🐧 hiding in the forts too: hit one and you lose a life. The kids pop up faster and faster as the fight goes on.
How many snowballs can you land before you're covered in snow?
.snow-game {
text-align: center;
}
.snow-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.snow-info {
margin-bottom: 10px;
font-size: 1.2em;
}
.snow-field {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
width: 330px;
max-width: 100%;
margin: 0 auto;
padding: 12px;
box-sizing: border-box;
border: 3px solid #1e88e5;
border-radius: 12px;
background: linear-gradient(#cfe8fb, #f5fbff);
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
}
.snow-fort {
position: relative;
height: 90px;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
background: #e3f2fd;
-webkit-tap-highlight-color: transparent;
}
.snow-fort::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 38px;
background: #ffffff;
border-radius: 50% 50% 8px 8px / 30px 30px 8px 8px;
box-shadow: inset 0 -6px 0 #d6e6f2;
pointer-events: none;
}
.snow-pop {
position: absolute;
left: 0;
right: 0;
bottom: -60px;
font-size: 44px;
line-height: 1;
transition: bottom 0.12s ease-out;
pointer-events: none;
}
.snow-fort.up .snow-pop {
bottom: 22px;
}
.snow-fort.hit .snow-pop::after {
content: '💥';
position: absolute;
left: 50%;
top: -6px;
font-size: 30px;
}
.snow-fort.bad {
background: #ffcdd2;
}
.snow-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.4em;
}
.snow-game .start-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #1e88e5;
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.snow-game .start-btn:hover {
background-color: #42a5f5;
}
.snow-game .start-btn:disabled {
background-color: #ccc;
cursor: default;
}
<div class="snow-game">
<h1>❄️ Snowball Fight ⛄</h1>
<div class="instructions">
Tap or click the kids who pop up behind the snow forts to hit them with a snowball before they throw theirs. Don't hit the penguins!
</div>
<div class="snow-info">Score: <span id="snowScore">0</span> | Lives: <span id="snowLives">❤️❤️❤️</span> | Best: <span id="snowBest">0</span></div>
<div class="snow-field" id="snowField"></div>
<div class="snow-message" id="snowMessage">Press "Start" to begin the snowball fight!</div>
<button class="start-btn" id="snowStart">Start</button>
</div>
const field = document.getElementById('snowField');
const scoreDisplay = document.getElementById('snowScore');
const livesDisplay = document.getElementById('snowLives');
const bestDisplay = document.getElementById('snowBest');
const message = document.getElementById('snowMessage');
const startButton = document.getElementById('snowStart');
const hiddenScore = document.getElementById('hiddenScore');
const shareButtons = document.querySelector('.share-buttons');
const FORTS = 9;
const KIDS = ['🧒', '👧', '👦', '⛄'];
const PENGUIN = '🐧';
const MAX_LIVES = 3;
let forts = [];
let score, lives, running, lastTime, spawnTimer, elapsed, animationId, best;
best = 0;
try {
best = parseInt(localStorage.getItem('snowballFightBest'), 10) || 0;
} catch (e) {
best = 0;
}
bestDisplay.textContent = best;
for (let i = 0; i < FORTS; i++) {
const el = document.createElement('div');
el.className = 'snow-fort';
const pop = document.createElement('div');
pop.className = 'snow-pop';
el.appendChild(pop);
field.appendChild(el);
const fort = { el: el, pop: pop, active: false, penguin: false, timeLeft: 0, hitTimer: 0 };
forts.push(fort);
el.addEventListener('touchstart', function (e) {
e.preventDefault();
hitFort(fort);
}, { passive: false });
el.addEventListener('mousedown', function () {
hitFort(fort);
});
}
function showLives() {
let text = '';
for (let i = 0; i < MAX_LIVES; i++) {
text += i < lives ? '❤️' : '🤍';
}
livesDisplay.textContent = text;
}
function setScore(value) {
score = value;
scoreDisplay.textContent = score;
if (hiddenScore) {
hiddenScore.value = score;
}
}
function hide(fort) {
fort.active = false;
fort.el.classList.remove('up');
}
function flashBad(fort) {
fort.el.classList.add('bad');
setTimeout(function () {
fort.el.classList.remove('bad');
}, 300);
}
function loseLife(fort, text) {
lives -= 1;
showLives();
flashBad(fort);
message.textContent = text;
if (lives <= 0) {
endGame();
}
}
function hitFort(fort) {
if (!running || !fort.active || fort.hitTimer > 0) {
return;
}
if (fort.penguin) {
hide(fort);
loseLife(fort, 'Oops, not the penguin! 🐧');
return;
}
fort.hitTimer = 0.25;
fort.el.classList.add('hit');
setScore(score + 1);
message.textContent = score % 10 === 0 ? score + ' hits! Keep throwing! ❄️' : 'Splat! ❄️';
}
// How long a kid stays up and how often a new one appears, both shrinking over time.
function stayTime() {
return Math.max(0.6, 1.5 - elapsed * 0.015);
}
function spawnDelay() {
return Math.max(0.35, 1.0 - elapsed * 0.012);
}
function spawn() {
const free = forts.filter(function (f) { return !f.active && f.hitTimer <= 0; });
if (free.length === 0) {
return;
}
const fort = free[Math.floor(Math.random() * free.length)];
fort.penguin = elapsed > 5 && Math.random() < 0.2;
fort.pop.textContent = fort.penguin ? PENGUIN : KIDS[Math.floor(Math.random() * KIDS.length)];
fort.timeLeft = fort.penguin ? stayTime() + 0.4 : stayTime();
fort.active = true;
fort.el.classList.add('up');
}
function loop(time) {
if (!running) {
return;
}
const dt = Math.min(0.05, (time - lastTime) / 1000);
lastTime = time;
elapsed += dt;
spawnTimer -= dt;
if (spawnTimer <= 0) {
spawn();
if (elapsed > 20 && Math.random() < 0.35) {
spawn();
}
spawnTimer = spawnDelay();
}
for (let i = 0; i < forts.length && running; i++) {
const fort = forts[i];
if (fort.hitTimer > 0) {
fort.hitTimer -= dt;
if (fort.hitTimer <= 0) {
fort.el.classList.remove('hit');
hide(fort);
}
continue;
}
if (fort.active) {
fort.timeLeft -= dt;
if (fort.timeLeft <= 0) {
hide(fort);
if (!fort.penguin) {
loseLife(fort, 'Ouch, you got hit by a snowball! ☃️');
}
}
}
}
if (running) {
animationId = requestAnimationFrame(loop);
}
}
function clearForts() {
forts.forEach(function (f) {
f.active = false;
f.hitTimer = 0;
f.el.classList.remove('up', 'hit', 'bad');
});
}
function startGame() {
clearForts();
setScore(0);
lives = MAX_LIVES;
showLives();
elapsed = 0;
spawnTimer = 0.6;
running = true;
startButton.disabled = true;
message.textContent = 'Hit them before they throw! ❄️';
if (shareButtons) {
shareButtons.style.display = 'none';
}
lastTime = performance.now();
animationId = requestAnimationFrame(loop);
}
function endGame() {
running = false;
cancelAnimationFrame(animationId);
clearForts();
if (score > best) {
best = score;
bestDisplay.textContent = best;
try {
localStorage.setItem('snowballFightBest', String(best));
} catch (e) {
// ignore storage errors
}
message.textContent = 'Game over! New best: ' + score + ' hits! 🏆';
} else {
message.textContent = 'Game over! You scored ' + score + ' hits. ⛄';
}
startButton.disabled = false;
startButton.textContent = 'Play again';
if (shareButtons) {
shareButtons.style.display = 'flex';
}
}
lives = MAX_LIVES;
showLives();
startButton.addEventListener('click', startGame);