From b909b7e64b9c77165b53d806a7d42fb91619cd6f Mon Sep 17 00:00:00 2001 From: jay Date: Wed, 10 Jun 2026 21:09:33 -0400 Subject: [PATCH] Word Search bug-fixes + Codex polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reported bugs, same root cause: the fixed-cell grid overflowed its wrapper on Large, so (a) the last column spilled past the border and (b) the pointer→cell math drifted across the row, recording finds "off by a letter". * Grid now uses 1fr columns with max-width = n·32px: the board grows with the grid and can never overflow (shrinks to fit a narrow phone instead). * cellAt() accounts for the grid padding/border, so selection is exact edge-to-edge. * restore() now validates each saved find against the CURRENT grid and drops any whose cells no longer spell the word — clears stale highlights if the day's puzzle changed. Codex follow-ups: * _ws_propose now requires >= large.count + 4 valid words before accepting an LLM proposal (else falls back to a curated theme), so a thin LLM result can't underfill Large. Added a thin-LLM fallback test. * Cleaned Svelte warnings: removed the now-unused .gamecard.soon CSS, added an ARIA role/label to the grid, declared gridEl with $state. Build is warning-clean. * Added a stale-load guard in WordSearchGame.load() so rapid size switches can't let an older request overwrite the newer selection. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/lib/components/WordSearchGame.svelte | 43 ++++++++++++------- frontend/src/routes/play/+page.svelte | 3 -- goodnews/games.py | 7 ++- tests/test_admin.py | 25 +++++++++++ 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/frontend/src/lib/components/WordSearchGame.svelte b/frontend/src/lib/components/WordSearchGame.svelte index 5ba808a..349b28d 100644 --- a/frontend/src/lib/components/WordSearchGame.svelte +++ b/frontend/src/lib/components/WordSearchGame.svelte @@ -22,7 +22,8 @@ let ready = $state(false); let okFlash = $state(false); let copied = $state(false); - let gridEl; + let gridEl = $state(null); + let loadSeq = 0; const n = $derived(grid.length); const stateKey = $derived(`goodnews:wordsearch:${size}:${date}`); @@ -42,17 +43,21 @@ }); async function load() { + const seq = ++loadSeq; // stale-load guard for rapid size switches loading = true; ready = false; foundWords = []; sel = []; resultMs = 0; startTime = 0; try { const p = await getJSON('/api/puzzle/wordsearch?variant=' + size); + if (seq !== loadSeq) return; // a newer size was selected — abandon theme = p.theme; words = p.words; grid = p.grid; date = p.date; restore(); if (!startTime) startTime = Date.now(); try { best = JSON.parse(localStorage.getItem(bestKey) || '0'); } catch { best = 0; } } catch { + if (seq !== loadSeq) return; theme = ''; words = []; } + if (seq !== loadSeq) return; loading = false; requestAnimationFrame(() => (ready = true)); } @@ -61,9 +66,14 @@ try { const s = JSON.parse(localStorage.getItem(stateKey) || 'null'); if (s && Array.isArray(s.foundWords)) { - foundWords = s.foundWords; + // Keep only finds whose stored cells still spell their word in the CURRENT + // grid — guards against stale highlights if the day's puzzle changed. + const valid = s.foundWords.filter((fw) => + fw && Array.isArray(fw.cells) && words.includes(fw.word) && + fw.cells.map(([r, c]) => (grid[r] && grid[r][c]) || '').join('') === fw.word); + foundWords = valid; startTime = s.startTime || 0; - resultMs = s.ms || 0; + resultMs = valid.length === words.length ? (s.ms || 0) : 0; } } catch { /* ignore */ } onstatus?.(summary()); @@ -77,9 +87,10 @@ function cellAt(e) { const rect = gridEl.getBoundingClientRect(); - const cw = rect.width / n; - const c = Math.min(n - 1, Math.max(0, Math.floor((e.clientX - rect.left) / cw))); - const r = Math.min(n - 1, Math.max(0, Math.floor((e.clientY - rect.top) / cw))); + const pad = 7; // grid padding (6) + border (1) + const cw = (rect.width - 2 * pad) / n; // even 1fr columns share the inner width + const c = Math.min(n - 1, Math.max(0, Math.floor((e.clientX - rect.left - pad) / cw))); + const r = Math.min(n - 1, Math.max(0, Math.floor((e.clientY - rect.top - pad) / cw))); return [r, c]; } @@ -144,6 +155,7 @@

Today’s theme{theme}

{#each grid as rowStr, r (r)} {#each rowStr.split('') as ch, c (c)} @@ -180,24 +192,23 @@