// The local move preview: it produces the same EvalResult the network `evaluate` // returns (legality, score, the words formed, the inferred orientation), computed // on-device from a loaded dictionary so composing a move needs no round trip. It // adapts the client's letter-space board and placements into the validator's // index space (lib/dict/validate.ts), assembling the per-variant ruleset from the // static geometry/constants (lib/premiums.ts) and the server-sent tile values // (lib/alphabet.ts). The server stays authoritative — submit re-validates — so any // throw here is caught by the caller, which falls back to the network preview. import { validatePlay, Horizontal, type Board as VBoard, type Ruleset, type Placement as VPlacement, type Dict } from './validate'; import { playDirection } from './direction'; import type { Board as ClientBoard } from '../board'; import type { PlacedTile } from '../client'; import type { EvalResult, Variant } from '../model'; import { premiumGrid, centre, BOARD_SIZE, RACK_SIZE, BINGO, type Premium } from '../premiums'; import { alphabetValues, indexForLetter, letterForIndex } from '../alphabet'; function letterMultOf(p: Premium): number { return p === 'DL' ? 2 : p === 'TL' ? 3 : 1; } function wordMultOf(p: Premium): number { return p === 'DW' ? 2 : p === 'TW' ? 3 : 1; } // buildBoard adapts the client's letter-space board into the validator's // index-space read view, precomputing the letter indices once. function buildBoard(variant: Variant, board: ClientBoard): VBoard { const grid: ({ letter: number; blank: boolean } | null)[] = new Array(BOARD_SIZE * BOARD_SIZE).fill(null); let empty = true; for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE; c++) { const cell = board[r][c]; if (cell) { grid[r * BOARD_SIZE + c] = { letter: indexForLetter(variant, cell.letter), blank: cell.blank }; empty = false; } } } const inBounds = (r: number, c: number): boolean => r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE; return { inBounds, filled: (r, c) => inBounds(r, c) && grid[r * BOARD_SIZE + c] !== null, cellAt: (r, c) => grid[r * BOARD_SIZE + c]!, isEmpty: () => empty, }; } // buildRuleset assembles the per-variant scoring rules the validator needs. function buildRuleset(variant: Variant, multipleWords: boolean): Ruleset { const prem = premiumGrid(variant); const ctr = centre(variant); const values = alphabetValues(variant); return { cols: BOARD_SIZE, center: ctr.row * BOARD_SIZE + ctr.col, rackSize: RACK_SIZE, bingo: BINGO[variant], values, letterMult: (r, c) => letterMultOf(prem[r][c]), wordMult: (r, c) => wordMultOf(prem[r][c]), ignoreCrossWords: !multipleWords, }; } // decodeWord turns a word's alphabet indices back into a lower-cased string, the // form the network EvalResult carries (the caption renders it directly). function decodeWord(variant: Variant, letters: number[]): string { let s = ''; for (const i of letters) s += letterForIndex(variant, i); return s.toLowerCase(); } /** * evaluateLocal computes the move preview for tiles placed on board in the given * game, returning the same shape as the network `evaluate`. dict is the loaded * dictionary reader for the game's (variant, version); multipleWords is the game's * cross-word rule. It may throw if the variant's alphabet table is not loaded — the * caller guards with hasAlphabet and falls back to the network on any failure. */ export function evaluateLocal( dict: Dict, variant: Variant, board: ClientBoard, tiles: PlacedTile[], multipleWords: boolean, ): EvalResult { const vboard = buildBoard(variant, board); const rs = buildRuleset(variant, multipleWords); const vtiles: VPlacement[] = tiles.map((t) => ({ row: t.row, col: t.col, letter: indexForLetter(variant, t.letter), blank: t.blank, })); const dir = playDirection(vboard, rs, dict, vtiles); const res = validatePlay(vboard, rs, dict, dir, vtiles); if (!res.legal || !res.move) { return { legal: false, score: 0, words: [], dir: '' }; } const m = res.move; const words = [m.main, ...m.cross].map((w) => decodeWord(variant, w.letters)); return { legal: true, score: m.score, words, dir: dir === Horizontal ? 'H' : 'V' }; }