The extension question was hidden behind the results screen (Gitea#11)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f2c87b6871
commit
e035dda5a3
+26
-2
@@ -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
|
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.
|
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
|
### Three wording and layout fixes
|
||||||
|
|
||||||
- **The collision entries** on all three screens now read "The game ends immediately and results in
|
- **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
|
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".
|
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
|
870 tests pass, three of them new; one existing test asserted the opposite of the collision ruling
|
||||||
says so where it was reversed.
|
above and says so where it was reversed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
"waiting on Carol" is legible once Carol has closed her laptop, are still unanswered. Worth being
|
||||||
the first thing the next play session does.
|
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"
|
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`
|
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
|
would supply it, "emitted per Stage, so a run of them is exactly the streak you describe". That
|
||||||
|
|||||||
@@ -1554,6 +1554,33 @@ function showResults(f: Frame): void {
|
|||||||
const body = document.getElementById('resultsbody');
|
const body = document.getElementById('resultsbody');
|
||||||
if (!dlg || !body) return;
|
if (!dlg || !body) return;
|
||||||
body.innerHTML = resultsHtml(f);
|
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
|
// A redraw can arrive while it is open — `showModal` throws on an already-open dialog rather
|
||||||
// than doing nothing (the same trap `noteDayEnd` documents).
|
// than doing nothing (the same trap `noteDayEnd` documents).
|
||||||
if (!dlg.open) dlg.showModal();
|
if (!dlg.open) dlg.showModal();
|
||||||
|
|||||||
@@ -1099,10 +1099,19 @@ ul.blocked li{padding:2px 0}
|
|||||||
<!-- THE END-OF-GAME RESULTS (Gitea#16). Filled by `resultsHtml` and opened from `renderEnding`,
|
<!-- THE END-OF-GAME RESULTS (Gitea#16). Filled by `resultsHtml` and opened from `renderEnding`,
|
||||||
which puts it up once per ending unasked and leaves a button to reopen it. Reopenable matters:
|
which puts it up once per ending unasked and leaves a button to reopen it. Reopenable matters:
|
||||||
Gitea#11 lets a table play past the end, and continuing must not cost you the results screen. -->
|
Gitea#11 lets a table play past the end, and continuing must not cost you the results screen. -->
|
||||||
|
<!-- THE EXTENSION QUESTION IS ASKED HERE, not only behind this dialog (Gitea#11 + #16).
|
||||||
|
This opens ITSELF at every ending, and it is modal — so `renderEnding`'s "play one more Day"
|
||||||
|
buttons, which it writes into `#actions`, sit underneath it. A player saw a results screen whose
|
||||||
|
only control was Close and reasonably concluded the game was over: reported by Jesse
|
||||||
|
2026-08-30, "Solitaire game ended. I did not have an option to extend the game by a day."
|
||||||
|
Gitea#11 was verified over the HTTP API, which renders no dialog, so the browser never was.
|
||||||
|
The two extension buttons are hidden unless the game is actually awaiting a vote. -->
|
||||||
<dialog id="resultsdlg" aria-labelledby="rs-title">
|
<dialog id="resultsdlg" aria-labelledby="rs-title">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<div id="resultsbody"></div>
|
<div id="resultsbody"></div>
|
||||||
<menu class="ng-buttons">
|
<menu class="ng-buttons">
|
||||||
|
<button value="extend-yes" id="rs-extend-yes" type="submit" hidden>Play One More Day</button>
|
||||||
|
<button value="extend-no" id="rs-extend-no" type="submit" hidden>End the Game Here</button>
|
||||||
<button value="ok" id="rs-ok" type="submit">Close</button>
|
<button value="ok" id="rs-ok" type="submit">Close</button>
|
||||||
</menu>
|
</menu>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3362,6 +3362,36 @@ describe('the end-of-game results screen (Gitea#16)', () => {
|
|||||||
return snapshot(s, [], null);
|
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', () => {
|
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 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.
|
// the one moment the game has the player's whole attention.
|
||||||
|
|||||||
Reference in New Issue
Block a user