๐ŸŽƒ Spooky Scramble ๐Ÿ‘ป

Unscramble as many Halloween 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!
Score: 0 Time: 60s Best: 0
 
Press "Start" to begin the spell...

The witches have mixed up all the Halloween words ๐Ÿง™! Pumpkins, ghosts, vampires and cauldrons 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 on a word? Skip it, but it costs you 5 precious seconds. How many spooky words can you unscramble before the clock strikes midnight?

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

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

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

.scr-hint {
    min-height: 1.5em;
    color: #7a4bb5;
    font-style: italic;
    margin-bottom: 10px;
}

.scr-answer,
.scr-tiles {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 56px;
    margin: 10px auto;
    max-width: 520px;
    touch-action: manipulation;
}

.scr-tile,
.scr-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;
}

.scr-tile {
    background: #ff7518;
    color: #1a1025;
    border: none;
    cursor: pointer;
    box-shadow: 0 3px 0 #b8510d;
}

.scr-tile:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 #b8510d;
}

.scr-tile.used {
    visibility: hidden;
}

.scr-slot {
    background: #1a1025;
    color: #f5efe6;
    border: 2px dashed #7a4bb5;
    cursor: pointer;
}

.scr-slot.filled {
    border-style: solid;
    border-color: #ff7518;
}

.scr-answer.correct .scr-slot {
    background: #2e8b57;
    border-color: #2e8b57;
}

.scr-answer.wrong {
    animation: scr-shake 0.35s;
}

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

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

.scr-buttons {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 15px;
}

.scr-btn {
    padding: 10px 20px;
    font-size: 1em;
    background-color: #ff7518;
    color: #1a1025;
    font-weight: bold;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.scr-btn:hover {
    background-color: #ff8f45;
}

.scr-btn-secondary {
    background-color: #7a4bb5;
    color: #fff;
}

.scr-btn-secondary:hover {
    background-color: #8d60c9;
}

.scr-btn:disabled {
    background-color: #ccc;
    color: #666;
    cursor: not-allowed;
}
HTML
<div class="scr-game">
    <h1>๐ŸŽƒ Spooky Scramble ๐Ÿ‘ป</h1>
    <div class="instructions">
        Unscramble as many Halloween 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="scr-info">
        <span>Score: <span id="scrScore">0</span></span>
        <span>Time: <span id="scrTime">60</span>s</span>
        <span>Best: <span id="scrBest">0</span></span>
    </div>
    <div class="scr-hint" id="scrHint">&nbsp;</div>
    <div class="scr-answer" id="scrAnswer"></div>
    <div class="scr-tiles" id="scrTiles"></div>
    <div class="scr-message" id="scrMessage">Press "Start" to begin the spell...</div>
    <div class="scr-buttons">
        <button class="scr-btn" id="scrStart">Start</button>
        <button class="scr-btn scr-btn-secondary" id="scrSkip" disabled>Skip (-5s)</button>
    </div>
</div>
JS
const scrScoreEl = document.getElementById('scrScore');
const scrTimeEl = document.getElementById('scrTime');
const scrBestEl = document.getElementById('scrBest');
const scrHintEl = document.getElementById('scrHint');
const scrAnswerEl = document.getElementById('scrAnswer');
const scrTilesEl = document.getElementById('scrTiles');
const scrMessageEl = document.getElementById('scrMessage');
const scrStartBtn = document.getElementById('scrStart');
const scrSkipBtn = document.getElementById('scrSkip');
const scrHiddenScore = document.getElementById('hiddenScore');

const SCR_GAME_SECONDS = 60;
const SCR_SKIP_PENALTY = 5;
const SCR_WORDS = [
    ['BAT', 'It flies at night ๐Ÿฆ‡'],
    ['OWL', 'A night bird that hoots ๐Ÿฆ‰'],
    ['WITCH', 'She rides a broom ๐Ÿงน'],
    ['GHOST', 'It says "Boo!" ๐Ÿ‘ป'],
    ['CANDY', 'Trick or treat! ๐Ÿฌ'],
    ['MUMMY', 'Wrapped in bandages'],
    ['BROOM', 'A witch\'s ride'],
    ['SPELL', 'Magic words โœจ'],
    ['GRAVE', 'Found in a cemetery ๐Ÿชฆ'],
    ['SPIDER', 'Eight legs and a web ๐Ÿ•ท๏ธ'],
    ['ZOMBIE', 'Walks slowly, wants brains ๐ŸงŸ'],
    ['POTION', 'Brewed in a cauldron ๐Ÿงช'],
    ['COFFIN', 'A vampire\'s bed โšฐ๏ธ'],
    ['GOBLIN', 'A small, mischievous creature'],
    ['CASTLE', 'Dracula lives in one ๐Ÿฐ'],
    ['PUMPKIN', 'Carve it into a lantern ๐ŸŽƒ'],
    ['VAMPIRE', 'Beware of its fangs ๐Ÿง›'],
    ['MONSTER', 'Hiding under the bed ๐Ÿ‘น'],
    ['HAUNTED', 'A house full of ghosts'],
    ['LANTERN', 'A light in the dark ๐Ÿฎ'],
    ['SKELETON', 'All bones, no skin ๐Ÿ’€'],
    ['CAULDRON', 'A big pot for potions'],
    ['WEREWOLF', 'Howls at the full moon ๐Ÿบ'],
    ['COSTUME', 'What you wear on October 31st']
];

let scrScore = 0;
let scrTimeLeft = SCR_GAME_SECONDS;
let scrRunning = false;
let scrWord = '';
let scrTiles = [];   // [{letter, used}]
let scrPicked = [];  // indexes into scrTiles, in answer order
let scrDeck = [];
let scrLastTick = 0;
let scrFrame = null;
let scrLocked = false;

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

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

let scrBest = scrReadBest();
scrBestEl.textContent = scrBest;

function scrShuffle(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 scrScramble(word) {
    let letters = word.split('');
    for (let attempt = 0; attempt < 20; attempt++) {
        letters = scrShuffle(letters);
        if (letters.join('') !== word) {
            break;
        }
    }
    return letters;
}

// Short words first, longer ones as the score grows.
function scrNextWord() {
    if (scrDeck.length === 0) {
        scrDeck = scrShuffle(SCR_WORDS);
    }
    const maxLength = scrScore < 3 ? 5 : (scrScore < 7 ? 6 : 8);
    let index = scrDeck.findIndex(function (entry) {
        return entry[0].length <= maxLength;
    });
    if (index === -1) {
        index = 0;
    }
    const entry = scrDeck.splice(index, 1)[0];
    scrWord = entry[0];
    scrHintEl.textContent = 'Hint: ' + entry[1];
    scrTiles = scrScramble(scrWord).map(function (letter) {
        return {letter: letter, used: false};
    });
    scrPicked = [];
    scrLocked = false;
    scrRender();
}

function scrRender() {
    scrAnswerEl.className = 'scr-answer';
    scrAnswerEl.innerHTML = '';
    for (let i = 0; i < scrWord.length; i++) {
        const slot = document.createElement('div');
        slot.className = 'scr-slot';
        if (i < scrPicked.length) {
            slot.textContent = scrTiles[scrPicked[i]].letter;
            slot.classList.add('filled');
            slot.addEventListener('click', function () {
                scrRemoveFrom(i);
            });
        }
        scrAnswerEl.appendChild(slot);
    }

    scrTilesEl.innerHTML = '';
    scrTiles.forEach(function (tile, index) {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'scr-tile' + (tile.used ? ' used' : '');
        button.textContent = tile.letter;
        button.addEventListener('click', function () {
            scrPick(index);
        });
        scrTilesEl.appendChild(button);
    });
}

function scrPick(index) {
    if (!scrRunning || scrLocked || scrTiles[index].used) {
        return;
    }
    scrTiles[index].used = true;
    scrPicked.push(index);
    scrRender();
    if (scrPicked.length === scrWord.length) {
        scrCheck();
    }
}

// Tapping a filled slot sends it, and every letter after it, back to the tiles.
function scrRemoveFrom(position) {
    if (!scrRunning || scrLocked) {
        return;
    }
    const removed = scrPicked.splice(position);
    removed.forEach(function (index) {
        scrTiles[index].used = false;
    });
    scrRender();
}

function scrCheck() {
    const guess = scrPicked.map(function (index) {
        return scrTiles[index].letter;
    }).join('');

    scrLocked = true;
    if (guess === scrWord) {
        scrScore++;
        scrScoreEl.textContent = scrScore;
        scrHiddenScore.value = scrScore; // Update hidden score field
        scrAnswerEl.classList.add('correct');
        scrMessageEl.textContent = 'โœจ ' + scrWord + '! Well done!';
        setTimeout(function () {
            if (scrRunning) {
                scrNextWord();
            }
        }, 500);
    } else {
        scrAnswerEl.classList.add('wrong');
        scrMessageEl.textContent = 'Not quite... try again!';
        setTimeout(function () {
            if (!scrRunning) {
                return;
            }
            scrPicked.forEach(function (index) {
                scrTiles[index].used = false;
            });
            scrPicked = [];
            scrLocked = false;
            scrRender();
        }, 450);
    }
}

function scrSkip() {
    if (!scrRunning || scrLocked) {
        return;
    }
    scrTimeLeft = Math.max(0, scrTimeLeft - SCR_SKIP_PENALTY);
    scrMessageEl.textContent = 'The word was ' + scrWord + '. -' + SCR_SKIP_PENALTY + 's';
    scrUpdateTime();
    if (scrTimeLeft > 0) {
        scrNextWord();
    }
}

function scrUpdateTime() {
    scrTimeEl.textContent = Math.ceil(scrTimeLeft);
}

// Real elapsed time drives the clock, whatever the frame rate.
function scrTick(now) {
    if (!scrRunning) {
        return;
    }
    scrTimeLeft -= (now - scrLastTick) / 1000;
    scrLastTick = now;
    if (scrTimeLeft <= 0) {
        scrTimeLeft = 0;
        scrUpdateTime();
        scrEndGame();
        return;
    }
    scrUpdateTime();
    scrFrame = requestAnimationFrame(scrTick);
}

function scrStartGame() {
    scrScore = 0;
    scrTimeLeft = SCR_GAME_SECONDS;
    scrScoreEl.textContent = '0';
    scrHiddenScore.value = 0;
    scrDeck = scrShuffle(SCR_WORDS);
    scrRunning = true;
    scrStartBtn.disabled = true;
    scrSkipBtn.disabled = false;
    scrMessageEl.textContent = 'Go!';
    document.querySelector('.share-buttons').style.display = 'none';
    scrNextWord();
    scrUpdateTime();
    scrLastTick = performance.now();
    scrFrame = requestAnimationFrame(scrTick);
}

function scrEndGame() {
    scrRunning = false;
    cancelAnimationFrame(scrFrame);
    scrStartBtn.disabled = false;
    scrStartBtn.textContent = 'Play again';
    scrSkipBtn.disabled = true;
    scrTilesEl.innerHTML = '';
    scrHintEl.innerHTML = '&nbsp;';
    scrAnswerEl.innerHTML = '';

    let text = 'โฐ Time\'s up! You unscrambled ' + scrScore + ' word' + (scrScore === 1 ? '' : 's') + '.';
    if (scrScore > scrBest) {
        scrBest = scrScore;
        scrSaveBest(scrBest);
        scrBestEl.textContent = scrBest;
        text += ' ๐ŸŽ‰ New best score!';
    }
    scrMessageEl.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 (!scrRunning || scrLocked) {
        return;
    }
    if (event.key === 'Backspace') {
        if (scrPicked.length > 0) {
            event.preventDefault();
            scrRemoveFrom(scrPicked.length - 1);
        }
        return;
    }
    const letter = event.key.length === 1 ? event.key.toUpperCase() : '';
    if (!/^[A-Z]$/.test(letter)) {
        return;
    }
    const index = scrTiles.findIndex(function (tile) {
        return !tile.used && tile.letter === letter;
    });
    if (index !== -1) {
        event.preventDefault();
        scrPick(index);
    }
});

scrStartBtn.addEventListener('click', scrStartGame);
scrSkipBtn.addEventListener('click', scrSkip);