❄️ Snowball Fight ⛄

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!
Score: 0  |  Lives: ❤️❤️❤️  |  Best: 0
Press "Start" to begin the snowball fight!

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?

Here is the code
CSS
.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;
}
HTML
<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> &nbsp;|&nbsp; Lives: <span id="snowLives">❤️❤️❤️</span> &nbsp;|&nbsp; 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>
JS
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);