fix(offline): cold-boot offline from the persisted session + profile #199

Merged
developer merged 5 commits from feature/offline-boot into development 2026-07-06 14:25:11 +00:00
4 changed files with 22 additions and 3 deletions
Showing only changes of commit 2a045a5b37 - Show all commits
+3
View File
@@ -17,6 +17,9 @@ import type { Variant } from '../model';
export interface Seat {
kind: 'human' | 'robot';
name: string;
/** The player's real account id for a human seat, so the client identifies "you" and the correct
* turn exactly as in an online game; absent for the robot (a synthetic id is derived). */
accountId?: string;
}
/** LocalGameRecord is the durable form of one local game — everything needed to reconstruct it. */
+14
View File
@@ -51,6 +51,20 @@ describe('LocalSource.list', () => {
expect(list.every((g) => g.vsAi && g.status === 'active')).toBe(true);
});
it('carries the human seat account id into the GameView so the client identifies "you"', async () => {
const src = new LocalSource();
await src.create({
...opts('local:me', 3n),
seats: [
{ kind: 'human', name: 'You', accountId: 'acct-42' },
{ kind: 'robot', name: 'Robot' },
],
});
const [g] = await src.list();
expect(g.seats[0].accountId).toBe('acct-42'); // the human's real id, matched against the session
expect(g.seats[1].accountId).not.toBe('acct-42'); // the robot keeps a synthetic id
});
it('reconstructs a game persisted by a previous session (not in the source cache)', async () => {
// Seed the store via one source, then list from a fresh source whose in-memory cache is empty.
await new LocalSource().create(opts('local:old', 7n));
+2 -2
View File
@@ -120,7 +120,7 @@ export class LocalSource implements GameLoopSource {
const now = nowUnix();
const record = serializeGame(game, {
id: opts.id,
seats: opts.seats.map((s) => ({ kind: s.kind, name: s.name })),
seats: opts.seats.map((s) => ({ kind: s.kind, name: s.name, accountId: s.accountId })),
createdAtUnix: now,
updatedAtUnix: now,
robotLastMoveAtUnix: now,
@@ -341,7 +341,7 @@ export class LocalSource implements GameLoopSource {
const { game, record } = entry;
const seats: Seat[] = record.seats.map((s, i) => ({
seat: i,
accountId: `${LOCAL_ID_PREFIX}${s.kind}:${i}`,
accountId: s.accountId ?? `${LOCAL_ID_PREFIX}${s.kind}:${i}`,
displayName: s.name,
score: game.scoreOf(i),
hintsUsed: 0,
+3 -1
View File
@@ -74,7 +74,9 @@
seed: randomSeed(),
multipleWords: multipleWordsForRequest(v, multipleWords),
seats: [
{ kind: 'human', name: app.profile?.displayName ?? 'You' },
// The human seat carries the real account id so the game screen and the lobby identify
// "you" and the correct turn exactly as for an online game (the robot uses a synthetic id).
{ kind: 'human', name: app.profile?.displayName ?? 'You', accountId: app.session?.userId },
{ kind: 'robot', name: 'Robot' },
],
});