diff --git a/ui/src/lib/localgame/serialize.ts b/ui/src/lib/localgame/serialize.ts index 9471362..3be0d60 100644 --- a/ui/src/lib/localgame/serialize.ts +++ b/ui/src/lib/localgame/serialize.ts @@ -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. */ diff --git a/ui/src/lib/localgame/source.list.test.ts b/ui/src/lib/localgame/source.list.test.ts index 6d8fd46..df620b0 100644 --- a/ui/src/lib/localgame/source.list.test.ts +++ b/ui/src/lib/localgame/source.list.test.ts @@ -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)); diff --git a/ui/src/lib/localgame/source.ts b/ui/src/lib/localgame/source.ts index 35ef2e0..3c8f848 100644 --- a/ui/src/lib/localgame/source.ts +++ b/ui/src/lib/localgame/source.ts @@ -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, diff --git a/ui/src/screens/NewGame.svelte b/ui/src/screens/NewGame.svelte index 9e962d3..e3597c9 100644 --- a/ui/src/screens/NewGame.svelte +++ b/ui/src/screens/NewGame.svelte @@ -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' }, ], });