// Per-variant alphabet table cache. The client is alphabet-agnostic: it caches // each variant's (index, letter, value) table — sent by the server on a per-variant cache // miss, behind game.state's include_alphabet flag — and renders the rack and the blank // chooser with it while live play exchanges bare alphabet indices on the wire. Letters are // stored upper-cased for display (the rest of the UI works in upper case) and index lookups // are case-insensitive. A blank rides as the sentinel index 255 in a rack/exchange list; a // placed blank instead travels as its designated letter's index with a separate blank flag. import type { Variant } from './model'; /** BLANK_INDEX is the wire sentinel for a blank tile in a rack/exchange index list. */ export const BLANK_INDEX = 255; /** BLANK_LETTER is the glyph a blank rack tile decodes to (matches placement.BLANK). */ const BLANK_LETTER = '?'; /** AlphabetEntryWire is one raw alphabet row as received from the wire or a mock fixture. */ export interface AlphabetEntryWire { index: number; letter: string; value: number; } interface Table { letters: string[]; // by index, upper-cased values: number[]; // by index indexByLetter: Map; // upper-cased letter -> index } const cache = new Map(); /** setAlphabet caches a variant's table, upper-casing letters for display. */ export function setAlphabet(variant: Variant, entries: AlphabetEntryWire[]): void { let size = 0; for (const e of entries) size = Math.max(size, e.index + 1); const letters = new Array(size).fill(''); const values = new Array(size).fill(0); const indexByLetter = new Map(); for (const e of entries) { const up = e.letter.toUpperCase(); letters[e.index] = up; values[e.index] = e.value; indexByLetter.set(up, e.index); } cache.set(variant, { letters, values, indexByLetter }); } /** hasAlphabet reports whether a variant's table is cached (so the client can skip asking * the server to resend it). */ export function hasAlphabet(variant: Variant): boolean { return cache.has(variant); } /** alphabetLetters lists a variant's letters (upper-cased) for the blank chooser and the * word-check input filter; empty when the table is not yet cached. */ export function alphabetLetters(variant: Variant): string[] { return cache.get(variant)?.letters.slice() ?? []; } /** letterForIndex maps a wire rack index to its display letter: the blank sentinel maps to * "?", an unknown index to "". */ export function letterForIndex(variant: Variant, index: number): string { if (index === BLANK_INDEX) return BLANK_LETTER; return cache.get(variant)?.letters[index] ?? ''; } /** valueForLetter returns a tile's point value; a blank ("?") and an unknown letter score 0. */ export function valueForLetter(variant: Variant, letter: string): number { if (!letter || letter === BLANK_LETTER) return 0; const t = cache.get(variant); if (!t) return 0; const i = t.indexByLetter.get(letter.toUpperCase()); return i === undefined ? 0 : t.values[i]; } /** alphabetValues returns a variant's tile values indexed by alphabet letter index, or an * empty array when the table is not yet cached. The local move validator scores with it. */ export function alphabetValues(variant: Variant): readonly number[] { return cache.get(variant)?.values ?? []; } /** indexForLetter maps a display letter to its wire index; a blank ("?") maps to the blank * sentinel. It throws when the letter is outside the cached alphabet — a placement bug, not * user input (the UI constrains every entry point to the variant's alphabet). */ export function indexForLetter(variant: Variant, letter: string): number { if (letter === BLANK_LETTER) return BLANK_INDEX; const i = cache.get(variant)?.indexByLetter.get(letter.toUpperCase()); if (i === undefined) throw new Error(`alphabet: no index for "${letter}" in ${variant}`); return i; }