A game you come back to has not begun
Reported by Jesse, 2026-08-30: "when you are continuing the saved game out of that screen, do not post a message that says 'The game has begun.' … it needs to say 'The game has resumed.'" A restored game draws exactly like a dealt one — mid-Day, mid-phase, with a log already several turns deep — and solitaire said nothing at all to tell the two apart. It flashes "The game has resumed — Day 3, Stage 7" now, on both ways back in: the setup screen's Continue saved game, and a bare reload that restores the save. THE SAME LINE WAS WRONG ON THE MULTIPLAYER SIDE, IN THE OTHER DIRECTION. noteFirstFrame guards on firstFrameSeen, which is per page-load — so re-entering a game this browser already held a seat in, by reloading mid-game or picking it out of the lobby's list, announced that the game had BEGUN to somebody who had been playing it for an hour. beginRemote carries whether this is a rejoin now, and the line reads "resumed" when it is. Both halves are pinned, including that a brand-new game does not claim to be a resume — an announcement that fires either way says nothing. Stays in the unshipped v0.7.9. 878 tests pass, eleven new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
788e5f2eec
commit
bb1b661211
+35
-7
@@ -506,7 +506,7 @@ const lobbyHandlers = {
|
||||
const record = readStore().games[gameId];
|
||||
if (!record) return;
|
||||
if (record.stage === 'game' && record.seat !== undefined) {
|
||||
beginRemote({ ...record, seat: record.seat });
|
||||
beginRemote({ ...record, seat: record.seat }, true);
|
||||
return;
|
||||
}
|
||||
// Still seated in a lobby that had not started: the stream puts us back on the seating screen,
|
||||
@@ -533,6 +533,8 @@ const HANDOFF_STALL_MS = 8000;
|
||||
let handoffOpenedAt = 0;
|
||||
let handoffStall: number | null = null;
|
||||
let firstFrameSeen = false;
|
||||
/** Set when this page entered a game it was already seated in, so the first frame says so. */
|
||||
let rejoiningRemote = false;
|
||||
|
||||
function setText(id: string, text: string): void {
|
||||
const el = document.getElementById(id);
|
||||
@@ -612,7 +614,7 @@ function noteDayEnd(f: Frame): void {
|
||||
* nothing saying this was the game just set up. `#phasenote` cannot help: it announces a CHANGE of
|
||||
* phase, and there is no previous phase to have changed from.
|
||||
*/
|
||||
function noteFirstFrame(f: Frame): void {
|
||||
function noteFirstFrame(f: Frame, rejoining = false): void {
|
||||
if (firstFrameSeen || isLocal(session)) return;
|
||||
firstFrameSeen = true;
|
||||
const held = Date.now() - handoffOpenedAt;
|
||||
@@ -621,14 +623,34 @@ function noteFirstFrame(f: Frame): void {
|
||||
window.setTimeout(
|
||||
() => {
|
||||
closeHandoff();
|
||||
/**
|
||||
* "BEGUN" IS ONLY TRUE ONCE. `firstFrameSeen` is per page-load, so re-entering a game this
|
||||
* browser already holds a seat in — a reload mid-game, or picking it out of the lobby's list
|
||||
* — announced that the game had begun, to a player who had been playing it for an hour.
|
||||
* Reported for the solitaire side by Jesse, 2026-08-30; the same line was wrong here.
|
||||
*/
|
||||
flashAnnounce(
|
||||
`The game has begun — ${type} · ${f.players.length} players · Day ${f.day}, Stage ${f.stage}`,
|
||||
`The game has ${rejoining ? 'resumed' : 'begun'} — ${type} · ${f.players.length} players · ` +
|
||||
`Day ${f.day}, Stage ${f.stage}`,
|
||||
);
|
||||
},
|
||||
Math.max(0, HANDOFF_BEAT_MS - held),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* COMING BACK TO A GAME, said out loud — the solitaire counterpart to `noteFirstFrame`.
|
||||
*
|
||||
* A restored game draws exactly like a dealt one: mid-Day, mid-phase, with a log already several
|
||||
* turns deep. Nothing distinguished "this is the game you left" from "this is a game that has just
|
||||
* started", and the multiplayer path had the opposite problem — it announced that the game had
|
||||
* BEGUN to a player rejoining one (Jesse, 2026-08-30: "do not post a message that says 'The game
|
||||
* has begun.' … it needs to say 'The game has resumed.'").
|
||||
*/
|
||||
function announceResumed(f: Frame): void {
|
||||
flashAnnounce(`The game has resumed — Day ${f.day}, Stage ${f.stage}`);
|
||||
}
|
||||
|
||||
/** Toggles the three mutually-exclusive top-level screens `play.html` defines — `#lobby` (Phase 4),
|
||||
* `#gameui` (the board, whether local or remote), and `#solitairesetup` (asked before the first
|
||||
* solitaire deal, the same way `#lobby` is asked before the first multiplayer one — Jesse,
|
||||
@@ -646,7 +668,7 @@ function showScreen(which: 'lobby' | 'gameui' | 'solitairesetup'): void {
|
||||
* earlier visit. Either way the token is what makes reconnection work (`lobby-and-sessions.md` §1),
|
||||
* so it is always written back here before anything else happens.
|
||||
*/
|
||||
function beginRemote(ready: LobbyReady): void {
|
||||
function beginRemote(ready: LobbyReady, rejoining = false): void {
|
||||
saveRemote({ token: ready.token, gameId: ready.gameId, gameCode: ready.gameCode, seat: ready.seat, stage: 'game' });
|
||||
gameCode = ready.gameCode;
|
||||
showScreen('gameui');
|
||||
@@ -656,6 +678,7 @@ function beginRemote(ready: LobbyReady): void {
|
||||
// banner (`#presence`), and it holds a beat so the game visibly begins.
|
||||
openHandoff();
|
||||
session = createRemoteSession(ready.token, ready.seat, abandonRemote);
|
||||
rejoiningRemote = rejoining;
|
||||
applyCapabilities();
|
||||
// A LocalSession has data the instant it is constructed; a RemoteSession does not — its first
|
||||
// real Frame only exists once the SSE connection's first push arrives, so the first render waits
|
||||
@@ -742,7 +765,7 @@ function start(): void {
|
||||
// session reports a dead game through `abandonRemote`, which lands in the lobby.
|
||||
const remembered = wantsSolitaire ? null : loadRemote();
|
||||
if (remembered && remembered.stage === 'game' && remembered.seat !== undefined) {
|
||||
beginRemote({ ...remembered, seat: remembered.seat });
|
||||
beginRemote({ ...remembered, seat: remembered.seat }, true);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
@@ -799,13 +822,17 @@ function start(): void {
|
||||
const seed = requested !== null ? Number(requested) || 1 : Math.floor(Math.random() * 1e9);
|
||||
const local = createLocalSession(seed, solitaireDefaults(gameOptionsFromUrl(params)));
|
||||
session = local;
|
||||
if (saved && requested === null) local.restore(saved);
|
||||
const restored = Boolean(saved) && requested === null;
|
||||
if (saved && restored) local.restore(saved);
|
||||
|
||||
applyCapabilities();
|
||||
// Every render goes through the session, so the page redraws whenever the game says it changed —
|
||||
// which is what a remote session will use to push. Locally it fires on each accepted intent.
|
||||
session.subscribe(render);
|
||||
render();
|
||||
// Coming back to a game is not the same event as being dealt one, and the board looks identical
|
||||
// either way — mid-Day, mid-phase, with a log already deep (Jesse, 2026-08-30).
|
||||
if (restored) announceResumed(session.view());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -934,7 +961,7 @@ function rulesSummary(f: Frame): string {
|
||||
function render(): void {
|
||||
const f = session.view();
|
||||
const menu = session.menu();
|
||||
noteFirstFrame(f);
|
||||
noteFirstFrame(f, rejoiningRemote);
|
||||
|
||||
// Which squares the selected card or track piece may go on. Highlighting them is what turns the
|
||||
// coordinate list into a board: you pick the thing, then click where it goes.
|
||||
@@ -2224,6 +2251,7 @@ function runSolitaireSetup(params: URLSearchParams, hasSave = false, live: Frame
|
||||
? () => {
|
||||
showScreen('gameui');
|
||||
render();
|
||||
announceResumed(session.view());
|
||||
}
|
||||
: () => void (location.search = '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user