ac29dca865
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 41s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s
- Highlight tracks the last move overall (not the last word): a trailing pass/exchange now highlights nothing, so the board no longer lights up the opponent's old word after our own empty move. - Make the highlight event-driven: refreshed only on a real game event (open/refresh, opponent move, our own committed move) and dismissed the moment composing starts, so recalling a just-placed tile never re-triggers it. - Localize non-play move-history labels via new move.* catalog keys (pass/exchange/resign/timeout); the label printed the raw English action. - Clamp the zoomed board's pan at its edge (overscroll-behavior: none), removing the native rubber-band past the content. Tests: lastMoveCells unit coverage (trailing pass/exchange -> empty), i18n RU label assertions, an e2e overscroll-contract check on the zoomed viewport.
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
// 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<string> {
|
|
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}`));
|
|
}
|