fuzzyworld

word-waltz / commit

build fix

09447c6803ac1dcbac4394d9d1d4e27a5666804e · Pavel Safronov · 2026-03-23 17:43:53 -0700 · parents 1c5481685a1f

build fix
 cspell.json                   |   3 +-
 src/components/WordleBoard.ts | 181 +++++++++++++++++++++++++++++++++++++++++-
 src/main.ts                   |  15 +++-
 src/utils/wordList.ts         |   1 +
 4 files changed, 193 insertions(+), 7 deletions(-)

diff --git a/cspell.json b/cspell.json
index b59a05e..dc55e84 100644
--- a/cspell.json
+++ b/cspell.json
@@ -23,6 +23,7 @@
     "ignorePaths": [
         "node_modules",
         "dist",
-        "package-lock.json"
+        "package-lock.json",
+        "src/utils/wordList.ts"
     ]
 }
\ No newline at end of file
diff --git a/src/components/WordleBoard.ts b/src/components/WordleBoard.ts
index 2cd9e08..3a468b9 100644
--- a/src/components/WordleBoard.ts
+++ b/src/components/WordleBoard.ts
@@ -5,6 +5,11 @@
  * Each cell is colored based on tile state: 'correct', 'present', or 'absent'.
  */
 
+import { mountKeyboard, updateKeyboard } from './Keyboard.js';
+import type { LetterState } from './Keyboard.js';
+import { isValidWord } from '../utils/wordList.js';
+import { calculateScore } from '../utils/scoring.js';
+
 interface RoundResult {
   solved: boolean;
   guesses: string[];
@@ -16,8 +21,178 @@ interface WordleBoardProps {
   onRoundEnd: (result: RoundResult) => void;
 }
 
-// TODO: implement board rendering and guess logic
+const MAX_GUESSES = 6;
+const WORD_LEN = 5;
+const PRAISE = ['Genius!', 'Magnificent!', 'Impressive!', 'Splendid!', 'Great!', 'Lucky!'];
+
+type TileState = LetterState | 'filled' | '';
+
+const STATE_PRIORITY: Record<LetterState, number> = { correct: 3, present: 2, absent: 1 };
+
+function evaluate(guess: string, secret: string): TileState[] {
+  const result: TileState[] = Array(WORD_LEN).fill('absent');
+  const remaining = secret.split('');
+
+  // Pass 1 — exact matches
+  for (let i = 0; i < WORD_LEN; i++) {
+    if (guess[i] === secret[i]) {
+      result[i] = 'correct';
+      remaining[i] = '';
+    }
+  }
+
+  // Pass 2 — right letter, wrong position
+  for (let i = 0; i < WORD_LEN; i++) {
+    if (result[i] === 'correct') continue;
+    const idx = remaining.indexOf(guess[i]);
+    if (idx !== -1) {
+      result[i] = 'present';
+      remaining[idx] = '';
+    }
+  }
+  return result;
+}
+
+function mergeLetterState(cur: LetterState | undefined, next: LetterState): LetterState {
+  if (!cur) return next;
+  return STATE_PRIORITY[next] > STATE_PRIORITY[cur] ? next : cur;
+}
+
 export function mountWordleBoard(container: HTMLElement, props: WordleBoardProps): () => void {
-  // TODO
-  return function cleanup() { };
+  const { secretWord, onRoundEnd } = props;
+  const secret = secretWord.toUpperCase();
+
+  // --- DOM ---
+  const root = document.createElement('div');
+  root.className = 'wordle-board';
+
+  const grid = document.createElement('div');
+  grid.className = 'wordle-board__grid';
+
+  // Build 6×5 tile cells
+  const tiles: HTMLElement[][] = [];
+  for (let r = 0; r < MAX_GUESSES; r++) {
+    const row = document.createElement('div');
+    row.className = 'wordle-board__row';
+    const rowTiles: HTMLElement[] = [];
+    for (let c = 0; c < WORD_LEN; c++) {
+      const tile = document.createElement('div');
+      tile.className = 'wordle-board__tile';
+      row.appendChild(tile);
+      rowTiles.push(tile);
+    }
+    grid.appendChild(row);
+    tiles.push(rowTiles);
+  }
+
+  const message = document.createElement('p');
+  message.className = 'wordle-board__message';
+
+  const keyboardEl = document.createElement('div');
+
+  root.appendChild(grid);
+  root.appendChild(message);
+  root.appendChild(keyboardEl);
+  container.appendChild(root);
+
+  // --- State ---
+  let currentRow = 0;
+  let currentInput = '';
+  let done = false;
+  const letterStates: Record<string, LetterState> = {};
+  const guesses: string[] = [];
+
+  // Mount keyboard — it sends keys back via onKey
+  const cleanupKeyboard = mountKeyboard(keyboardEl, {
+    onKey: handleKey,
+    letterStates,
+  });
+
+  // --- Input handling ---
+  function handleKey(key: string): void {
+    if (done) return;
+    if (key === 'Backspace') {
+      currentInput = currentInput.slice(0, -1);
+      redrawCurrentRow();
+    } else if (key === 'Enter') {
+      submitGuess();
+    } else if (/^[A-Z]$/.test(key) && currentInput.length < WORD_LEN) {
+      currentInput += key;
+      redrawCurrentRow();
+    }
+  }
+
+  function redrawCurrentRow(): void {
+    tiles[currentRow].forEach((tile, i) => {
+      const letter = currentInput[i] ?? '';
+      tile.textContent = letter;
+      tile.className = 'wordle-board__tile' + (letter ? ' wordle-board__tile--filled' : '');
+    });
+  }
+
+  function submitGuess(): void {
+    if (currentInput.length !== WORD_LEN) {
+      flash('Not enough letters');
+      return;
+    }
+    const guess = currentInput;
+    if (!isValidWord(guess)) {
+      flash('Not in word list');
+      return;
+    }
+
+    const states = evaluate(guess, secret);
+
+    // Color tiles
+    tiles[currentRow].forEach((tile, i) => {
+      tile.textContent = guess[i];
+      tile.className = `wordle-board__tile wordle-board__tile--${states[i]}`;
+    });
+
+    // Merge letter states for keyboard
+    states.forEach((s, i) => {
+      if (s === 'correct' || s === 'present' || s === 'absent') {
+        letterStates[guess[i]] = mergeLetterState(letterStates[guess[i]], s);
+      }
+    });
+    updateKeyboard(keyboardEl, letterStates);
+
+    guesses.push(guess);
+    currentRow++;
+    currentInput = '';
+
+    const solved = states.every((s) => s === 'correct');
+    if (solved) {
+      done = true;
+      message.textContent = PRAISE[Math.min(guesses.length - 1, PRAISE.length - 1)];
+      scheduleRoundEnd(true);
+    } else if (currentRow >= MAX_GUESSES) {
+      done = true;
+      message.textContent = `The word was ${secret}`;
+      scheduleRoundEnd(false);
+    }
+  }
+
+  function flash(text: string): void {
+    message.textContent = text;
+    setTimeout(() => { message.textContent = ''; }, 1200);
+  }
+
+  let endTimer: ReturnType<typeof setTimeout> | null = null;
+
+  function scheduleRoundEnd(solved: boolean): void {
+    endTimer = setTimeout(() => {
+      onRoundEnd({
+        solved,
+        guesses,
+        score: calculateScore(guesses.length, solved),
+      });
+    }, 1600);
+  }
+
+  return function cleanup() {
+    if (endTimer !== null) clearTimeout(endTimer);
+    cleanupKeyboard();
+    container.removeChild(root);
+  };
 }
diff --git a/src/main.ts b/src/main.ts
index d4a8844..827cb65 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -206,16 +206,25 @@ function showNewGameScreen(app: HTMLElement): void {
             };
 
             const envelope = await encrypt(JSON.stringify(initialState), cryptoKey);
-            await createGame(gameId, envelope);
+
+            // Race the Firestore write against a 10 s timeout so we surface
+            // errors (e.g. database not created, network issue) instead of
+            // hanging on "Creating…" forever.
+            const timeout = new Promise<never>((_, reject) =>
+                setTimeout(() => reject(new Error('Firestore write timed out — check that the Firestore database exists in the Firebase console and that security rules allow writes.')), 10_000),
+            );
+            await Promise.race([createGame(gameId, envelope), timeout]);
+
             setRole(gameId, 'a');
 
             // Navigate — key is in the fragment and is never sent to the server
             window.location.href = buildLink(gameId, base64urlKey);
         } catch (err) {
-            console.error(err);
+            console.error('Game creation failed:', err);
             btn.disabled = false;
             btn.textContent = 'Create game';
-            showError(app, 'Failed to create game. Please try again.');
+            const msg = err instanceof Error ? err.message : 'Failed to create game. Please try again.';
+            showError(app, msg);
         }
     });
 }
diff --git a/src/utils/wordList.ts b/src/utils/wordList.ts
index 40b5d65..914c730 100644
--- a/src/utils/wordList.ts
+++ b/src/utils/wordList.ts
@@ -1,3 +1,4 @@
+// cspell:disable
 const WORDS = new Set<string>([
   "aalii",
   "abaca",