feat(offline): local game persistence + replay (Phase B2)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
Store an offline game durably and reconstruct it — the offline counterpart of the server's replay rehydration. Builds on the engine (#191); not wired into the UI yet (Phase B3). - serialize.ts: a LocalGameRecord (seed + rules + seat metadata + the alphabet-index move journal) and replayGame() — reconstruct a live LocalGame by seeding a fresh engine identically and replaying the journal. The board/bag/racks are not stored; they are deterministic from the seed and the replayed operations, so the record stays small. The journal is dictionary-independent (alphabet-index space, stable per variant). - store.ts: an IndexedDB store for local games (save/get/list/delete), mirroring lib/dict/store.ts — its own database, best-effort, guarded when IndexedDB is absent. - engine.ts: record the swapped tiles on an exchange (needed for exact replay) and expose the game's rule config. - serialize.test.ts: a round-trip — reconstruct a mid-game and a finished game by replay and assert the state (board/racks/bag/scores/turn/log) is identical. Pure additive library code; no runtime behavior change (bundle unchanged).
This commit is contained in:
@@ -35,7 +35,11 @@ export interface LocalMove {
|
||||
action: 'play' | 'pass' | 'exchange' | 'resign';
|
||||
dir?: Direction;
|
||||
tiles?: Placement[];
|
||||
/** ActionExchange only: number of tiles swapped. */
|
||||
count?: number;
|
||||
/** ActionExchange only: the swapped tiles (alphabet-index bytes, BLANK_INDEX for a blank),
|
||||
* recorded so the game can be reconstructed by replaying the journal (see localgame/serialize). */
|
||||
exchanged?: number[];
|
||||
score: number;
|
||||
total: number;
|
||||
}
|
||||
@@ -163,6 +167,7 @@ export class LocalGame {
|
||||
private readonly values: readonly number[];
|
||||
private readonly rackSize: number;
|
||||
private readonly dawg: Dawg;
|
||||
private readonly multipleWords: boolean;
|
||||
private readonly dropoutTiles: DropoutTiles;
|
||||
|
||||
private readonly board: LocalBoard;
|
||||
@@ -184,6 +189,7 @@ export class LocalGame {
|
||||
this.version = opts.version;
|
||||
this.seed = opts.seed;
|
||||
this.dawg = opts.dawg;
|
||||
this.multipleWords = opts.multipleWords;
|
||||
this.dropoutTiles = opts.dropoutTiles ?? 'remove';
|
||||
this.vrs = buildRuleset(opts.variant, opts.multipleWords);
|
||||
this.values = RULESETS[opts.variant].values;
|
||||
@@ -231,6 +237,11 @@ export class LocalGame {
|
||||
get winnerIndex(): number {
|
||||
return winner(this.over, this.reason, this.scores, this.resigned);
|
||||
}
|
||||
/** config returns the rule settings the game was created with — the bits, alongside the seed and
|
||||
* journal, needed to reconstruct it (see localgame/serialize). */
|
||||
get config(): { multipleWords: boolean; dropoutTiles: DropoutTiles } {
|
||||
return { multipleWords: this.multipleWords, dropoutTiles: this.dropoutTiles };
|
||||
}
|
||||
/** history returns a copy of the move log. */
|
||||
get history(): LocalMove[] {
|
||||
return this.log.map((m) => ({ ...m }));
|
||||
@@ -303,7 +314,7 @@ export class LocalGame {
|
||||
this.bag.return(tiles);
|
||||
this.scorelessRun++;
|
||||
|
||||
const rec: LocalMove = { player, action: 'exchange', count: tiles.length, score: 0, total: this.scores[player] };
|
||||
const rec: LocalMove = { player, action: 'exchange', count: tiles.length, exchanged: [...tiles], score: 0, total: this.scores[player] };
|
||||
this.log.push(rec);
|
||||
this.endTurnAfterScoreless();
|
||||
return rec;
|
||||
|
||||
Reference in New Issue
Block a user