🥔 Latke Flip - Hanukkah Game 🕎

Three pans, three sizzling latkes! Tap a pan (or press 1, 2 or 3) when its latke turns golden to flip it. Too early or burnt costs you a candle!
Score: 0 Candles: 🕯️🕯️🕯️ Best: 0
Press "Start", then flip each latke when it turns golden...

The menorah is lit and the oil is sizzling 🕎! Three pans of latkes are frying at once, and each one turns from pale potato to crispy gold, then to burnt in a few seconds. Tap a pan, or press 1, 2 or 3 on your keyboard, when its latke glows golden to flip it and score a point. Flip too early and it is still raw; wait too long and it burns: either way you lose one of your three candles. The latkes cook faster as your score grows, and the game ends when the last candle goes out. How many crispy latkes can you flip for the Festival of Lights?

Here is the code
CSS
.ltk-game {
    text-align: center;
}

.ltk-game .instructions {
    max-width: 500px;
    margin: 0 auto 15px;
    font-size: 1em;
    color: #555;
}

.ltk-info {
    display: flex;
    justify-content: center;
    gap: 20px;
    flex-wrap: wrap;
    margin-bottom: 12px;
    font-size: 1.1em;
}

.ltk-stove {
    display: flex;
    justify-content: center;
    gap: 12px;
    max-width: 100%;
    width: 480px;
    margin: 0 auto;
    padding: 18px 12px;
    background: #1f3a73;
    border-radius: 16px;
    border: 3px solid #d9e2f5;
    box-sizing: border-box;
    touch-action: manipulation;
}

.ltk-pan {
    position: relative;
    flex: 1 1 0;
    max-width: 140px;
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 45%, #555 0%, #2b2b2b 70%, #111 100%);
    border: 4px solid #8a8a8a;
    cursor: pointer;
    padding: 0;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

.ltk-pan:disabled {
    cursor: default;
}

.ltk-latke {
    position: absolute;
    left: 20%;
    top: 20%;
    width: 60%;
    height: 60%;
    border-radius: 48% 52% 46% 54%;
    background: #f1e2b0;
    box-shadow: inset 0 -4px 0 rgba(0, 0, 0, 0.15);
    transition: transform 0.25s;
}

.ltk-latke.empty {
    visibility: hidden;
}

.ltk-latke.golden {
    box-shadow: 0 0 14px 4px #ffd34d, inset 0 -4px 0 rgba(0, 0, 0, 0.15);
}

.ltk-latke.flip {
    transform: rotateX(180deg) scale(1.1);
}

.ltk-pan.miss {
    border-color: #ff3b3b;
    animation: ltk-shake 0.35s;
}

.ltk-pan.hit {
    border-color: #ffd34d;
}

@keyframes ltk-shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-6px); }
    75% { transform: translateX(6px); }
}

.ltk-key {
    position: absolute;
    bottom: -4px;
    right: -4px;
    width: 24px;
    height: 24px;
    line-height: 24px;
    border-radius: 50%;
    background: #d9e2f5;
    color: #1f3a73;
    font-size: 0.8em;
    font-weight: bold;
}

.ltk-message {
    margin-top: 15px;
    font-weight: bold;
    min-height: 1.5em;
}

.ltk-btn {
    margin-top: 12px;
    padding: 10px 24px;
    font-size: 1em;
    background-color: #1f3a73;
    color: #ffd34d;
    font-weight: bold;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.ltk-btn:hover {
    background-color: #2d4f99;
}

.ltk-btn:disabled {
    background-color: #ccc;
    color: #666;
    cursor: not-allowed;
}
HTML
<div class="ltk-game">
    <h1>🥔 Latke Flip - Hanukkah Game 🕎</h1>
    <div class="instructions">
        Three pans, three sizzling latkes! Tap a pan (or press 1, 2 or 3) when its latke turns golden to flip it. Too early or burnt costs you a candle!
    </div>
    <div class="ltk-info">
        <span>Score: <span id="ltkScore">0</span></span>
        <span>Candles: <span id="ltkLives">🕯️🕯️🕯️</span></span>
        <span>Best: <span id="ltkBest">0</span></span>
    </div>
    <div class="ltk-stove" id="ltkStove">
        <button class="ltk-pan" data-index="0" aria-label="Pan 1"><span class="ltk-latke"></span><span class="ltk-key">1</span></button>
        <button class="ltk-pan" data-index="1" aria-label="Pan 2"><span class="ltk-latke"></span><span class="ltk-key">2</span></button>
        <button class="ltk-pan" data-index="2" aria-label="Pan 3"><span class="ltk-latke"></span><span class="ltk-key">3</span></button>
    </div>
    <div class="ltk-message" id="ltkMessage">Press "Start", then flip each latke when it turns golden...</div>
    <button class="ltk-btn" id="ltkStart">Start</button>
</div>
JS
const ltkScoreEl = document.getElementById('ltkScore');
const ltkLivesEl = document.getElementById('ltkLives');
const ltkBestEl = document.getElementById('ltkBest');
const ltkMessageEl = document.getElementById('ltkMessage');
const ltkStartBtn = document.getElementById('ltkStart');
const ltkPanEls = Array.prototype.slice.call(document.querySelectorAll('.ltk-pan'));
const ltkHiddenScore = document.getElementById('hiddenScore');

const LTK_LIVES = 3;
const LTK_GOLDEN_START = 0.62;
const LTK_GOLDEN_END = 0.9;
// Colours from raw batter to golden to burnt.
const LTK_RAW = [241, 226, 176];
const LTK_GOLD = [224, 154, 40];
const LTK_BURNT = [60, 34, 14];

let ltkScore = 0;
let ltkLives = LTK_LIVES;
let ltkRunning = false;
let ltkLastTime = 0;
let ltkFrame = null;
let ltkPans = [];

function ltkReadBest() {
    try {
        return parseInt(localStorage.getItem('latkeFlipBest') || '0', 10) || 0;
    } catch (e) {
        return 0;
    }
}

function ltkSaveBest(value) {
    try {
        localStorage.setItem('latkeFlipBest', String(value));
    } catch (e) {
        // storage blocked: best score only lasts for this visit
    }
}

let ltkBest = ltkReadBest();
ltkBestEl.textContent = ltkBest;

function ltkMix(a, b, t) {
    return [0, 1, 2].map(function (i) {
        return Math.round(a[i] + (b[i] - a[i]) * t);
    });
}

function ltkColour(level) {
    let rgb;
    if (level < LTK_GOLDEN_START) {
        rgb = ltkMix(LTK_RAW, LTK_GOLD, level / LTK_GOLDEN_START);
    } else if (level < LTK_GOLDEN_END) {
        rgb = LTK_GOLD;
    } else {
        rgb = ltkMix(LTK_GOLD, LTK_BURNT, Math.min(1, (level - LTK_GOLDEN_END) / (1 - LTK_GOLDEN_END)));
    }
    return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')';
}

// Seconds a latke takes to go from raw to burnt: shorter as the score grows.
function ltkCookTime() {
    const base = Math.max(2.2, 5.5 - ltkScore * 0.12);
    return base * (0.8 + Math.random() * 0.5);
}

function ltkNewLatke(pan, delay) {
    pan.level = 0;
    pan.speed = 1 / ltkCookTime();
    pan.wait = delay;
}

function ltkRenderLives() {
    let text = '';
    for (let i = 0; i < LTK_LIVES; i++) {
        text += i < ltkLives ? '🕯️' : '▫️';
    }
    ltkLivesEl.textContent = text;
}

function ltkRender() {
    ltkPans.forEach(function (pan, i) {
        const latke = ltkPanEls[i].querySelector('.ltk-latke');
        if (pan.wait > 0) {
            latke.classList.add('empty');
            latke.classList.remove('golden');
            return;
        }
        latke.classList.remove('empty');
        latke.style.background = ltkColour(pan.level);
        if (pan.level >= LTK_GOLDEN_START && pan.level < LTK_GOLDEN_END) {
            latke.classList.add('golden');
        } else {
            latke.classList.remove('golden');
        }
    });
}

function ltkFlash(i, cls) {
    const el = ltkPanEls[i];
    el.classList.remove('hit', 'miss');
    void el.offsetWidth;
    el.classList.add(cls);
    setTimeout(function () {
        el.classList.remove(cls);
    }, 350);
}

function ltkLoseLife(i, text) {
    ltkLives -= 1;
    ltkRenderLives();
    ltkFlash(i, 'miss');
    ltkMessageEl.textContent = text;
    ltkNewLatke(ltkPans[i], 0.8);
    if (ltkLives <= 0) {
        ltkEnd();
    }
}

function ltkTap(i) {
    if (!ltkRunning) {
        return;
    }
    const pan = ltkPans[i];
    if (pan.wait > 0) {
        return;
    }
    if (pan.level < LTK_GOLDEN_START) {
        ltkLoseLife(i, 'Too early! That latke was still raw 🥔');
        return;
    }
    // A tap on a golden latke flips it; burnt ones are handled in the loop.
    ltkScore += 1;
    ltkScoreEl.textContent = ltkScore;
    if (ltkHiddenScore) {
        ltkHiddenScore.value = ltkScore;
    }
    ltkFlash(i, 'hit');
    const latke = ltkPanEls[i].querySelector('.ltk-latke');
    latke.classList.add('flip');
    setTimeout(function () {
        latke.classList.remove('flip');
    }, 250);
    ltkMessageEl.textContent = ltkScore % 8 === 0 ? 'Eight crispy latkes! Chag sameach! 🕎' : 'Golden and crispy! ✨';
    ltkNewLatke(pan, 0.3 + Math.random() * 0.7);
}

function ltkLoop(now) {
    if (!ltkRunning) {
        return;
    }
    const dt = Math.min(0.1, (now - ltkLastTime) / 1000);
    ltkLastTime = now;
    for (let i = 0; i < ltkPans.length && ltkRunning; i++) {
        const pan = ltkPans[i];
        if (pan.wait > 0) {
            pan.wait -= dt;
            continue;
        }
        pan.level += pan.speed * dt;
        if (pan.level >= 1) {
            ltkLoseLife(i, 'Oy! A latke burnt 🔥');
        }
    }
    ltkRender();
    if (ltkRunning) {
        ltkFrame = requestAnimationFrame(ltkLoop);
    }
}

function ltkStart() {
    ltkScore = 0;
    ltkLives = LTK_LIVES;
    ltkScoreEl.textContent = '0';
    ltkRenderLives();
    if (ltkHiddenScore) {
        ltkHiddenScore.value = 0;
    }
    const share = document.querySelector('.share-buttons');
    if (share) {
        share.style.display = 'none';
    }
    ltkPans = [];
    for (let i = 0; i < 3; i++) {
        const pan = {};
        ltkNewLatke(pan, 0.4 + i * 1.1);
        ltkPans.push(pan);
    }
    ltkPanEls.forEach(function (el) {
        el.disabled = false;
    });
    ltkStartBtn.disabled = true;
    ltkMessageEl.textContent = 'The oil is hot... watch for golden latkes!';
    ltkRunning = true;
    ltkLastTime = performance.now();
    ltkRender();
    ltkFrame = requestAnimationFrame(ltkLoop);
}

function ltkEnd() {
    ltkRunning = false;
    if (ltkFrame) {
        cancelAnimationFrame(ltkFrame);
    }
    let text = 'The candles are out! You flipped ' + ltkScore + ' latke' + (ltkScore === 1 ? '' : 's') + '.';
    if (ltkScore > ltkBest) {
        ltkBest = ltkScore;
        ltkBestEl.textContent = ltkBest;
        ltkSaveBest(ltkBest);
        text += ' New best score! 🎉';
    }
    ltkMessageEl.textContent = text;
    ltkStartBtn.disabled = false;
    ltkStartBtn.textContent = 'Play again';
    ltkPanEls.forEach(function (el) {
        el.disabled = true;
    });
    const share = document.querySelector('.share-buttons');
    if (share) {
        share.style.display = 'flex';
    }
}

ltkPanEls.forEach(function (el, i) {
    el.disabled = true;
    el.addEventListener('touchstart', function (e) {
        e.preventDefault();
        ltkTap(i);
    }, {passive: false});
    el.addEventListener('mousedown', function (e) {
        e.preventDefault();
        ltkTap(i);
    });
});

document.addEventListener('keydown', function (e) {
    if (!ltkRunning) {
        return;
    }
    const index = ['1', '2', '3'].indexOf(e.key);
    if (index !== -1) {
        e.preventDefault();
        ltkTap(index);
    }
});

ltkStartBtn.addEventListener('click', ltkStart);