🎄 Advent Calendar Dash 🔔

Open the doors in order, from 1 to 24, before the time runs out. A wrong door costs you two seconds!
Doors: 0  |  Time: 40s  |  Best: 0
Next door: 1
Press "Start" to open the calendar!

December is coming and the Advent calendar is ready, but the doors are all mixed up 🎄. Tap or click the doors in order, from 1 to 24, to find the treat behind each one. Every door you open scores a point. Pick the wrong door and you lose two seconds, and when the clock reaches zero the game is over. Finish a whole calendar and you win bonus time and a freshly shuffled one, with a little less bonus each time.

How many doors can you open before the countdown ends?

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

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

.adv-info {
    margin-bottom: 6px;
    font-size: 1.1em;
}

.adv-next {
    margin-bottom: 10px;
    font-size: 1.1em;
}

.adv-next strong {
    display: inline-block;
    min-width: 1.8em;
    padding: 0 6px;
    border-radius: 6px;
    background: #c62828;
    color: #fff;
}

.adv-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
    width: 100%;
    max-width: 320px;
    margin: 0 auto;
    padding: 8px;
    box-sizing: border-box;
    border: 3px solid #1b5e20;
    border-radius: 12px;
    background: #2e7d32;
    touch-action: manipulation;
    user-select: none;
    -webkit-user-select: none;
}

.adv-door {
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px dashed #f5e6c8;
    border-radius: 8px;
    background: #c62828;
    color: #fff;
    font-size: 1.15em;
    font-weight: bold;
    cursor: pointer;
    padding: 0;
    -webkit-tap-highlight-color: transparent;
}

.adv-door.open {
    background: #f5e6c8;
    border-style: solid;
    cursor: default;
    font-size: 1.3em;
}

.adv-door.wrong {
    background: #4a148c;
}

.adv-door:disabled {
    cursor: default;
}

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

.adv-game .start-btn {
    margin-top: 15px;
    padding: 10px 20px;
    font-size: 1em;
    background-color: #c62828;
    color: #fff;
    font-weight: bold;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.adv-game .start-btn:hover {
    background-color: #e53935;
}

.adv-game .start-btn:disabled {
    background-color: #ccc;
    cursor: default;
}
HTML
<div class="adv-game">
    <h1>🎄 Advent Calendar Dash 🔔</h1>
    <div class="instructions">
        Open the doors in order, from 1 to 24, before the time runs out. A wrong door costs you two seconds!
    </div>
    <div class="adv-info">Doors: <span id="advScore">0</span> &nbsp;|&nbsp; Time: <span id="advTime">40</span>s &nbsp;|&nbsp; Best: <span id="advBest">0</span></div>
    <div class="adv-next">Next door: <strong id="advNext">1</strong></div>
    <div class="adv-grid" id="advGrid"></div>
    <div class="adv-message" id="advMessage">Press "Start" to open the calendar!</div>
    <button class="start-btn" id="advStart">Start</button>
</div>
JS
const grid = document.getElementById('advGrid');
const scoreDisplay = document.getElementById('advScore');
const timeDisplay = document.getElementById('advTime');
const bestDisplay = document.getElementById('advBest');
const nextDisplay = document.getElementById('advNext');
const message = document.getElementById('advMessage');
const startButton = document.getElementById('advStart');

const DOORS = 24;
const START_TIME = 40;
const PENALTY = 2;
const TREATS = ['🍬', '🍫', '⭐', '🎁', '🔔', '🕯️', '🍪', '❄️', '🎅', '🦌', '⛄', '🧦'];

let doors = [];
let score, next, timeLeft, running, lastTime, animationId, calendars, best;

best = 0;
try {
    best = parseInt(localStorage.getItem('adventDashBest'), 10) || 0;
} catch (e) {
    best = 0;
}
bestDisplay.textContent = best;

// The share buttons are rendered after this script, so look them up when needed.
function setShareButtons(display) {
    const shareButtons = document.querySelector('.share-buttons');
    if (shareButtons) {
        shareButtons.style.display = display;
    }
}

function setScore(value) {
    score = value;
    scoreDisplay.textContent = score;
    const hiddenScore = document.getElementById('hiddenScore');
    if (hiddenScore) {
        hiddenScore.value = score;
    }
}

function shuffle(list) {
    for (let i = list.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        const tmp = list[i];
        list[i] = list[j];
        list[j] = tmp;
    }
    return list;
}

function press(door) {
    if (!running || door.open) {
        return;
    }
    if (door.number === next) {
        door.open = true;
        door.el.classList.add('open');
        door.el.textContent = TREATS[Math.floor(Math.random() * TREATS.length)];
        setScore(score + 1);
        next += 1;
        if (next > DOORS) {
            calendars += 1;
            const bonus = Math.max(8, 20 - (calendars - 1) * 4);
            timeLeft += bonus;
            message.textContent = 'Calendar complete! +' + bonus + ' seconds, here is a new one. 🎄';
            buildCalendar();
        } else {
            nextDisplay.textContent = next;
        }
    } else {
        timeLeft -= PENALTY;
        door.el.classList.add('wrong');
        setTimeout(function () {
            door.el.classList.remove('wrong');
        }, 250);
        message.textContent = 'Not yet! Door ' + next + ' comes first. −' + PENALTY + ' s';
    }
}

function buildCalendar() {
    grid.innerHTML = '';
    doors = [];
    next = 1;
    nextDisplay.textContent = next;
    const numbers = [];
    for (let i = 1; i <= DOORS; i++) {
        numbers.push(i);
    }
    shuffle(numbers).forEach(function (number) {
        const el = document.createElement('button');
        el.type = 'button';
        el.className = 'adv-door';
        el.textContent = number;
        el.setAttribute('aria-label', 'Door ' + number);
        const door = { el: el, number: number, open: false };
        el.addEventListener('touchstart', function (e) {
            e.preventDefault();
            press(door);
        }, { passive: false });
        el.addEventListener('click', function () {
            press(door);
        });
        grid.appendChild(el);
        doors.push(door);
    });
}

function showTime() {
    timeDisplay.textContent = Math.max(0, Math.ceil(timeLeft));
}

function loop(time) {
    if (!running) {
        return;
    }
    const dt = Math.min(0.1, (time - lastTime) / 1000);
    lastTime = time;
    timeLeft -= dt;
    showTime();
    if (timeLeft <= 0) {
        endGame();
        return;
    }
    animationId = requestAnimationFrame(loop);
}

function startGame() {
    setScore(0);
    calendars = 0;
    timeLeft = START_TIME;
    buildCalendar();
    showTime();
    running = true;
    startButton.disabled = true;
    setShareButtons('none');
    message.textContent = 'Find door 1! 🔔';
    lastTime = performance.now();
    animationId = requestAnimationFrame(loop);
}

function endGame() {
    running = false;
    cancelAnimationFrame(animationId);
    timeLeft = 0;
    showTime();
    doors.forEach(function (door) {
        door.el.disabled = true;
    });
    if (score > best) {
        best = score;
        bestDisplay.textContent = best;
        try {
            localStorage.setItem('adventDashBest', String(best));
        } catch (e) {
            // ignore storage errors
        }
        message.textContent = 'Time is up! New best: ' + score + ' doors! 🏆';
    } else {
        message.textContent = 'Time is up! You opened ' + score + ' doors. 🎄';
    }
    startButton.disabled = false;
    startButton.textContent = 'Play again';
    setShareButtons('flex');
}

buildCalendar();
doors.forEach(function (door) {
    door.el.disabled = true;
});
startButton.addEventListener('click', startGame);