From e035dda5a38e1a417b6e839913499a9d2217b433 Mon Sep 17 00:00:00 2001 From: "Jesse.Markowitz" Date: Sun, 30 Aug 2026 01:26:03 -0400 Subject: [PATCH] The extension question was hidden behind the results screen (Gitea#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jesse, playing v0.7.8: "Solitaire game ended. I did not have an option to extend the game by a day." The engine and the Frame were right — checked before changing anything. A solitaire game at the end of its timetable reaches awaitingExtension with extensionVotes [null], and renderEnding writes "play one more Day" into #actions. It then opens #resultsdlg, which is MODAL, so those buttons were directly underneath a dialog whose only control was Close. The results dialog now carries the question itself, hidden unless a vote is pending: Play One More Day / End the Game Here, casting the same game.extend intent. The #actions buttons stay as the fallback once it is closed. TODO.md #35 recorded extended play as verified on phoenix.local — over the HTTP API, which renders no dialog. What was proven was that the server supports it, not that a player can reach it. Noted there. Rides along in the unshipped v0.7.9. 870 tests pass, one new. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX --- CHANGELOG.md | 28 ++++++++++++++++++++++++++-- TODO.md | 9 +++++++++ src/web/main.ts | 27 +++++++++++++++++++++++++++ src/web/play.html | 9 +++++++++ test/web.test.ts | 30 ++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e5fd8..590a1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,30 @@ when `start.seat !== seatOf(s, player)`, which cannot happen — so this is a la gameplay effect and nothing to re-measure. The permissive label described a permission a lone player was never being granted, and named an "any player" they have no contrast with. +### The extension question was hidden behind the results screen + +Jesse, 2026-08-30: "Solitaire game ended. I did not have an option to extend the game by a day." + +**The engine and the Frame were right the whole time** — checked before changing anything: a +solitaire game at the end of its timetable reaches `awaitingExtension` with `extensionVotes: [null]`, +and `isExtendable` covers both `revenueFloor` and `daysElapsed`. `renderActions` reaches +`renderEnding` for any non-active status, and `renderEnding` writes "play one more Day" and "end the +game here" into `#actions`. + +Then it opens `#resultsdlg`, **which is modal**. So the two buttons were rendered directly underneath +a dialog whose only control was Close, and the player read a results screen that offered nothing but +Close and concluded the game was over — which is precisely what it looked like. + +The results dialog now carries the question itself, hidden unless a vote is actually pending: +**Play One More Day** and **End the Game Here**, casting the same `game.extend` intent. The +`#actions` buttons stay, because they are what remains once the dialog is closed and what a player +who reopens it with "see the full results" comes back to. + +**Why this survived being "verified".** `TODO.md` #35 recorded extended play as checked on +`phoenix.local` — over the HTTP API, which renders no dialog. The browser path had never been run to +the end of a timetable. Same shape as the three attempts before it: the thing that was verified was +not the thing the player uses. + ### Three wording and layout fixes - **The collision entries** on all three screens now read "The game ends immediately and results in @@ -66,8 +90,8 @@ was never being granted, and named an "any player" they have no contrast with. cost, not a danger, and red would outrank the actual rules above it. The buttons say **Continue Existing Saved Game** and **Deal New Game** rather than "Continue saved game" and "Deal". -869 tests pass, two of them new; one existing test asserted the opposite of the ruling above and -says so where it was reversed. +870 tests pass, three of them new; one existing test asserted the opposite of the collision ruling +above and says so where it was reversed. --- diff --git a/TODO.md b/TODO.md index 860404e..a0c06c9 100644 --- a/TODO.md +++ b/TODO.md @@ -246,6 +246,15 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main "waiting on Carol" is legible once Carol has closed her laptop, are still unanswered. Worth being the first thing the next play session does. + **A HUMAN DID REACH IT ON 2026-08-30, AND IT WAS UNUSABLE — fixed in v0.7.9.** Jesse played a + solitaire game to the end and was never offered the extra Day. Nothing was wrong with the engine + or the Frame: `renderEnding` wrote the two buttons into `#actions` and then opened `#resultsdlg`, + which is **modal**, so the question sat underneath a dialog whose only control was Close. The + dialog asks it now. **The verification recorded above is exactly why this survived** — it was + driven over the HTTP API, which renders no dialog, so what was proven was that the SERVER + supports extended play, not that a player can reach it. Read that distinction into every "verified + on phoenix.local" line in this file. + 36. **There is no per-Stage "this train did not move" signal, so "longest an engine sat on a siding" cannot be answered.** Gitea#16 asks for it and the comment on that issue said `trainStoodStill` would supply it, "emitted per Stage, so a run of them is exactly the streak you describe". That diff --git a/src/web/main.ts b/src/web/main.ts index 2c2cafd..a4d3b77 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -1554,6 +1554,33 @@ function showResults(f: Frame): void { const body = document.getElementById('resultsbody'); if (!dlg || !body) return; body.innerHTML = resultsHtml(f); + + /** + * ASK THE EXTENSION QUESTION ON THE THING THAT IS ACTUALLY IN FRONT OF THE PLAYER. + * + * This dialog opens itself at every ending and it is MODAL, so `renderEnding`'s own "play one more + * Day" buttons — written into `#actions` — are behind it. The player read a results screen offering + * nothing but Close and concluded the game was over, which is exactly what it looked like (Jesse, + * 2026-08-30). Gitea#11 was verified over the HTTP API, where there is no dialog to be behind. + * + * The buttons in `#actions` stay, and are still correct: they are what remains after this is + * closed, and what a player who reopened the results with "see the full results" comes back to. + * Voting from either place submits the same intent. + */ + const yes = document.getElementById('rs-extend-yes') as HTMLButtonElement | null; + const no = document.getElementById('rs-extend-no') as HTMLButtonElement | null; + const asking = f.status === 'awaitingExtension' && f.extensionVotes[f.viewer] === null; + if (yes && no) { + yes.hidden = !asking; + no.hidden = !asking; + if (asking) { + // `method="dialog"` closes it on click; the vote rides along. Assigned every time rather than + // once, because `f` is a fresh Frame on each ending. + yes.onclick = () => void session.submit({ type: 'game.extend', player: f.viewer, agree: true }); + no.onclick = () => void session.submit({ type: 'game.extend', player: f.viewer, agree: false }); + } + } + // A redraw can arrive while it is open — `showModal` throws on an already-open dialog rather // than doing nothing (the same trap `noteDayEnd` documents). if (!dlg.open) dlg.showModal(); diff --git a/src/web/play.html b/src/web/play.html index 7ae102c..c10e338 100644 --- a/src/web/play.html +++ b/src/web/play.html @@ -1099,10 +1099,19 @@ ul.blocked li{padding:2px 0} +
+ +
diff --git a/test/web.test.ts b/test/web.test.ts index f87ee8e..32177a6 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -3362,6 +3362,36 @@ describe('the end-of-game results screen (Gitea#16)', () => { return snapshot(s, [], null); }; + it('asks the extension question ON the results dialog, not only behind it (Gitea#11)', () => { + /** + * REPORTED BY JESSE 2026-08-30: "Solitaire game ended. I did not have an option to extend the + * game by a day." + * + * The engine and the Frame were right all along — a solitaire game at the end of its timetable + * reaches `awaitingExtension` with an uncast vote, asserted below. What went wrong is that + * `renderEnding` writes its two buttons into `#actions` and then opens `#resultsdlg`, which is + * MODAL: the question was underneath a dialog whose only control was Close. Gitea#11 was + * verified over the HTTP API, which renders no dialog, so the browser path never was. + */ + const f = finished(); + assert.equal(f.status, 'awaitingExtension', 'a solitaire game no longer pauses to ask'); + assert.equal(f.extensionVotes[f.viewer], null, 'the viewer has somehow already voted'); + + const html = readFileSync(join(dist, 'play.html'), 'utf8'); + for (const id of ['rs-extend-yes', 'rs-extend-no']) { + assert.ok(html.includes(`id="${id}"`), `the results dialog cannot ask: no #${id}`); + } + + // And they are wired: shown only while a vote is pending, and casting the same intent the + // `#actions` buttons do. Read from the source, since the dialog needs a DOM to drive. + const src = readFileSync(join(root, 'src/web/main.ts'), 'utf8'); + const fn = /function showResults\(f: Frame\): void \{[\s\S]*?\n\}/.exec(src)?.[0] ?? ''; + assert.ok(fn !== '', 'showResults moved and this test cannot see it'); + assert.match(fn, /awaitingExtension/, 'the dialog does not know the game is asking'); + assert.match(fn, /'game\.extend'/, 'the dialog offers no way to answer'); + assert.match(fn, /yes\.hidden = !asking/, 'the buttons are not hidden on a game that is over'); + }); + it('never prints a raw enum at the player', () => { // The bug the issue opens on: `GAME OVER — revenueFloor` is an internal identifier, shown at // the one moment the game has the player's whole attention.