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:
@@ -1,28 +1,44 @@
|
||||
import { Builder, ByteBuffer } from 'flatbuffers';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as fb from '../gen/fbs/scrabblefb';
|
||||
import { BLANK_INDEX, setAlphabet } from './alphabet';
|
||||
import {
|
||||
decodeFriendList,
|
||||
decodeGameList,
|
||||
decodeInvitation,
|
||||
decodeLinkResult,
|
||||
decodeSession,
|
||||
decodeStateView,
|
||||
decodeStats,
|
||||
encodeCheckWord,
|
||||
encodeExchange,
|
||||
encodeStateRequest,
|
||||
encodeSubmitPlay,
|
||||
encodeTarget,
|
||||
} from './codec';
|
||||
|
||||
describe('codec', () => {
|
||||
it('encodes a SubmitPlayRequest the gateway can read', () => {
|
||||
const buf = encodeSubmitPlay('g1', 'H', [
|
||||
{ row: 7, col: 7, letter: 'A', blank: false },
|
||||
{ row: 7, col: 8, letter: 'B', blank: true },
|
||||
it('encodes a SubmitPlayRequest with alphabet indices (Stage 13)', () => {
|
||||
setAlphabet('english', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
{ index: 1, letter: 'b', value: 3 },
|
||||
]);
|
||||
// A placed blank carries its designated letter's index with the blank flag set.
|
||||
const buf = encodeSubmitPlay(
|
||||
'g1',
|
||||
'H',
|
||||
[
|
||||
{ row: 7, col: 7, letter: 'A', blank: false },
|
||||
{ row: 7, col: 8, letter: 'B', blank: true },
|
||||
],
|
||||
'english',
|
||||
);
|
||||
const r = fb.SubmitPlayRequest.getRootAsSubmitPlayRequest(new ByteBuffer(buf));
|
||||
expect(r.gameId()).toBe('g1');
|
||||
expect(r.dir()).toBe('H');
|
||||
expect(r.tilesLength()).toBe(2);
|
||||
expect(r.tiles(0)?.letter()).toBe('A');
|
||||
expect(r.tiles(0)?.letter()).toBe(0);
|
||||
expect(r.tiles(1)?.letter()).toBe(1);
|
||||
expect(r.tiles(1)?.blank()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -214,3 +230,72 @@ describe('codec', () => {
|
||||
expect(inv.variant).toBe('english');
|
||||
});
|
||||
});
|
||||
|
||||
// Stage 13: the live play loop exchanges alphabet indices, mapped through the per-variant
|
||||
// table cached in lib/alphabet. Each test seeds the cache it needs (setAlphabet replaces
|
||||
// the whole table), so they are independent of order.
|
||||
describe('codec — alphabet on the wire (Stage 13)', () => {
|
||||
it('encodes an ExchangeRequest as alphabet indices, blank as the sentinel', () => {
|
||||
setAlphabet('english', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
{ index: 1, letter: 'b', value: 3 },
|
||||
]);
|
||||
const r = fb.ExchangeRequest.getRootAsExchangeRequest(
|
||||
new ByteBuffer(encodeExchange('g1', ['A', '?'], 'english')),
|
||||
);
|
||||
expect(r.tilesLength()).toBe(2);
|
||||
expect(r.tiles(0)).toBe(0);
|
||||
expect(r.tiles(1)).toBe(BLANK_INDEX);
|
||||
});
|
||||
|
||||
it('encodes a CheckWordRequest as alphabet indices', () => {
|
||||
setAlphabet('english', [
|
||||
{ index: 0, letter: 'a', value: 1 },
|
||||
{ index: 2, letter: 'c', value: 3 },
|
||||
{ index: 19, letter: 't', value: 1 },
|
||||
]);
|
||||
const r = fb.CheckWordRequest.getRootAsCheckWordRequest(
|
||||
new ByteBuffer(encodeCheckWord('g1', 'CAT', 'english')),
|
||||
);
|
||||
expect(r.wordLength()).toBe(3);
|
||||
expect([r.word(0), r.word(1), r.word(2)]).toEqual([2, 0, 19]);
|
||||
});
|
||||
|
||||
it('carries the include_alphabet flag on a StateRequest', () => {
|
||||
const on = fb.StateRequest.getRootAsStateRequest(new ByteBuffer(encodeStateRequest('g1', true)));
|
||||
expect(on.gameId()).toBe('g1');
|
||||
expect(on.includeAlphabet()).toBe(true);
|
||||
const off = fb.StateRequest.getRootAsStateRequest(new ByteBuffer(encodeStateRequest('g1', false)));
|
||||
expect(off.includeAlphabet()).toBe(false);
|
||||
});
|
||||
|
||||
it('caches the alphabet table from a StateView and decodes the index rack to letters', () => {
|
||||
const b = new Builder(128);
|
||||
const la = b.createString('a');
|
||||
fb.AlphabetEntry.startAlphabetEntry(b);
|
||||
fb.AlphabetEntry.addIndex(b, 0);
|
||||
fb.AlphabetEntry.addLetter(b, la);
|
||||
fb.AlphabetEntry.addValue(b, 1);
|
||||
const ea = fb.AlphabetEntry.endAlphabetEntry(b);
|
||||
const lb = b.createString('b');
|
||||
fb.AlphabetEntry.startAlphabetEntry(b);
|
||||
fb.AlphabetEntry.addIndex(b, 1);
|
||||
fb.AlphabetEntry.addLetter(b, lb);
|
||||
fb.AlphabetEntry.addValue(b, 3);
|
||||
const eb = fb.AlphabetEntry.endAlphabetEntry(b);
|
||||
const alpha = fb.StateView.createAlphabetVector(b, [ea, eb]);
|
||||
const rack = fb.StateView.createRackVector(b, [0, BLANK_INDEX]);
|
||||
fb.StateView.startStateView(b);
|
||||
fb.StateView.addSeat(b, 0);
|
||||
fb.StateView.addRack(b, rack);
|
||||
fb.StateView.addBagLen(b, 10);
|
||||
fb.StateView.addHintsRemaining(b, 0);
|
||||
fb.StateView.addAlphabet(b, alpha);
|
||||
b.finish(fb.StateView.endStateView(b));
|
||||
|
||||
// No GameView on the buffer, so decode falls back to the default variant 'english';
|
||||
// the embedded table is cached under it and the rack [0, blank] decodes to letters.
|
||||
const sv = decodeStateView(b.asUint8Array());
|
||||
expect(sv.rack).toEqual(['A', '?']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user