Stage 7 (wip): UI shell, libs, mock transport, screens (lobby->game), e2e smoke
- plain Svelte 5 + TS + Vite (no SvelteKit); CSS-token design system (Telegram-ready), hash router, IndexedDB session - pure libs: domain model, premium/value maps ported from solver, board replay, placement state machine, i18n en/ru - in-memory mock transport + seed data; pnpm start runs lobby->active game->board with no backend - board: pointer-drag + tap placement, MakeMove (popup / 1s-hold commit), two-state zoom, blank chooser, exchange, hint, word-check, chat - Playwright smoke (mock) green; svelte-check clean; mock bundle ~37 KB gzip
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// Board premium layout and tile values — ported verbatim from the engine source of
|
||||
// truth, scrabble-solver/rules/rules.go (standardBoard / eruditBoard, and the
|
||||
// per-variant value tables). These are NOT transmitted on the wire (StateView has
|
||||
// no board), so the client renders them locally. A Vitest parity test pins the
|
||||
// layout against the known geometry. Keep this in lockstep with the solver.
|
||||
|
||||
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' ? 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 };
|
||||
}
|
||||
|
||||
// --- tile values (points shown on the tile face); blank scores 0 ---
|
||||
|
||||
// English Latin a..z (rules.go English()).
|
||||
const enValues =
|
||||
'a1 b3 c3 d2 e1 f4 g2 h4 i1 j8 k5 l1 m3 n1 o1 p3 q10 r1 s1 t1 u1 v4 w4 x8 y4 z10';
|
||||
// Russian а..я incl. ё (rules.go RussianScrabble()).
|
||||
const ruValues =
|
||||
'а1 б3 в1 г3 д2 е1 ё3 ж5 з5 и1 й4 к2 л2 м2 н1 о1 п2 р1 с1 т1 у2 ф10 х5 ц5 ч5 ш8 щ10 ъ10 ы4 ь3 э8 ю8 я3';
|
||||
// Эрудит а..я incl. ё=0 (rules.go Erudit()).
|
||||
const eruditValues =
|
||||
'а1 б3 в2 г3 д2 е1 ё0 ж5 з5 и1 й2 к2 л2 м2 н1 о1 п2 р2 с2 т2 у3 ф10 х5 ц10 ч5 ш10 щ10 ъ10 ы5 ь5 э10 ю10 я3';
|
||||
|
||||
// Split each "letter+value" token into its letter (all but trailing digits) and its
|
||||
// integer value (the trailing digits).
|
||||
function valueTable(spec: string): Map<string, number> {
|
||||
const m = new Map<string, number>();
|
||||
for (const pair of spec.trim().split(/\s+/)) {
|
||||
const match = pair.match(/^(.+?)(\d+)$/);
|
||||
if (!match) continue;
|
||||
m.set(match[1].toUpperCase(), Number(match[2]));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
const VALUES: Record<Variant, Map<string, number>> = {
|
||||
english: valueTable(enValues),
|
||||
russian: valueTable(ruValues),
|
||||
erudit: valueTable(eruditValues),
|
||||
};
|
||||
|
||||
/** tileValue returns the point value of a concrete letter; a blank ("?") is 0. */
|
||||
export function tileValue(variant: Variant, letter: string): number {
|
||||
if (!letter || letter === '?') return 0;
|
||||
return VALUES[variant]?.get(letter.toUpperCase()) ?? 0;
|
||||
}
|
||||
|
||||
/** alphabet lists a variant's letters in dictionary order (used by the blank chooser). */
|
||||
export function alphabet(variant: Variant): string[] {
|
||||
return [...VALUES[variant].keys()];
|
||||
}
|
||||
Reference in New Issue
Block a user