Word Search: extract pure selection/match logic + pin with vitest

Per Codex's Phase 2 audit notes. Moved the drag-snap (lineFrom) and find-match
(matchWord) logic into $lib/wordsearch.js and added vitest coverage:
- lineFrom always yields a straight, in-bounds path — a non-straight drag snaps,
  never returns bent; single cell and edge-clamping covered.
- matchWord matches forward + reversed selections, is a harmless no-op on an
  already-found word (so completion/best-time can't double-record), and returns
  null for non-words / too-short selections.

Restore behaviour audited: finish() (which records best-time) only runs when the
final word is found mid-play; on refresh, restore() repopulates found cells +
time and the derived status flips to done WITHOUT calling finish(), so best-time
never re-records. First JS test runner for the frontend (npm test → vitest run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-10 20:23:13 -04:00
parent 90cd0291a3
commit 33d5d55c33
5 changed files with 443 additions and 28 deletions
@@ -1,5 +1,6 @@
<script>
import { getJSON } from '$lib/api.js';
import { lineFrom, matchWord } from '$lib/wordsearch.js';
let { onstatus } = $props();
@@ -69,25 +70,6 @@
return [r, c];
}
// Snap a drag to the nearest of the 8 straight directions.
function lineFrom(start, end) {
const [r0, c0] = start, [r1, c1] = end;
let dr = r1 - r0, dc = c1 - c0;
if (dr === 0 && dc === 0) return [start];
const adr = Math.abs(dr), adc = Math.abs(dc);
if (adr > adc * 2) dc = 0;
else if (adc > adr * 2) dr = 0;
else { const m = Math.max(adr, adc); dr = Math.sign(dr) * m; dc = Math.sign(dc) * m; }
const steps = Math.max(Math.abs(dr), Math.abs(dc));
const sr = Math.sign(dr), sc = Math.sign(dc), cells = [];
for (let i = 0; i <= steps; i++) {
const r = r0 + sr * i, c = c0 + sc * i;
if (r < 0 || r >= size || c < 0 || c >= size) break;
cells.push([r, c]);
}
return cells.length ? cells : [start];
}
function down(e) {
if (status === 'done') return;
selecting = true;
@@ -98,7 +80,7 @@
}
function move(e) {
if (!selecting) return;
sel = lineFrom(sel[0], cellAt(e));
sel = lineFrom(sel[0], cellAt(e), size);
}
function up() {
if (!selecting) return;
@@ -108,11 +90,7 @@
}
function evaluate(cells) {
if (cells.length < 2) return;
const word = cells.map(([r, c]) => grid[r][c]).join('');
const rev = [...word].reverse().join('');
const hit = words.includes(word) && !found.includes(word) ? word
: words.includes(rev) && !found.includes(rev) ? rev : null;
const hit = matchWord(cells, grid, words, found);
if (!hit) return;
found = [...found, hit];
const fc = new Set(foundCells);