The Christmas tree is ready, but the lights have a mind of their own 🎄! Each round, the red, green, gold and blue bulbs blink in a pattern: watch closely, then repeat it in the same order by tapping the bulbs, or pressing 1, 2, 3 and 4 on your keyboard. Every round adds one more light, and the pattern plays a little faster as it grows. Get one bulb wrong and the whole tree goes dark. Your score is the longest pattern you remembered. Can you keep the tree shining longer than last time?
.xl-game {
text-align: center;
}
.xl-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.xl-info {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 1.1em;
}
.xl-tree {
position: relative;
max-width: 380px;
margin: 0 auto;
padding: 18px 16px 26px;
background: linear-gradient(180deg, #0b2a1b 0%, #134e32 100%);
border-radius: 16px;
box-shadow: 0 6px 0 #071a10;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
}
.xl-tree.xl-dark {
animation: xl-shake 0.4s;
filter: grayscale(0.7) brightness(0.6);
}
@keyframes xl-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
.xl-star {
font-size: 2.4em;
line-height: 1;
transition: transform 0.2s, filter 0.2s;
}
.xl-star.xl-shine {
transform: scale(1.25) rotate(12deg);
filter: drop-shadow(0 0 12px #ffe066);
}
.xl-wire {
height: 4px;
margin: 14px 8% -6px;
border-radius: 4px;
background: #2d2d2d;
}
.xl-bulbs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.xl-bulb {
position: relative;
aspect-ratio: 3 / 4;
width: 100%;
max-width: 78px;
margin: 0 auto;
border: none;
border-radius: 50% 50% 46% 46% / 40% 40% 60% 60%;
cursor: pointer;
opacity: 0.35;
transition: opacity 0.12s, transform 0.12s, box-shadow 0.12s;
touch-action: manipulation;
}
.xl-bulb::before {
content: "";
position: absolute;
top: -10px;
left: 50%;
width: 36%;
height: 12px;
transform: translateX(-50%);
border-radius: 3px;
background: #8a8a8a;
}
.xl-bulb span {
position: absolute;
bottom: 8px;
left: 0;
right: 0;
font-size: 0.85em;
font-weight: bold;
color: rgba(255, 255, 255, 0.85);
}
.xl-bulb:disabled {
cursor: default;
}
.xl-red { background: #ff3b3b; }
.xl-green { background: #2ecc71; }
.xl-gold { background: #ffcc00; }
.xl-blue { background: #3fa9ff; }
.xl-bulb.xl-on {
opacity: 1;
transform: scale(1.08);
}
.xl-red.xl-on { box-shadow: 0 0 22px 6px #ff3b3b; }
.xl-green.xl-on { box-shadow: 0 0 22px 6px #2ecc71; }
.xl-gold.xl-on { box-shadow: 0 0 22px 6px #ffcc00; }
.xl-blue.xl-on { box-shadow: 0 0 22px 6px #3fa9ff; }
.xl-message {
min-height: 1.5em;
margin: 14px 0 10px;
font-size: 1.1em;
font-weight: bold;
}
.xl-btn {
padding: 10px 28px;
font-size: 1.1em;
font-weight: bold;
color: #ffffff;
background: #c0392b;
border: none;
border-radius: 10px;
box-shadow: 0 4px 0 #7b241c;
cursor: pointer;
touch-action: manipulation;
}
.xl-btn:disabled {
opacity: 0.5;
cursor: default;
}
@media (max-width: 420px) {
.xl-bulbs {
gap: 8px;
}
}
<div class="xl-game">
<h1>🎄 Christmas Lights Memory 🌟</h1>
<div class="instructions">
Watch the Christmas lights blink, then repeat the pattern in the same order. Tap the bulbs, or press 1, 2, 3 and 4. Each round adds one more light. One wrong bulb and the tree goes dark!
</div>
<div class="xl-info">
<span>Score: <span id="xlScore">0</span></span>
<span>Best: <span id="xlBest">0</span></span>
</div>
<div class="xl-tree" id="xlTree">
<div class="xl-star" id="xlStar">⭐</div>
<div class="xl-wire"></div>
<div class="xl-bulbs">
<button class="xl-bulb xl-red" data-index="0" aria-label="Red light (1)" disabled><span>1</span></button>
<button class="xl-bulb xl-green" data-index="1" aria-label="Green light (2)" disabled><span>2</span></button>
<button class="xl-bulb xl-gold" data-index="2" aria-label="Gold light (3)" disabled><span>3</span></button>
<button class="xl-bulb xl-blue" data-index="3" aria-label="Blue light (4)" disabled><span>4</span></button>
</div>
</div>
<div class="xl-message" id="xlMessage">Press "Start" to switch on the lights...</div>
<button class="xl-btn" id="xlStart">Start</button>
</div>
const xlScoreEl = document.getElementById('xlScore');
const xlBestEl = document.getElementById('xlBest');
const xlTreeEl = document.getElementById('xlTree');
const xlStarEl = document.getElementById('xlStar');
const xlMessageEl = document.getElementById('xlMessage');
const xlStartBtn = document.getElementById('xlStart');
const xlBulbs = Array.prototype.slice.call(document.querySelectorAll('.xl-bulb'));
const xlHiddenScore = document.getElementById('hiddenScore');
// One jingle note per bulb (C5, E5, G5, C6).
const XL_NOTES = [523.25, 659.25, 783.99, 1046.5];
let xlSequence = [];
let xlStep = 0;
let xlScore = 0;
let xlAccepting = false;
let xlRunning = false;
let xlTimers = [];
let xlAudio = null;
function xlReadBest() {
try {
return parseInt(localStorage.getItem('christmasLightsMemoryBest') || '0', 10) || 0;
} catch (e) {
return 0;
}
}
function xlSaveBest(value) {
try {
localStorage.setItem('christmasLightsMemoryBest', String(value));
} catch (e) {
// storage blocked: best score only lasts for this visit
}
}
let xlBest = xlReadBest();
xlBestEl.textContent = xlBest;
function xlBeep(index, duration) {
try {
if (!xlAudio) {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) {
return;
}
xlAudio = new Ctx();
}
const osc = xlAudio.createOscillator();
const gain = xlAudio.createGain();
osc.type = 'sine';
osc.frequency.value = index < 0 ? 140 : XL_NOTES[index];
gain.gain.setValueAtTime(0.15, xlAudio.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, xlAudio.currentTime + duration / 1000);
osc.connect(gain);
gain.connect(xlAudio.destination);
osc.start();
osc.stop(xlAudio.currentTime + duration / 1000);
} catch (e) {
// no sound available: the game works silently
}
}
function xlLater(fn, delay) {
xlTimers.push(setTimeout(fn, delay));
}
function xlClearTimers() {
xlTimers.forEach(function (timer) { clearTimeout(timer); });
xlTimers = [];
}
function xlFlash(index, duration) {
const bulb = xlBulbs[index];
bulb.classList.add('xl-on');
xlBeep(index, duration);
xlLater(function () {
bulb.classList.remove('xl-on');
}, duration);
}
// The pattern speeds up as it grows: 560 ms per light at first, down to 260 ms.
function xlSpeed() {
return Math.max(260, 560 - xlSequence.length * 22);
}
function xlSetBulbs(enabled) {
xlBulbs.forEach(function (bulb) { bulb.disabled = !enabled; });
}
function xlPlaySequence() {
xlAccepting = false;
xlSetBulbs(false);
xlMessageEl.textContent = 'Watch the lights... 👀';
const speed = xlSpeed();
xlSequence.forEach(function (index, i) {
xlLater(function () {
xlFlash(index, speed * 0.7);
}, 500 + i * speed);
});
xlLater(function () {
xlStep = 0;
xlAccepting = true;
xlSetBulbs(true);
xlMessageEl.textContent = 'Your turn! ' + xlSequence.length + (xlSequence.length === 1 ? ' light' : ' lights') + ' to repeat';
}, 500 + xlSequence.length * speed);
}
function xlNextRound() {
xlSequence.push(Math.floor(Math.random() * 4));
xlPlaySequence();
}
function xlPress(index) {
if (!xlRunning || !xlAccepting) {
return;
}
if (index !== xlSequence[xlStep]) {
xlEnd();
return;
}
xlFlash(index, 220);
xlStep++;
if (xlStep === xlSequence.length) {
xlAccepting = false;
xlSetBulbs(false);
xlScore = xlSequence.length;
xlScoreEl.textContent = xlScore;
xlHiddenScore.value = xlScore;
xlStarEl.classList.add('xl-shine');
xlMessageEl.textContent = ['Merry and bright! ✨', 'Ho ho ho! 🎅', 'Beautiful lights! 🎄', 'Jingle all the way! 🔔'][Math.floor(Math.random() * 4)];
xlLater(function () {
xlStarEl.classList.remove('xl-shine');
xlNextRound();
}, 800);
}
}
function xlStart() {
xlClearTimers();
xlSequence = [];
xlScore = 0;
xlRunning = true;
xlScoreEl.textContent = '0';
xlHiddenScore.value = 0;
xlTreeEl.classList.remove('xl-dark');
document.querySelector('.share-buttons').style.display = 'none';
xlStartBtn.disabled = true;
xlNextRound();
}
function xlEnd() {
xlRunning = false;
xlAccepting = false;
xlClearTimers();
xlSetBulbs(false);
xlBulbs.forEach(function (bulb) { bulb.classList.remove('xl-on'); });
xlBeep(-1, 450);
xlTreeEl.classList.add('xl-dark');
const correct = xlSequence[xlStep];
let text = 'Oh no, the tree went dark! The next light was ' + ['red', 'green', 'gold', 'blue'][correct] + '. You remembered ' + xlScore + (xlScore === 1 ? ' light.' : ' lights.');
if (xlScore > xlBest) {
xlBest = xlScore;
xlBestEl.textContent = xlBest;
xlSaveBest(xlBest);
text += ' New best! 🏆';
}
xlMessageEl.textContent = text;
xlStartBtn.textContent = 'Play again';
xlStartBtn.disabled = false;
document.querySelector('.share-buttons').style.display = 'flex';
}
xlBulbs.forEach(function (bulb, index) {
bulb.addEventListener('touchstart', function (event) {
event.preventDefault();
xlPress(index);
}, {passive: false});
bulb.addEventListener('click', function () {
xlPress(index);
});
});
document.addEventListener('keydown', function (event) {
if (!xlRunning) {
return;
}
if (event.key === '1' || event.key === '2' || event.key === '3' || event.key === '4') {
event.preventDefault();
xlPress(parseInt(event.key, 10) - 1);
}
});
xlStartBtn.addEventListener('click', xlStart);