The eight nights of Hanukkah are coming and the festival words are all mixed up 🕯️! Menorah, dreidel, latke, gelt and their friends are hiding behind scrambled letters, and you have 60 seconds to put them back in order. Tap the letters, or type them on your keyboard, to spell each word; a little hint helps you along, and the words get longer as your score grows. Stuck? Skip a word, but it costs you 5 seconds. The game ends when the timer runs out. How many words can you light up before the candles burn down?
.han-game {
text-align: center;
}
.han-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.han-info {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 10px;
font-size: 1.1em;
}
.han-hint {
min-height: 1.5em;
color: #2f5fb3;
font-style: italic;
margin-bottom: 10px;
}
.han-answer,
.han-tiles {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
min-height: 56px;
margin: 10px auto;
max-width: 520px;
touch-action: manipulation;
}
.han-tile,
.han-slot {
width: 46px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
font-weight: bold;
border-radius: 8px;
user-select: none;
}
.han-tile {
background: #f2b632;
color: #14234a;
border: none;
cursor: pointer;
box-shadow: 0 3px 0 #b07f12;
}
.han-tile:active {
transform: translateY(2px);
box-shadow: 0 1px 0 #b07f12;
}
.han-tile.used {
visibility: hidden;
}
.han-slot {
background: #14234a;
color: #fdf6e3;
border: 2px dashed #2f5fb3;
cursor: pointer;
}
.han-slot.filled {
border-style: solid;
border-color: #f2b632;
}
.han-answer.correct .han-slot {
background: #2e8b57;
border-color: #2e8b57;
}
.han-answer.wrong {
animation: han-shake 0.35s;
}
@keyframes han-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
.han-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.5em;
}
.han-buttons {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 15px;
}
.han-btn {
padding: 10px 20px;
font-size: 1em;
background-color: #f2b632;
color: #14234a;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.han-btn:hover {
background-color: #f6c65a;
}
.han-btn-secondary {
background-color: #2f5fb3;
color: #fff;
}
.han-btn-secondary:hover {
background-color: #4274c9;
}
.han-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
<div class="han-game">
<h1>🕎 Hanukkah Word Scramble ✨</h1>
<div class="instructions">
Unscramble as many Hanukkah words as you can in 60 seconds. Tap the letters (or type them) in the right order. Stuck? Skip a word, but it costs 5 seconds!
</div>
<div class="han-info">
<span>Score: <span id="hanScore">0</span></span>
<span>Time: <span id="hanTime">60</span>s</span>
<span>Best: <span id="hanBest">0</span></span>
</div>
<div class="han-hint" id="hanHint"> </div>
<div class="han-answer" id="hanAnswer"></div>
<div class="han-tiles" id="hanTiles"></div>
<div class="han-message" id="hanMessage">Press "Start" to light the first candle...</div>
<div class="han-buttons">
<button class="han-btn" id="hanStart">Start</button>
<button class="han-btn han-btn-secondary" id="hanSkip" disabled>Skip (-5s)</button>
</div>
</div>
const hanScoreEl = document.getElementById('hanScore');
const hanTimeEl = document.getElementById('hanTime');
const hanBestEl = document.getElementById('hanBest');
const hanHintEl = document.getElementById('hanHint');
const hanAnswerEl = document.getElementById('hanAnswer');
const hanTilesEl = document.getElementById('hanTiles');
const hanMessageEl = document.getElementById('hanMessage');
const hanStartBtn = document.getElementById('hanStart');
const hanSkipBtn = document.getElementById('hanSkip');
const hanHiddenScore = document.getElementById('hiddenScore');
const HAN_GAME_SECONDS = 60;
const HAN_SKIP_PENALTY = 5;
const HAN_WORDS = [
['OIL', 'It burned for eight days 🫒'],
['GELT', 'Chocolate coins 🪙'],
['SPIN', 'What you do with a dreidel'],
['GLOW', 'Candles do it all night'],
['FLAME', 'The top of a candle 🔥'],
['LATKE', 'A fried potato pancake 🥔'],
['EIGHT', 'How many nights Hanukkah lasts'],
['LIGHT', 'Hanukkah is the festival of...'],
['CANDLE', 'One more is lit each night 🕯️'],
['NIGHTS', 'Hanukkah lasts eight of them 🌙'],
['TEMPLE', 'It was rededicated in Jerusalem'],
['DREIDEL', 'A spinning top with four letters'],
['MENORAH', 'The candle holder 🕎'],
['MIRACLE', 'The oil lasted much longer than expected'],
['SHAMASH', 'The helper candle that lights the others'],
['MACCABEE', 'The family that led the revolt'],
['BLESSING', 'Said when lighting the candles'],
['DOUGHNUT', 'Jelly-filled and fried in oil 🍩']
];
let hanScore = 0;
let hanTimeLeft = HAN_GAME_SECONDS;
let hanRunning = false;
let hanWord = '';
let hanTiles = []; // [{letter, used}]
let hanPicked = []; // indexes into hanTiles, in answer order
let hanDeck = [];
let hanLastTick = 0;
let hanFrame = null;
let hanLocked = false;
function hanReadBest() {
try {
return parseInt(localStorage.getItem('hanukkahScrambleBest') || '0', 10) || 0;
} catch (e) {
return 0;
}
}
function hanSaveBest(value) {
try {
localStorage.setItem('hanukkahScrambleBest', String(value));
} catch (e) {
// storage blocked: best score only lasts for this visit
}
}
let hanBest = hanReadBest();
hanBestEl.textContent = hanBest;
function hanShuffle(list) {
const copy = list.slice();
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
}
return copy;
}
function hanScramble(word) {
let letters = word.split('');
for (let attempt = 0; attempt < 20; attempt++) {
letters = hanShuffle(letters);
if (letters.join('') !== word) {
break;
}
}
return letters;
}
// Short words first, longer ones as the score grows.
function hanNextWord() {
if (hanDeck.length === 0) {
hanDeck = hanShuffle(HAN_WORDS);
}
const maxLength = hanScore < 3 ? 5 : (hanScore < 7 ? 6 : 8);
let index = hanDeck.findIndex(function (entry) {
return entry[0].length <= maxLength;
});
if (index === -1) {
index = 0;
}
const entry = hanDeck.splice(index, 1)[0];
hanWord = entry[0];
hanHintEl.textContent = 'Hint: ' + entry[1];
hanTiles = hanScramble(hanWord).map(function (letter) {
return {letter: letter, used: false};
});
hanPicked = [];
hanLocked = false;
hanRender();
}
function hanRender() {
hanAnswerEl.className = 'han-answer';
hanAnswerEl.innerHTML = '';
for (let i = 0; i < hanWord.length; i++) {
const slot = document.createElement('div');
slot.className = 'han-slot';
if (i < hanPicked.length) {
slot.textContent = hanTiles[hanPicked[i]].letter;
slot.classList.add('filled');
slot.addEventListener('click', function () {
hanRemoveFrom(i);
});
}
hanAnswerEl.appendChild(slot);
}
hanTilesEl.innerHTML = '';
hanTiles.forEach(function (tile, index) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'han-tile' + (tile.used ? ' used' : '');
button.textContent = tile.letter;
button.addEventListener('click', function () {
hanPick(index);
});
hanTilesEl.appendChild(button);
});
}
function hanPick(index) {
if (!hanRunning || hanLocked || hanTiles[index].used) {
return;
}
hanTiles[index].used = true;
hanPicked.push(index);
hanRender();
if (hanPicked.length === hanWord.length) {
hanCheck();
}
}
// Tapping a filled slot sends it, and every letter after it, back to the tiles.
function hanRemoveFrom(position) {
if (!hanRunning || hanLocked) {
return;
}
const removed = hanPicked.splice(position);
removed.forEach(function (index) {
hanTiles[index].used = false;
});
hanRender();
}
function hanCheck() {
const guess = hanPicked.map(function (index) {
return hanTiles[index].letter;
}).join('');
hanLocked = true;
if (guess === hanWord) {
hanScore++;
hanScoreEl.textContent = hanScore;
hanHiddenScore.value = hanScore; // Update hidden score field
hanAnswerEl.classList.add('correct');
hanMessageEl.textContent = '✨ ' + hanWord + '! Well done!';
setTimeout(function () {
if (hanRunning) {
hanNextWord();
}
}, 500);
} else {
hanAnswerEl.classList.add('wrong');
hanMessageEl.textContent = 'Not quite... try again!';
setTimeout(function () {
if (!hanRunning) {
return;
}
hanPicked.forEach(function (index) {
hanTiles[index].used = false;
});
hanPicked = [];
hanLocked = false;
hanRender();
}, 450);
}
}
function hanSkip() {
if (!hanRunning || hanLocked) {
return;
}
hanTimeLeft = Math.max(0, hanTimeLeft - HAN_SKIP_PENALTY);
hanMessageEl.textContent = 'The word was ' + hanWord + '. -' + HAN_SKIP_PENALTY + 's';
hanUpdateTime();
if (hanTimeLeft > 0) {
hanNextWord();
}
}
function hanUpdateTime() {
hanTimeEl.textContent = Math.ceil(hanTimeLeft);
}
// Real elapsed time drives the clock, whatever the frame rate.
function hanTick(now) {
if (!hanRunning) {
return;
}
hanTimeLeft -= (now - hanLastTick) / 1000;
hanLastTick = now;
if (hanTimeLeft <= 0) {
hanTimeLeft = 0;
hanUpdateTime();
hanEndGame();
return;
}
hanUpdateTime();
hanFrame = requestAnimationFrame(hanTick);
}
function hanStartGame() {
hanScore = 0;
hanTimeLeft = HAN_GAME_SECONDS;
hanScoreEl.textContent = '0';
hanHiddenScore.value = 0;
hanDeck = hanShuffle(HAN_WORDS);
hanRunning = true;
hanStartBtn.disabled = true;
hanSkipBtn.disabled = false;
hanMessageEl.textContent = 'Go!';
document.querySelector('.share-buttons').style.display = 'none';
hanNextWord();
hanUpdateTime();
hanLastTick = performance.now();
hanFrame = requestAnimationFrame(hanTick);
}
function hanEndGame() {
hanRunning = false;
cancelAnimationFrame(hanFrame);
hanStartBtn.disabled = false;
hanStartBtn.textContent = 'Play again';
hanSkipBtn.disabled = true;
hanTilesEl.innerHTML = '';
hanHintEl.innerHTML = ' ';
hanAnswerEl.innerHTML = '';
let text = '🕯️ Time\'s up! You unscrambled ' + hanScore + ' word' + (hanScore === 1 ? '' : 's') + '.';
if (hanScore > hanBest) {
hanBest = hanScore;
hanSaveBest(hanBest);
hanBestEl.textContent = hanBest;
text += ' 🎉 New best score!';
}
hanMessageEl.textContent = text;
document.querySelector('.share-buttons').style.display = 'flex'; // Show share buttons
}
// Keyboard: a letter picks the first matching unused tile, Backspace removes the last one.
document.addEventListener('keydown', function (event) {
if (!hanRunning || hanLocked) {
return;
}
if (event.key === 'Backspace') {
if (hanPicked.length > 0) {
event.preventDefault();
hanRemoveFrom(hanPicked.length - 1);
}
return;
}
const letter = event.key.length === 1 ? event.key.toUpperCase() : '';
if (!/^[A-Z]$/.test(letter)) {
return;
}
const index = hanTiles.findIndex(function (tile) {
return !tile.used && tile.letter === letter;
});
if (index !== -1) {
event.preventDefault();
hanPick(index);
}
});
hanStartBtn.addEventListener('click', hanStartGame);
hanSkipBtn.addEventListener('click', hanSkip);