Stage 13: alphabet on the wire (UI alphabet-agnostic, TODO-4)
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Live play now exchanges per-variant alphabet indices instead of concrete letters (rack out; submit-play, evaluate, exchange, word-check in). The client caches each variant's (index, letter, value) table behind StateRequest.include_alphabet and renders the rack and blank chooser from it, dropping the hardcoded value/alphabet tables. History, the durable journal and GCG stay decoded concrete characters (ARCHITECTURE §9.1, unchanged). - pkg/fbs: new AlphabetEntry + PlayTile; StateView.rack -> [ubyte] + alphabet; StateRequest.include_alphabet; SubmitPlay/Eval tiles -> [PlayTile]; Exchange tiles + CheckWord word -> [ubyte] (committed Go + TS regenerated). - engine: AlphabetTable + a cached per-variant codec (LetterForIndex/EncodeRack/ DecodeTiles/DecodeWord) + BlankIndex sentinel; Go parity test. - backend server edge maps index<->letter (new thin game.Service.GameVariant); game.Service domain methods, engine.Game and the robot keep one letter-based play path. The gateway forwards indices verbatim (no alphabet table). - ui: lib/alphabet.ts in-memory cache; codec encodes/decodes indices; premiums.ts is geometry-only; the mock seeds a fixture table; the UI normalises display to upper case (codec + cache), leaving placement/board/checkword unchanged. Parity moved to the Go engine.AlphabetTable test; premiums.ts loses its value tables. Discharges TODO-4.
This commit is contained in:
+52
-22
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Builder, ByteBuffer, type Offset } from 'flatbuffers';
|
||||
import * as fb from '../gen/fbs/scrabblefb';
|
||||
import { indexForLetter, letterForIndex, setAlphabet, type AlphabetEntryWire } from './alphabet';
|
||||
import type { PlacedTile } from './client';
|
||||
import type {
|
||||
AccountRef,
|
||||
@@ -37,14 +38,15 @@ import type {
|
||||
|
||||
// --- request encoders ---
|
||||
|
||||
function buildTile(b: Builder, t: PlacedTile): Offset {
|
||||
const letter = b.createString(t.letter);
|
||||
fb.TileRecord.startTileRecord(b);
|
||||
fb.TileRecord.addRow(b, t.row);
|
||||
fb.TileRecord.addCol(b, t.col);
|
||||
fb.TileRecord.addLetter(b, letter);
|
||||
fb.TileRecord.addBlank(b, t.blank);
|
||||
return fb.TileRecord.endTileRecord(b);
|
||||
// buildPlayTile encodes one to-place tile by its alphabet index (Stage 13); a placed blank
|
||||
// carries its designated letter's index with blank set.
|
||||
function buildPlayTile(b: Builder, t: PlacedTile, variant: Variant): Offset {
|
||||
fb.PlayTile.startPlayTile(b);
|
||||
fb.PlayTile.addRow(b, t.row);
|
||||
fb.PlayTile.addCol(b, t.col);
|
||||
fb.PlayTile.addLetter(b, indexForLetter(variant, t.letter));
|
||||
fb.PlayTile.addBlank(b, t.blank);
|
||||
return fb.PlayTile.endPlayTile(b);
|
||||
}
|
||||
|
||||
function finish(b: Builder, root: Offset): Uint8Array {
|
||||
@@ -62,17 +64,23 @@ export function encodeGameAction(gameId: string): Uint8Array {
|
||||
return finish(b, fb.GameActionRequest.endGameActionRequest(b));
|
||||
}
|
||||
|
||||
export function encodeStateRequest(gameId: string): Uint8Array {
|
||||
export function encodeStateRequest(gameId: string, includeAlphabet: boolean): Uint8Array {
|
||||
const b = new Builder(64);
|
||||
const gid = b.createString(gameId);
|
||||
fb.StateRequest.startStateRequest(b);
|
||||
fb.StateRequest.addGameId(b, gid);
|
||||
fb.StateRequest.addIncludeAlphabet(b, includeAlphabet);
|
||||
return finish(b, fb.StateRequest.endStateRequest(b));
|
||||
}
|
||||
|
||||
export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
||||
export function encodeSubmitPlay(
|
||||
gameId: string,
|
||||
dir: 'H' | 'V',
|
||||
tiles: PlacedTile[],
|
||||
variant: Variant,
|
||||
): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const tileOffs = tiles.map((t) => buildTile(b, t));
|
||||
const tileOffs = tiles.map((t) => buildPlayTile(b, t, variant));
|
||||
const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs);
|
||||
const gid = b.createString(gameId);
|
||||
const d = b.createString(dir);
|
||||
@@ -83,9 +91,14 @@ export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTi
|
||||
return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b));
|
||||
}
|
||||
|
||||
export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
||||
export function encodeEval(
|
||||
gameId: string,
|
||||
dir: 'H' | 'V',
|
||||
tiles: PlacedTile[],
|
||||
variant: Variant,
|
||||
): Uint8Array {
|
||||
const b = new Builder(256);
|
||||
const tileOffs = tiles.map((t) => buildTile(b, t));
|
||||
const tileOffs = tiles.map((t) => buildPlayTile(b, t, variant));
|
||||
const vec = fb.EvalRequest.createTilesVector(b, tileOffs);
|
||||
const gid = b.createString(gameId);
|
||||
const d = b.createString(dir);
|
||||
@@ -96,10 +109,12 @@ export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]):
|
||||
return finish(b, fb.EvalRequest.endEvalRequest(b));
|
||||
}
|
||||
|
||||
export function encodeExchange(gameId: string, tiles: string[]): Uint8Array {
|
||||
export function encodeExchange(gameId: string, tiles: string[], variant: Variant): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const offs = tiles.map((s) => b.createString(s));
|
||||
const vec = fb.ExchangeRequest.createTilesVector(b, offs);
|
||||
const vec = fb.ExchangeRequest.createTilesVector(
|
||||
b,
|
||||
tiles.map((l) => indexForLetter(variant, l)),
|
||||
);
|
||||
const gid = b.createString(gameId);
|
||||
fb.ExchangeRequest.startExchangeRequest(b);
|
||||
fb.ExchangeRequest.addGameId(b, gid);
|
||||
@@ -107,13 +122,16 @@ export function encodeExchange(gameId: string, tiles: string[]): Uint8Array {
|
||||
return finish(b, fb.ExchangeRequest.endExchangeRequest(b));
|
||||
}
|
||||
|
||||
export function encodeCheckWord(gameId: string, word: string): Uint8Array {
|
||||
export function encodeCheckWord(gameId: string, word: string, variant: Variant): Uint8Array {
|
||||
const b = new Builder(128);
|
||||
const vec = fb.CheckWordRequest.createWordVector(
|
||||
b,
|
||||
Array.from(word).map((ch) => indexForLetter(variant, ch)),
|
||||
);
|
||||
const gid = b.createString(gameId);
|
||||
const w = b.createString(word);
|
||||
fb.CheckWordRequest.startCheckWordRequest(b);
|
||||
fb.CheckWordRequest.addGameId(b, gid);
|
||||
fb.CheckWordRequest.addWord(b, w);
|
||||
fb.CheckWordRequest.addWord(b, vec);
|
||||
return finish(b, fb.CheckWordRequest.endCheckWordRequest(b));
|
||||
}
|
||||
|
||||
@@ -188,7 +206,8 @@ function s(v: string | null): string {
|
||||
}
|
||||
|
||||
function decodeTile(t: fb.TileRecord): Tile {
|
||||
return { row: t.row(), col: t.col(), letter: s(t.letter()), blank: t.blank() };
|
||||
// The wire keeps the move journal in the solver's lower case; the UI renders upper case.
|
||||
return { row: t.row(), col: t.col(), letter: s(t.letter()).toUpperCase(), blank: t.blank() };
|
||||
}
|
||||
|
||||
function decodeSeat(v: fb.SeatView): Seat {
|
||||
@@ -229,7 +248,7 @@ function decodeMove(m: fb.MoveRecord): MoveRecord {
|
||||
if (t) tiles.push(decodeTile(t));
|
||||
}
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)));
|
||||
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)).toUpperCase());
|
||||
return {
|
||||
player: m.player(),
|
||||
action: s(m.action()),
|
||||
@@ -280,8 +299,19 @@ export function decodeProfile(buf: Uint8Array): Profile {
|
||||
export function decodeStateView(buf: Uint8Array): StateView {
|
||||
const v = fb.StateView.getRootAsStateView(new ByteBuffer(buf));
|
||||
const g = v.game();
|
||||
const variant = (g ? s(g.variant()) : 'english') as Variant;
|
||||
// Cache the alphabet table when the server included it (a per-variant cache miss), then
|
||||
// decode the index rack to display letters with it (Stage 13).
|
||||
if (v.alphabetLength() > 0) {
|
||||
const entries: AlphabetEntryWire[] = [];
|
||||
for (let i = 0; i < v.alphabetLength(); i++) {
|
||||
const e = v.alphabet(i);
|
||||
if (e) entries.push({ index: e.index(), letter: s(e.letter()), value: e.value() });
|
||||
}
|
||||
setAlphabet(variant, entries);
|
||||
}
|
||||
const rack: string[] = [];
|
||||
for (let i = 0; i < v.rackLength(); i++) rack.push(s(v.rack(i)));
|
||||
for (let i = 0; i < v.rackLength(); i++) rack.push(letterForIndex(variant, v.rack(i) ?? 0));
|
||||
return {
|
||||
game: g ? decodeGameView(g) : emptyGame(),
|
||||
seat: v.seat(),
|
||||
|
||||
Reference in New Issue
Block a user