d24a127406
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
Score and validate a tentative move on-device instead of a per-arrangement
network round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a new `conformance` CI job. The server stays
authoritative — submit_play re-validates — so the local result is an advisory
accelerator only; any cache miss, storage eviction or a bad-connection breaker
falls back to the network evaluate.
- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
(immutable) streaming the pinned per-game dawg.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: an IndexedDB blob cache (best-effort, storage.persist()) + a loader
(memory -> IndexedDB -> network, session-scoped bad-connection breaker) + a lobby
prefetch (your-turn first); an adapter over the existing premiums/alphabet; a
DictWarmup overlay while a cold dictionary loads (120ms flash-guard, 5s cap ->
network); a ?nolocal flag; the DebugPanel reset also clears the dict cache.
- parity: generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run
in CI against the release dictionaries.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
// Board premium layout — the 15x15 premium-square geometry, ported from the engine source
|
|
// of truth, scrabble-solver/rules/rules.go (standardBoard / eruditBoard). The board is not
|
|
// transmitted on the wire (StateView has no board), so the client renders the premiums
|
|
// locally; only the centre differs by variant. A Vitest parity test pins the geometry.
|
|
// Tile values and the alphabet come from the server-sent per-variant table (see
|
|
// lib/alphabet.ts), so this file is geometry only.
|
|
|
|
import type { Variant } from './model';
|
|
|
|
export const BOARD_SIZE = 15;
|
|
|
|
export type Premium = '' | 'TW' | 'DW' | 'TL' | 'DL';
|
|
|
|
// Legend (rules.go): T=triple word, D=double word, t=triple letter, d=double
|
|
// letter, .=plain, *=centre (a double word), +=centre with no premium.
|
|
const standardBoard = [
|
|
'T..d...T...d..T',
|
|
'.D...t...t...D.',
|
|
'..D...d.d...D..',
|
|
'd..D...d...D..d',
|
|
'....D.....D....',
|
|
'.t...t...t...t.',
|
|
'..d...d.d...d..',
|
|
'T..d...*...d..T',
|
|
'..d...d.d...d..',
|
|
'.t...t...t...t.',
|
|
'....D.....D....',
|
|
'd..D...d...D..d',
|
|
'..D...d.d...D..',
|
|
'.D...t...t...D.',
|
|
'T..d...T...d..T',
|
|
];
|
|
|
|
// Эрудит: the standard layout but a non-doubling centre ('+').
|
|
const eruditBoard = [
|
|
'T..d...T...d..T',
|
|
'.D...t...t...D.',
|
|
'..D...d.d...D..',
|
|
'd..D...d...D..d',
|
|
'....D.....D....',
|
|
'.t...t...t...t.',
|
|
'..d...d.d...d..',
|
|
'T..d...+...d..T',
|
|
'..d...d.d...d..',
|
|
'.t...t...t...t.',
|
|
'....D.....D....',
|
|
'd..D...d...D..d',
|
|
'..D...d.d...D..',
|
|
'.D...t...t...D.',
|
|
'T..d...T...d..T',
|
|
];
|
|
|
|
function template(variant: Variant): string[] {
|
|
return variant === 'erudit_ru' ? eruditBoard : standardBoard;
|
|
}
|
|
|
|
function premiumOf(ch: string): Premium {
|
|
switch (ch) {
|
|
case 'T':
|
|
return 'TW';
|
|
case 'D':
|
|
case '*':
|
|
return 'DW';
|
|
case 't':
|
|
return 'TL';
|
|
case 'd':
|
|
return 'DL';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/** premiumGrid returns the 15x15 premium layout for a variant (row-major). */
|
|
export function premiumGrid(variant: Variant): Premium[][] {
|
|
return template(variant).map((line) => Array.from(line, premiumOf));
|
|
}
|
|
|
|
/** centre returns the first-move anchor square (row, col). */
|
|
export function centre(variant: Variant): { row: number; col: number } {
|
|
const lines = template(variant);
|
|
for (let r = 0; r < lines.length; r++) {
|
|
const c = lines[r].search(/[*+]/);
|
|
if (c >= 0) return { row: r, col: c };
|
|
}
|
|
return { row: 7, col: 7 };
|
|
}
|
|
|
|
/** RACK_SIZE is the number of tiles drawn to a full rack — 7 for every variant. It sizes
|
|
* the all-tiles (bingo) bonus test in the local move preview. */
|
|
export const RACK_SIZE = 7;
|
|
|
|
/** BINGO is the all-tiles bonus per variant, ported from scrabble-solver/rules/rules.go:
|
|
* 50 for the Scrabble variants, 15 for Эрудит. The local move preview adds it when a play
|
|
* uses the whole rack. The dict conformance test pins these to the engine's rulesets. */
|
|
export const BINGO: Record<Variant, number> = {
|
|
scrabble_en: 50,
|
|
scrabble_ru: 50,
|
|
erudit_ru: 15,
|
|
};
|
|
|
|
// Tile values and the per-variant alphabet arrive from the server (lib/alphabet.ts); the
|
|
// board geometry, centre, rack size and bingo above are the static per-variant constants
|
|
// this module owns (all ported from scrabble-solver/rules/rules.go).
|