• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
07Games
└ Rockfall└ Speak & Math└ Space Invaders└ Apache└ Asteroids└ Molly Roger└ Carrom└ Chess└ Connect 4└ Anagrams
08Voronoi
└ 001└ 002└ 003└ 004└ 005
09Terrain
└ Simple Polygon└ Simple Flyover└ Infinite Flyover 01└ Infinite Flyover 02└ Hexagonal Plane└ Mountain Ridges
10Critter Models 01
└ Wireframe Spider└ Wireframe Fly└ Wireframe Scorpion└ Wireframe Mosquito└ Wireframe Bee└ Wireframe Butterfly└ Wireframe Locust
11Chromatophores
└ 01└ 02└ 03└ 04
12Conformal Maps
└ 01└ 02└ 03└ 04└ 05
02 / 03

ANAGRAMS - INTERACTIVE ARCADE GAME SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <title>Anagram Verse — Dark Edition | Pre‑filled Letters</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    :root {
      --bg-color: #000000;
      --text-color: #f0f3f8;
      --tile-bg: #11161f;
      --tile-border: #2d3a4a;
      --tile-hover-bg: #1f2a36;
      --tile-hover-border: #4d7cb0;
      --slot-bg: #0c0f15;
      --slot-border: #3a4c62;
      --slot-filled-bg: #111a24;
      --slot-filled-border: #4c6a8a;
      --slot-filled-hover-bg: #2a1c1c;
      --slot-filled-hover-border: #e07a7a;
      --msg-correct-bg: #102a1a;
      --msg-correct-text: #9effb0;
      --msg-wrong-bg: #2f1818;
      --msg-wrong-text: #ffb0a8;
      --pill-bg: #12161e;
      --pill-border: #2b3543;
      --pill-text: #cbdbe6;
      --tag-bg: #172618;
      --tag-border: #2d543a;
      --tag-text: #b3f7c2;
      --btn-bg: #12171f;
      --btn-border: #2f4052;
      --btn-text: #eef3fc;
      --btn-hover-bg: #202a36;
      --btn-hover-border: #5f7f9e;
      --btn-primary-bg: #0f2b44;
      --btn-primary-border: #2e628b;
      --btn-primary-text: #cae3ff;
      --btn-primary-hover-bg: #1a3f62;
      --btn-primary-hover-border: #78b0e0;
      --prog-bg: #181e26;
      --hint-bg: #0f1a24;
      --hint-border: #2c4968;
      --hint-text: #bbd6ff;
      --hr-border: #263340;
    }
    
    html:not(.dark) {
      --bg-color: #f5f7fa;
      --text-color: #1a202c;
      --tile-bg: #ffffff;
      --tile-border: #e2e8f0;
      --tile-hover-bg: #edf2f7;
      --tile-hover-border: #3182ce;
      --slot-bg: #e2e8f0;
      --slot-border: #cbd5e0;
      --slot-filled-bg: #ffffff;
      --slot-filled-border: #a0aec0;
      --slot-filled-hover-bg: #fff5f5;
      --slot-filled-hover-border: #f56565;
      --msg-correct-bg: #f0fff4;
      --msg-correct-text: #2f855a;
      --msg-wrong-bg: #fff5f5;
      --msg-wrong-text: #c53030;
      --pill-bg: #ffffff;
      --pill-border: #e2e8f0;
      --pill-text: #4a5568;
      --tag-bg: #f0fff4;
      --tag-border: #9ae6b4;
      --tag-text: #2f855a;
      --btn-bg: #ffffff;
      --btn-border: #e2e8f0;
      --btn-text: #2d3748;
      --btn-hover-bg: #edf2f7;
      --btn-hover-border: #cbd5e0;
      --btn-primary-bg: #3182ce;
      --btn-primary-border: #2b6cb0;
      --btn-primary-text: #ffffff;
      --btn-primary-hover-bg: #2b6cb0;
      --btn-primary-hover-border: #2c5282;
      --prog-bg: #e2e8f0;
      --hint-bg: #ebf8ff;
      --hint-border: #bee3f8;
      --hint-text: #2b6cb0;
      --hr-border: #e2e8f0;
    }

    body {
      background: var(--bg-color);
      font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, sans-serif;
      color: var(--text-color);
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      padding: 1rem;
      margin: 0;
    }

    .game-full {
      width: 100%;
      max-width: 900px;
      margin: 0 auto;
      padding: 1.2rem 1rem;
    }

    /* letter tiles & answer slots (dark mode) */
    .letter-tile {
      display: inline-flex; align-items: center; justify-content: center;
      width: 54px; height: 60px;
      border-radius: 16px;
      font-size: 24px; font-weight: 600;
      cursor: pointer; user-select: none;
      border: 1px solid var(--tile-border);
      background: var(--tile-bg);
      color: var(--text-color);
      transition: transform 0.12s, background 0.1s, box-shadow 0.1s;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
    }
    .letter-tile:hover {
      background: var(--tile-hover-bg);
      transform: translateY(-2px);
      border-color: var(--tile-hover-border);
    }
    .letter-tile.used {
      opacity: 0.28;
      pointer-events: none;
      filter: grayscale(0.4);
      transform: none;
    }

    .answer-slot {
      display: inline-flex; align-items: center; justify-content: center;
      width: 54px; height: 60px;
      border-radius: 16px;
      font-size: 24px; font-weight: 600;
      cursor: pointer;
      border: 1.5px dashed var(--slot-border);
      background: var(--slot-bg);
      color: var(--text-color);
      transition: all 0.12s;
    }
    .answer-slot.filled {
      border-style: solid;
      border-color: var(--slot-filled-border);
      background: var(--slot-filled-bg);
      box-shadow: 0 1px 3px rgba(0,0,0,0.2);
    }
    .answer-slot.filled:hover {
      background: var(--slot-filled-hover-bg);
      border-color: var(--slot-filled-hover-border);
      transform: scale(0.97);
    }

    .tiles-row {
      display: flex;
      gap: 12px;
      justify-content: center;
      flex-wrap: wrap;
      margin: 0.25rem 0;
    }

    @media (max-width: 540px) {
      .letter-tile, .answer-slot { width: 44px; height: 50px; font-size: 20px; }
      .tiles-row { gap: 8px; }
    }

    /* Centered messages (no left-aligned warnings) */
    .result-msg {
      font-size: 15px;
      font-weight: 550;
      min-height: 2.7rem;
      text-align: center;
      padding: 0.4rem;
      letter-spacing: -0.2px;
      display: flex;
      justify-content: center;
    }
    .correct-msg {
      color: var(--msg-correct-text);
      background: var(--msg-correct-bg);
      display: inline-block;
      padding: 0.3rem 1.2rem;
      border-radius: 60px;
      width: auto;
    }
    .wrong-msg {
      color: var(--msg-wrong-text);
      background: var(--msg-wrong-bg);
      display: inline-block;
      padding: 0.3rem 1.2rem;
      border-radius: 60px;
    }

    .score-pill {
      display: inline-block;
      background: var(--pill-bg);
      border-radius: 60px;
      padding: 6px 18px;
      font-size: 13px;
      font-weight: 500;
      color: var(--pill-text);
      backdrop-filter: blur(2px);
      border: 0.5px solid var(--pill-border);
    }
    .score-pill strong {
      color: var(--text-color);
      font-weight: 700;
    }

    .word-history {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      justify-content: center;
      min-height: 38px;
      margin-top: 8px;
    }
    .solved-tag {
      font-size: 12px;
      background: var(--tag-bg);
      color: var(--tag-text);
      padding: 5px 14px;
      border-radius: 40px;
      font-weight: 500;
      border: 0.5px solid var(--tag-border);
      letter-spacing: 0.3px;
    }

    .btn {
      cursor: pointer;
      padding: 8px 22px;
      border-radius: 44px;
      font-size: 14px;
      font-weight: 530;
      border: 0.5px solid var(--btn-border);
      background: var(--btn-bg);
      color: var(--btn-text);
      transition: all 0.15s;
      font-family: inherit;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      box-shadow: 0 1px 2px rgba(0,0,0,0.1);
    }
    .btn:hover {
      background: var(--btn-hover-bg);
      transform: translateY(-1px);
      border-color: var(--btn-hover-border);
    }
    .btn-primary {
      background: var(--btn-primary-bg);
      color: var(--btn-primary-text);
      border-color: var(--btn-primary-border);
    }
    .btn-primary:hover {
      background: var(--btn-primary-hover-bg);
      border-color: var(--btn-primary-hover-border);
    }

    .progress-bar-bg {
      background: var(--prog-bg);
      border-radius: 80px;
      height: 6px;
      overflow: hidden;
      box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
    }
    .progress-bar-fill {
      height: 100%;
      background: #4f9eff;
      border-radius: 80px;
      transition: width 0.35s ease;
    }

    .hint-text {
      font-size: 13px;
      color: var(--hint-text);
      background: var(--hint-bg);
      padding: 8px 16px;
      border-radius: 40px;
      display: inline-block;
      margin: 0 auto;
      font-weight: 470;
      border: 0.5px solid var(--hint-border);
    }

    hr {
      border: none;
      border-top: 1px solid var(--hr-border);
      margin: 0.75rem 0;
    }
  </style>
</head>
<body>
<div class="game-full">
  <h2 class="sr-only" style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;">Anagram word unscramble – 8‑letter challenge with pre‑filled hints</h2>

  <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:12px;">
    <div style="display:flex; gap:10px;">
      <span class="score-pill">🏆 SCORE: <strong id="score">0</strong></span>
      <span class="score-pill">📖 WORD <strong id="word-num">1</strong> / <strong id="word-total">?</strong></span>
    </div>
    <button class="btn" id="skipButton">⏩ SKIP</button>
  </div>

  <div style="margin-bottom:1.2rem;">
    <div class="progress-bar-bg"><div class="progress-bar-fill" id="progress-fill" style="width:0%"></div></div>
  </div>

  <div style="text-align:center; margin-bottom:1.8rem;">
    <p style="font-size:13px; font-weight:500; color:var(--pill-text); margin:0 0 12px; letter-spacing:0.4px;">✍️ YOUR ANSWER</p>
    <div class="tiles-row" id="answer-row"></div>
  </div>

  <div style="text-align:center; margin-bottom:1.8rem;">
    <p style="font-size:13px; font-weight:500; color:var(--pill-text); margin:0 0 12px;">🔀 SCRAMBLED — tap to add</p>
    <div class="tiles-row" id="source-row"></div>
  </div>

  <div class="result-msg" id="result-msg" style="margin-bottom:1rem;"></div>
  <div style="text-align:center; margin-bottom:1.2rem;" id="hint-wrapper">
    <div class="hint-text" id="hint-text"></div>
  </div>

  <div style="display:flex; gap:14px; justify-content:center; margin-bottom:2rem; flex-wrap:wrap;">
    <button class="btn" id="clearBtn">🗑️ CLEAR</button>
    <button class="btn btn-primary" id="checkBtn">✅ CHECK</button>
    <button class="btn" id="hintBtn">💡 HINT (−1 pt)</button>
  </div>

  <div>
    <p style="font-size:12px; color:var(--pill-text); text-align:center; margin:0 0 10px; font-weight:500;">✨ SOLVED WORDS</p>
    <div class="word-history" id="word-history"></div>
  </div>
</div>

<script>
  (function() {
    function syncTheme() {
      try {
        if (window.parent && window.parent.document) {
          const isDark = window.parent.document.documentElement.classList.contains('dark');
          if (isDark) {
            document.documentElement.classList.add('dark');
          } else {
            document.documentElement.classList.remove('dark');
          }
        }
      } catch (e) {}
    }
    syncTheme();
    try {
      if (window.parent && window.parent.document) {
        const observer = new MutationObserver(syncTheme);
        observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
      }
    } catch (e) {}
  })();

  // ======================= 8-LETTER WORD DATABASE =======================
  const RAW_WORDS = [
    "Absolute","Bachelor","Computer","Abstract","Bacteria","Conclude","Academic","Baseball","Concrete",
    "Accepted","Bathroom","Conflict","Accident","Becoming","Confused","Accuracy","Birthday","Congress",
    "Accurate","Boundary","Consider","Achieved","Breaking","Constant","Acquired","Breeding","Consumer",
    "Activity","Building","Continue","Actually","Bulletin","Contract","Addition","Business","Contrary",
    "Adequate","Calendar","Contrast","Adjacent","Campaign","Convince","Adjusted","Capacity","Corridor",
    "Advanced","Cashmere","Casualty","Catching","Category","Catholic","Cautious","Cellular","Ceremony",
    "Chairman","Champion","Chemical","Children","Circular","Civilian","Clearing","Clinical","Clothing",
    "Collapse","Colonial","Colourful","Commence","Commerce","Complain","Complete","Composed","Compound",
    "Comprise","Doubtful","Emerging","Directly","Dramatic","Emphasis","Director","Dressing","Employee",
    "Disabled","Dropping","Endeavour","Disaster","Duration","Engaging","Disclose","Dynamics","Engineer",
    "Discount","Earnings","Enormous","Discover","Economic","Entirely","Disorder","Educated","Entrance",
    "Disposal","Efficacy","Envelope","Distance","Eighteen","Equality","Distinct","Election","Equation",
    "District","Electric","Estimate","Dividend","Eligible","Evaluate","Division","External","Eventual",
    "Doctrine","Facility","Everyday","Document","Familiar","Everyone","Domestic","Featured","Evidence",
    "Dominant","Feedback","Exchange","Dominate","Festival","Exciting","Floating","Finished","Exercise",
    "Football","Firewall","Explicit","Foothill","Flagship","Exposure","Forecast","Flexible","Extended",
    "Foremost","Function","Guidance","Formerly","Generate","Handling","Fourteen","Generous","Hardware",
    "Fraction","Genomics","Heritage","Fracture","Goodwill","Highland","Frequent","Governor","Historic",
    "Friendly","Graduate","Homeless","Frontier","Graphics","Homepage","Imperial","Grateful","Hospital",
    "Incident","Included","Initiate","Identify","Increase","Innocent","Identity","Indicate","Inspired",
    "Ideology","Indirect","Instance","Intimate","Industry","Integral","Intranet","Informal","Intended",
    "Invasion","Informed","Interact","Involved","Inherent","Interest","Isolated","Keyboard","Interior",
    "Judgment","Landlord","Internal","Judicial","Language","Interval","Junction","Laughter","Material",
    "Minority","Learning","Maturity","Mobility","Leverage","Maximise","Modelling","Moderate","Lifetime",
    "Meantime","Momentum","Lighting","Measured","Monetary","Likewise","Medicine","Moreover","Literary",
    "Medieval","Mortgage","Location","Merchant","Mountain","Magazine","Midnight","Mounting","Magnetic",
    "Military","Movement","Maintain","Minimise","Multiple","Majority","Minister","National","Marginal",
    "Ministry","Negative","Marriage","Original","Nineteen","Preserve","Overcome","Northern","Pressing",
    "Overhead","Notebook","Pressure","Overseas","Numerous","Previous","Overview","Observer","Princess",
    "Painting","Occasion","Printing","Parallel","Offering","Priority","Parental","Official","Probable",
    "Patented","Offshore","Probably","Patience","Operator","Producer","Peaceful","Opponent","Profound",
    "Periodic","Opposite","Progress","Personal","Optimism","Property","Persuade","Optional","Proposal",
    "Petition","Organise","Prospect","Physical","Organised","Protocol","Pipeline","Relation","Provided",
    "Platform","Relative","Provider","Pleasant","Relevant","Province","Pleasure","Reliable","Publicly",
    "Politics","Reliance","Purchase","Portable","Religion","Pursuant","Portrait","Remember","Quantity",
    "Position","Renowned","Question","Positive","Repeated","Rational","Possible","Reporter","Reaction",
    "Powerful","Republic","Received","Practice","Required","Receiver","Precious","Research","Recovery",
    "Pregnant","Reserved","Regional","Presence","Resident","Register","Scenario","Resigned","Revision",
    "Schedule","Resource","Rigorous","Scrutiny","Response","Romantic","Seasonal","Restrict","Sampling",
    "Secondly","Somebody","Struggle","Security","Somewhat","Stunning","Sensible","Southern","Suburban",
    "Sentence","Speaking","Suitable","Separate","Specific","Superior","Sequence","Spectrum","Supposed",
    "Sergeant","Sporting","Surgical","Shipping","Standard","Surprise","Shortage","Standing","Survival",
    "Shoulder","Standout","Sweeping","Simplify","Sterling","Swimming","Situated","Straight","Symbolic",
    "Slightly","Strategy","Sympathy","Software","Strength","Syndrome","Solution","Striking","Tactical",
    "Terrible","Training","Tailored","Thinking","Transfer","Takeover","Thirteen","Tragical","Tangible",
    "Thorough","Treasury","Taxation","Thousand","Triangle","Taxpayer","Together","Tropical","Teaching",
    "Tomorrow","Turnover","Tendency","Touching","Ultimate","Terminal","Tracking","Umbrella","Universe",
    "Weakness","Withdraw","Unlawful","Weighted","Woodland","Unlikely","Whatever","Workshop","Valuable",
    "Whenever","Yourself","Variable","Wherever","Volatile","Vertical","Wildlife","Warranty","Victoria",
    "Wireless","Violence"
  ];
  const WORDS = [...new Set(RAW_WORDS.filter(w => w.length === 8))];

  function shuffleArray(arr) {
    const a = [...arr];
    for (let i = a.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  }

  // pick two positions that are NOT adjacent (spaced apart)
  function getTwoSpacedApartPositions() {
    for (let tries = 0; tries < 50; tries++) {
      const i1 = Math.floor(Math.random() * 8);
      let i2 = Math.floor(Math.random() * 8);
      while (i2 === i1) i2 = Math.floor(Math.random() * 8);
      if (Math.abs(i1 - i2) > 1) return [i1, i2].sort((a,b)=>a-b);
    }
    return [0, 2]; // safe fallback
  }

  let gameQueue = shuffleArray([...WORDS]);
  let currentIdx = 0;
  let totalScore = 0;
  let currentAnswer = [];        // array of length 8, each { letter, srcIdx } or undefined
  let sourceLetters = [];        // full scrambled array (length 8)
  let usedSourceIndices = new Set(); // indices into sourceLetters that are already placed
  let hintCountForWord = 0;
  let hintPenalty = 0;
  let isTransitioning = false;

  // DOM elements
  const answerRowEl = document.getElementById('answer-row');
  const sourceRowEl = document.getElementById('source-row');
  const scoreSpan = document.getElementById('score');
  const wordNumSpan = document.getElementById('word-num');
  const wordTotalSpan = document.getElementById('word-total');
  const progressFill = document.getElementById('progress-fill');
  const resultMsgDiv = document.getElementById('result-msg');
  const hintTextDiv = document.getElementById('hint-text');
  const wordHistoryDiv = document.getElementById('word-history');
  const skipBtn = document.getElementById('skipButton');
  const clearBtn = document.getElementById('clearBtn');
  const checkBtn = document.getElementById('checkBtn');
  const hintBtn = document.getElementById('hintBtn');

  function updateScoreUI() {
    scoreSpan.innerText = totalScore;
  }

  // Reset current word to prefilled state (keep the same target word)
  function resetToPrefilledState() {
    const targetWord = gameQueue[currentIdx].toUpperCase();
    // scramble letters fresh each reset (more dynamic)
    let scrambledArr = shuffleArray(targetWord.split(''));
    while (scrambledArr.join('') === targetWord) {
      scrambledArr = shuffleArray(targetWord.split(''));
    }
    sourceLetters = scrambledArr;

    // pick two spaced apart positions
    const positions = getTwoSpacedApartPositions();
    const newAnswer = new Array(8).fill(undefined);
    const newUsed = new Set();

    for (let pos of positions) {
      const neededChar = targetWord[pos];
      // find first occurrence in sourceLetters that matches neededChar and not already used
      let foundIdx = -1;
      for (let i = 0; i < sourceLetters.length; i++) {
        if (!newUsed.has(i) && sourceLetters[i] === neededChar) {
          foundIdx = i;
          break;
        }
      }
      if (foundIdx !== -1) {
        newAnswer[pos] = { letter: neededChar, srcIdx: foundIdx };
        newUsed.add(foundIdx);
      }
    }
    currentAnswer = newAnswer;
    usedSourceIndices = newUsed;
    renderSource();
    renderAnswer();
  }

  function loadWord() {
    isTransitioning = false;
    if (currentIdx >= gameQueue.length) {
      document.querySelector('.game-full').innerHTML = `
        <div style="text-align:center; padding:2rem 1.5rem; width:100%;">
          <div style="font-size:64px; margin-bottom:1rem;">🏆✨</div>
          <h2 style="font-size:28px; font-weight:600; margin:0 0 0.6rem; color:#fff;">Anagram Master!</h2>
          <p style="color:#bfd6f0; font-size:1rem; margin-bottom:1.8rem;">Final score: <strong style="font-size:1.8rem;">${totalScore}</strong> out of ${gameQueue.length}</p>
          <button class="btn btn-primary" onclick="location.reload()">🔄 PLAY AGAIN</button>
        </div>`;
      return;
    }

    const targetWord = gameQueue[currentIdx].toUpperCase();
    // create scrambled full set (8 letters)
    let scrambledArr = shuffleArray(targetWord.split(''));
    while (scrambledArr.join('') === targetWord) {
      scrambledArr = shuffleArray(targetWord.split(''));
    }
    sourceLetters = scrambledArr;

    // pick two spaced apart positions
    const positions = getTwoSpacedApartPositions();
    const newAnswer = new Array(8).fill(undefined);
    const newUsed = new Set();

    for (let pos of positions) {
      const neededChar = targetWord[pos];
      let foundIdx = -1;
      for (let i = 0; i < sourceLetters.length; i++) {
        if (!newUsed.has(i) && sourceLetters[i] === neededChar) {
          foundIdx = i;
          break;
        }
      }
      if (foundIdx !== -1) {
        newAnswer[pos] = { letter: neededChar, srcIdx: foundIdx };
        newUsed.add(foundIdx);
      }
    }
    currentAnswer = newAnswer;
    usedSourceIndices = newUsed;

    // reset hint state
    hintCountForWord = 0;
    hintPenalty = 0;
    resultMsgDiv.innerHTML = '';
    hintTextDiv.textContent = '';

    renderSource();
    renderAnswer();

    wordNumSpan.innerText = currentIdx + 1;
    wordTotalSpan.innerText = gameQueue.length;
    const progressPercent = (currentIdx / gameQueue.length) * 100;
    progressFill.style.width = `${progressPercent}%`;
  }

  function renderSource() {
    sourceRowEl.innerHTML = '';
    sourceLetters.forEach((letter, idx) => {
      const tile = document.createElement('span');
      tile.className = 'letter-tile' + (usedSourceIndices.has(idx) ? ' used' : '');
      tile.textContent = letter;
      tile.onclick = () => pickLetter(idx);
      sourceRowEl.appendChild(tile);
    });
  }

  function renderAnswer() {
    answerRowEl.innerHTML = '';
    for (let i = 0; i < 8; i++) {
      const slot = document.createElement('span');
      const isFilled = currentAnswer[i] !== undefined;
      slot.className = 'answer-slot' + (isFilled ? ' filled' : '');
      slot.textContent = isFilled ? currentAnswer[i].letter : '';
      if (isFilled) {
        slot.onclick = (() => { if (!isTransitioning) removeLetter(i); });
      } else {
        slot.onclick = null;
      }
      answerRowEl.appendChild(slot);
    }
  }

  function pickLetter(idx) {
    if (isTransitioning) return;
    if (usedSourceIndices.has(idx)) return;
    // find first empty slot in answer
    const emptyPos = currentAnswer.findIndex(slot => slot === undefined);
    if (emptyPos === -1) {
      resultMsgDiv.innerHTML = '<span class="wrong-msg">❌ Answer is full (8 letters). Clear or remove some.</span>';
      return;
    }
    currentAnswer[emptyPos] = { letter: sourceLetters[idx], srcIdx: idx };
    usedSourceIndices.add(idx);
    renderSource();
    renderAnswer();
    resultMsgDiv.innerHTML = '';
  }

  function removeLetter(pos) {
    if (isTransitioning) return;
    const item = currentAnswer[pos];
    if (!item) return;
    usedSourceIndices.delete(item.srcIdx);
    currentAnswer[pos] = undefined;
    renderSource();
    renderAnswer();
    resultMsgDiv.innerHTML = '';
  }

  function clearAnswer() {
    if (isTransitioning) return;
    // Reset current word to original prefilled state (same target, fresh scramble)
    resetToPrefilledState();
    resultMsgDiv.innerHTML = '';
    hintTextDiv.textContent = '';
    hintCountForWord = 0;
    hintPenalty = 0;
  }

  function submitAnswer() {
    if (isTransitioning) return;
    if (currentAnswer.some(slot => slot === undefined)) {
      resultMsgDiv.innerHTML = '<span class="wrong-msg">⚠️ Fill all 8 letters before checking!</span>';
      return;
    }
    const guess = currentAnswer.map(a => a.letter).join('').toUpperCase();
    const target = gameQueue[currentIdx].toUpperCase();
    if (guess === target) {
      let pointsEarned = Math.max(1, 3 - hintPenalty);
      totalScore += pointsEarned;
      updateScoreUI();

      resultMsgDiv.innerHTML = pointsEarned === 3
        ? '<span class="correct-msg">🎉 PERFECT! +3 points</span>'
        : `<span class="correct-msg">✔ CORRECT! +${pointsEarned} point${pointsEarned !== 1 ? 's' : ''} (hint used)</span>`;

      const solvedWord = gameQueue[currentIdx];
      const tag = document.createElement('span');
      tag.className = 'solved-tag';
      tag.textContent = solvedWord;
      wordHistoryDiv.appendChild(tag);

      isTransitioning = true;
      setTimeout(() => {
        currentIdx++;
        loadWord();
      }, 820);
    } else {
      resultMsgDiv.innerHTML = '<span class="wrong-msg">✗ INCORRECT — keep trying!</span>';
      const filledSlots = document.querySelectorAll('.answer-slot.filled');
      filledSlots.forEach(slot => {
        slot.style.transition = 'background 0.08s';
        slot.style.background = '#361b1b';
        slot.style.borderColor = '#d97a72';
        setTimeout(() => {
          if (slot) { slot.style.background = ''; slot.style.borderColor = ''; }
        }, 480);
      });
    }
  }

  function showHint() {
    if (isTransitioning) return;
    if (currentIdx >= gameQueue.length) return;
    const targetWord = gameQueue[currentIdx].toUpperCase();
    hintCountForWord++;
    if (hintPenalty < 2) hintPenalty = Math.min(2, hintCountForWord);
    let hintMsg = '';
    if (hintCountForWord === 1) {
      hintMsg = `💡 STARTS WITH “${targetWord[0]}”`;
    } else if (hintCountForWord === 2) {
      hintMsg = `🔍 STARTS WITH “${targetWord.slice(0, 2)}” … ENDS WITH “${targetWord.slice(-1)}”`;
    } else {
      const firstThree = targetWord.slice(0, 3);
      hintMsg = `📖 PATTERN: ${firstThree} _ _ _ _ _`;
    }
    hintTextDiv.textContent = hintMsg;
    hintTextDiv.style.opacity = '0.8';
    setTimeout(() => { if(hintTextDiv) hintTextDiv.style.opacity = '1'; }, 120);
  }

  function skipWord() {
    if (isTransitioning) return;
    if (currentIdx >= gameQueue.length) return;
    isTransitioning = true;
    const skippedWord = gameQueue[currentIdx];
    resultMsgDiv.innerHTML = `<span class="wrong-msg">⏩ SKIPPED — the word was “${skippedWord}”</span>`;
    setTimeout(() => {
      currentIdx++;
      loadWord();
    }, 1050);
  }

  skipBtn.addEventListener('click', skipWord);
  clearBtn.addEventListener('click', clearAnswer);
  checkBtn.addEventListener('click', submitAnswer);
  hintBtn.addEventListener('click', showHint);

  loadWord();
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.