// 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 = { 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).