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?
.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;
}
<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> | Time: <span id="advTime">40</span>s | 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>
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);