Word Search bug-fixes + Codex polish
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
<p class="theme"><span class="lbl">Today’s theme</span>{theme}</p>
|
||||
|
||||
<div class="grid" class:ok={okFlash} class:done={status === 'done'} bind:this={gridEl} style="--n:{n}"
|
||||
role="application" aria-label="Word search grid — drag across letters to find words"
|
||||
onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
||||
{#each grid as rowStr, r (r)}
|
||||
{#each rowStr.split('') as ch, c (c)}
|
||||
@@ -180,24 +192,23 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wordsearch { max-width: 460px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
|
||||
.wordsearch { max-width: 520px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
|
||||
.wordsearch.ready { opacity: 1; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
|
||||
.theme { text-align: center; font-family: var(--serif); font-size: 1.3rem; color: var(--accent-deep); margin: 0 0 14px; }
|
||||
.theme .lbl { display: block; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.64rem;
|
||||
font-family: var(--label); color: var(--muted); margin-bottom: 2px; }
|
||||
.grid {
|
||||
/* Constant cell size — the board grows with the grid; on a narrow phone the
|
||||
biggest grid gently shrinks to fit the width. */
|
||||
--cell: min(32px, calc((100vw - 44px) / var(--n)));
|
||||
display: grid; grid-template-columns: repeat(var(--n), var(--cell)); gap: 2px;
|
||||
width: fit-content; max-width: 100%; margin: 0 auto 16px; touch-action: none;
|
||||
user-select: none; -webkit-user-select: none; border-radius: 10px; padding: 6px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
/* Cells are ~32px on a wide screen (board grows with the grid) and shrink via
|
||||
1fr to fit a narrow phone — capped by max-width so it can never overflow. */
|
||||
display: grid; grid-template-columns: repeat(var(--n), 1fr); gap: 2px;
|
||||
width: 100%; max-width: calc(var(--n) * 32px + 16px); margin: 0 auto 16px;
|
||||
touch-action: none; user-select: none; -webkit-user-select: none;
|
||||
border-radius: 10px; padding: 6px; background: var(--surface); border: 1px solid var(--line);
|
||||
}
|
||||
.grid.done { opacity: 0.9; }
|
||||
.cell {
|
||||
width: var(--cell); height: var(--cell); display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--label); font-weight: 600; font-size: calc(var(--cell) * 0.5);
|
||||
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--label); font-weight: 600; font-size: clamp(0.58rem, 2.5vw, 1rem);
|
||||
color: var(--ink); border-radius: 5px; background: transparent; text-transform: uppercase;
|
||||
}
|
||||
.cell.sel { background: var(--accent) !important; color: #fff !important; }
|
||||
|
||||
Reference in New Issue
Block a user