// Pure board reconstruction. The wire carries no board (StateView is summary + rack // only), so the live grid is rebuilt by replaying the decoded move journal — exactly // the dictionary-independent history invariant (ARCHITECTURE §9.1): apply each play's // placed tiles onto an empty grid. import type { MoveRecord } from './model'; import { BOARD_SIZE } from './premiums'; export interface BoardCell { letter: string; blank: boolean; } export type Board = (BoardCell | null)[][]; export function emptyBoard(): Board { return Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => null as BoardCell | null), ); } function inBounds(r: number, c: number): boolean { return r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE; } /** replay folds every play move's tiles onto an empty board (pass/exchange/resign * change no squares). */ export function replay(moves: MoveRecord[]): Board { const b = emptyBoard(); for (const m of moves) { if (m.action !== 'play') continue; for (const t of m.tiles) { if (inBounds(t.row, t.col)) b[t.row][t.col] = { letter: t.letter, blank: t.blank }; } } return b; } /** lastMoveCells returns the cells of the last move's tiles (as "row,col" keys), but only * when that move placed tiles (a play). A trailing pass/exchange/resign/timeout highlights * nothing — the recent-move highlight tracks the last move overall, not the last word. */ export function lastMoveCells(moves: MoveRecord[]): Set { const last = moves.length ? moves[moves.length - 1] : null; if (!last || last.action !== 'play') return new Set(); return new Set(last.tiles.map((t) => `${t.row},${t.col}`)); }