Phase 2: async play-by-turn — the games ledger and turn signals

The lobby is now the front door to all your games. A "your games"
ledger lists every seat this browser holds — room code, whose turn it
is (including counteraction/discard/interrupt waits, which count as
your turn), the round, and how long since the last move — with
one-click resume, a forget control, and a green highlight when a game
waits on you. The server answers a token-validated myGames query with
per-seat summaries; the client polls every 45 seconds, so turns in
other rooms reach you wherever you are. Turn signals travel three
ways: the tab title flips to "● Your turn", the favicon grows a green
dot, and — opt-in via "notify me on my turn" — a browser notification
fires when a turn becomes yours anywhere. Landing in the app now
shows the ledger rather than teleporting into the last game;
mid-game socket drops still walk straight back to the table.
Verified live: two seeded games, ledger showing "gandalf's turn" and
YOUR TURN, one-click resume into the correct game with history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 23:53:13 -04:00
co-authored by Claude Fable 5
parent 7cfffb8ab5
commit bc8013863b
4 changed files with 294 additions and 16 deletions
+33
View File
@@ -140,6 +140,39 @@ export function runCommand(
return { events: result.events };
}
export interface GameSummary {
roomId: string;
name: PlayerId;
players: PlayerId[];
started: boolean;
finished: boolean;
winner: PlayerId | null;
activePlayerId: PlayerId | null;
yourTurn: boolean;
round: number | null;
lastMoveAt: string | null;
}
/** A seat-holder's one-line view of a room, for the lobby ledger. */
export function summarize(room: Room, playerId: PlayerId): GameSummary {
const s = room.state;
const active = s && s.phase === "playing" ? s.players[s.turn.activeIndex]!.id : null;
const waitingOn = s?.stack?.waitingOn ?? s?.pendingDiscard ?? s?.outOfTurnWindow?.playerId ?? null;
const turnHolder = waitingOn ?? active;
return {
roomId: room.id,
name: playerId,
players: [...room.players],
started: s !== null,
finished: s?.phase === "finished",
winner: s?.winner ?? null,
activePlayerId: active,
yourTurn: s?.phase === "playing" && turnHolder === playerId,
round: s?.turn.round ?? null,
lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : null,
};
}
export function viewForPlayer(room: Room, playerId: PlayerId): GameView | null {
return room.state ? viewFor(room.state, playerId) : null;
}