Files
scrabble-game/ui/src/lib/localgame/board.ts
T
Ilia Denisov e4cf143e9f
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
feat(offline): local game engine (Phase B1)
The offline vs_ai game engine — a faithful TS port of backend/internal/engine that
drives a whole local game with no backend. Composes with the move generator (#188) and
robot strategy (#189) from Phase A; not yet wired into the UI (Phase B2/B3).

- ui/src/lib/localgame/ruleset.ts: static per-variant tile values / bag counts / blank
  count, mirrored from rules.go (offline scoring is self-contained; online uses the
  server alphabet). Pinned by ruleset.parity.test.ts against a Go fixture.
- bag.ts: the tile bag (fill from counts/blanks, draw-from-end, return+reshuffle) on a
  deterministic in-house PRNG — a game replays from its seed, not bit-identical to a
  server game (per plan).
- board.ts: the mutable board, satisfying the validator/generator read view + set().
- engine.ts: LocalGame — deal / play (reusing validate.ts) / pass / exchange / resign,
  scoreless(6) & out-of-tiles end detection, end-of-game rack penalties, winner; mirrors
  game.go. The end-game math is exported as pure functions, pinned against the Go engine
  (engine.parity.test.ts, 9 constructed positions).
- engine.test.ts: a full-loop smoke — two robots play a whole vs_ai game to completion
  via generateMoves + decide, and it is reproducible from the seed.
- backend: movegen now dumps the per-variant rulesets; a new in-package engine emitter
  (endfixture_test.go, env-gated) produces the end-game golden.

Pure additive library code; no runtime behavior change (unused at runtime, bundle unchanged).
2026-07-06 08:02:05 +02:00

45 lines
1.6 KiB
TypeScript

// The mutable local-game board: a 15x15 row-major grid of placed tiles (alphabet-index letter
// plus a blank flag). It satisfies the read view the validator and generator need (GenBoard,
// which extends validate.ts's Board) and adds set() so the engine can apply a play. The board is
// alphabet-agnostic — a cell's letter is a variant index, meaningful with the variant's ruleset.
import { BOARD_SIZE } from '../premiums';
import type { Cell } from '../dict/validate';
import type { GenBoard } from '../dict/generate';
/** LocalBoard is the engine's in-memory board; it reads as a GenBoard and applies plays via set. */
export class LocalBoard implements GenBoard {
readonly rows = BOARD_SIZE;
readonly cols = BOARD_SIZE;
private readonly grid: (Cell | null)[];
private count = 0;
constructor() {
this.grid = new Array<Cell | null>(BOARD_SIZE * BOARD_SIZE).fill(null);
}
inBounds(row: number, col: number): boolean {
return row >= 0 && row < this.rows && col >= 0 && col < this.cols;
}
filled(row: number, col: number): boolean {
return this.inBounds(row, col) && this.grid[row * this.cols + col] !== null;
}
cellAt(row: number, col: number): Cell {
return this.grid[row * this.cols + col]!;
}
isEmpty(): boolean {
return this.count === 0;
}
/** set places a tile at (row, col). It assumes the square was empty (a play only lays tiles
* on empty squares), keeping the filled count exact for isEmpty. */
set(row: number, col: number, letter: number, blank: boolean): void {
const i = row * this.cols + col;
if (this.grid[i] === null) this.count++;
this.grid[i] = { letter, blank };
}
}