// Play-direction inference, ported from the backend engine (direction.go // resolveDirection/runLength and domain.go (*Game).playDirection, v1.1.1). The // client sends tiles without an orientation; the server infers it, so the local // move preview must infer the same one or its words and score would diverge. import { validatePlay, Horizontal, Vertical, type Board, type Ruleset, type Placement, type Direction, type Dict } from './validate'; // runLength mirrors engine.runLength: how many cells the word through (row, col) // along dir spans, counting the target square plus the filled runs either side. function runLength(b: Board, row: number, col: number, dir: Direction): number { let dr = 0; let dc = 1; if (dir === Vertical) { dr = 1; dc = 0; } let n = 1; for (let r = row - dr, c = col - dc; b.filled(r, c); r -= dr, c -= dc) n++; for (let r = row + dr, c = col + dc; b.filled(r, c); r += dr, c += dc) n++; return n; } /** * resolveDirection infers a play's orientation from geometry alone, mirroring * engine.resolveDirection: two or more tiles by the line they share; a single tile * by the axis along which it abuts existing tiles, preferring the longer word and * horizontal on a tie; a tile abutting nothing falls back to horizontal. */ export function resolveDirection(b: Board, placements: Placement[]): Direction { if (placements.length >= 2) { const row = placements[0].row; for (let i = 1; i < placements.length; i++) { if (placements[i].row !== row) return Vertical; } return Horizontal; } if (placements.length === 1) { const p = placements[0]; const h = runLength(b, p.row, p.col, Horizontal); const v = runLength(b, p.row, p.col, Vertical); if (v >= 2 && v > h) return Vertical; if (h >= 2) return Horizontal; if (v >= 2) return Vertical; } return Horizontal; } /** * playDirection resolves the orientation for a live play, mirroring engine * (*Game).playDirection. It is the geometric resolution, except a single tile under * the single-word rule (ignoreCrossWords) is ambiguous — its two orientations may * differ in legality — so both are tried through the validator and the * higher-scoring legal one is kept (horizontal breaks a tie). */ export function playDirection(b: Board, rs: Ruleset, dict: Dict, placements: Placement[]): Direction { const geo = resolveDirection(b, placements); if (placements.length !== 1 || !rs.ignoreCrossWords) { return geo; } let best = geo; let found = false; let bestScore = 0; for (const dir of [Horizontal, Vertical] as Direction[]) { const r = validatePlay(b, rs, dict, dir, placements); if (!r.legal || !r.move) continue; if (!found || r.move.score > bestScore) { best = dir; found = true; bestScore = r.move.score; } } return best; }