When game ends allow players to continue playing if they wish... don't force end
When game ends allow players to continue playing if they wish... don't force end
Jesse.Markowitz
changed title from End of Game Summary to End of Game Allow Extended Play (and update Summary/Win screen)2026-08-24 13:13:57 +00:00
Jesse.Markowitz
changed title from End of Game Allow Extended Play (and update Summary/Win screen) to End of Game Allow Extended Play2026-08-25 16:43:06 +00:00
Not in v0.7.1 / v0.4.9g — noting where the code stands, and the rules questions this needs answered
before it can be built, because they change the implementation substantially.
Where a game ends today
advance.ts sets status = 'finished' in two places, and both are real endings rather than a screen
being shown:
checkVictory — fires the moment daysElapsed >= config.days. It sets the status, then
decides the outcome (revenue floor missed → loss; co-op → win with no winner; otherwise the
highest revenue wins).
the §3.4 collision check — a per-Day or total collision breach, competitive and co-op only.
Once status is finished, the client's action menu returns early (main.ts) and offers exactly
one button, new game. Nothing is disabled or hidden — the game genuinely stopped.
The questions I can't answer for you
"Don't force end" is clear as an instruction and ambiguous as a rule, and each of these leads
somewhere different:
Does the outcome freeze, or re-evaluate? The game already recorded a winner and a reason at
the moment it ended. If play continues and someone overtakes the leader, did they win? If the
outcome freezes, extended play is an exhibition and the result stands. If it re-evaluates, the
Day count stops being what decides the game and the ending becomes "whenever everyone stops",
which needs its own rule.
Does the target still mean anything? Revenue keeps accruing either way. The status block
currently reads "N of TARGET · D Days left", and past the last Day both halves of that sentence
are false.
What ends the extended game? Somebody has to be able to stop. A button that ends it for
everyone, an agreement, or another Day limit — in multiplayer this is a table decision, not one
player's.
Does a collision ending extend too? A game that ended because the railroad was unsafe is a
different case from one that ran out of Days, and "carry on regardless" may not be wanted there.
One constraint worth knowing before choosing
The multiplayer server does not resume finished games. On restart it skips any game whose status
is finished — they are left on disk for replay and never loaded back into memory (src/server/index.ts).
So extended play cannot simply be "the client keeps letting you click past the end": either the game
stays active with the outcome recorded separately, or the server has to learn a third state. That
is the difference between a client-side change and a persistence-format one, so it is worth deciding
early.
My suggestion, if it helps: keep status active and add a recorded-but-not-final outcome, freeze the
result at the moment the last Day ended, and let extended play be explicitly an exhibition. That
answers 1, 2 and the server constraint at once, and leaves 3 as the only genuine rules decision. But
this is your call, not mine.
Related: #16 is the summary screen half, which was split out of the original issue. Whatever is
decided here, the summary has to stay reachable after play continues — otherwise continuing costs
you the results screen.
Not in v0.7.1 / v0.4.9g — noting where the code stands, and the rules questions this needs answered
before it can be built, because they change the implementation substantially.
### Where a game ends today
`advance.ts` sets `status = 'finished'` in two places, and both are real endings rather than a screen
being shown:
- **`checkVictory`** — fires the moment `daysElapsed >= config.days`. It sets the status, then
decides the outcome (revenue floor missed → loss; co-op → win with no winner; otherwise the
highest revenue wins).
- **the §3.4 collision check** — a per-Day or total collision breach, competitive and co-op only.
Once `status` is `finished`, the client's action menu returns early (`main.ts`) and offers exactly
one button, `new game`. Nothing is disabled or hidden — the game genuinely stopped.
### The questions I can't answer for you
"Don't force end" is clear as an instruction and ambiguous as a rule, and each of these leads
somewhere different:
1. **Does the outcome freeze, or re-evaluate?** The game already recorded a winner and a reason at
the moment it ended. If play continues and someone overtakes the leader, did they win? If the
outcome freezes, extended play is an exhibition and the result stands. If it re-evaluates, the
Day count stops being what decides the game and the ending becomes "whenever everyone stops",
which needs its own rule.
2. **Does the target still mean anything?** Revenue keeps accruing either way. The status block
currently reads "N of TARGET · D Days left", and past the last Day both halves of that sentence
are false.
3. **What ends the extended game?** Somebody has to be able to stop. A button that ends it for
everyone, an agreement, or another Day limit — in multiplayer this is a table decision, not one
player's.
4. **Does a collision ending extend too?** A game that ended because the railroad was unsafe is a
different case from one that ran out of Days, and "carry on regardless" may not be wanted there.
### One constraint worth knowing before choosing
**The multiplayer server does not resume finished games.** On restart it skips any game whose status
is `finished` — they are left on disk for replay and never loaded back into memory (`src/server/index.ts`).
So extended play cannot simply be "the client keeps letting you click past the end": either the game
stays `active` with the outcome recorded separately, or the server has to learn a third state. That
is the difference between a client-side change and a persistence-format one, so it is worth deciding
early.
My suggestion, if it helps: keep `status` active and add a recorded-but-not-final outcome, freeze the
result at the moment the last Day ended, and let extended play be explicitly an exhibition. That
answers 1, 2 and the server constraint at once, and leaves 3 as the only genuine rules decision. But
this is your call, not mine.
Related: #16 is the summary screen half, which was split out of the original issue. Whatever is
decided here, the summary has to stay reachable after play continues — otherwise continuing costs
you the results screen.
Built in v0.7.3, commit 45580d8 — "a game that asks before it ends, and a results screen
worth reading". Not on the 0.4.9 playtest line, per your call that it may be complete and this is
not a fix people mid-playtest need.
The rule as built
Your answers, 2026-08-28/29, and what each turned into:
The official result is settled at the original game length. In a five-Day game extended to
eight, the winner is whoever led at the end of Day 5. state.official is written at the first
ending and never rewritten; config.days is deliberately never touched, so "the winner is decided
at the original length" is a fact about the code rather than a comment on it. extraDays counts
the borrowed Days.
One Day at a time, and the question is put again at the end of it. Solitaire the player decides
alone; multiplayer is unanimous, and one refusal ends it immediately — nobody waits on somebody who
has already said no.
Days-based endings only.daysElapsed and revenueFloor offer another Day. A §3.4 collision
breach is final, and a breach during an extended Day ends play at once without touching the
recorded result: a railroad declared unsafe on Day 9 does not retract who won on Day 5.
Bots never lead. They agree only once every human has agreed. With no humans at the table their
own answer stands, which is no — so a bot-only game ends on the timetable it was dealt.
Why it could not be a client-side change
Three independent reasons, each ruling out the others' shortcuts:
apply.ts refused every intent once status left active.
The server never loads a finished game back into memory (src/server/index.ts).
A save is { seed, config, history } replayed through the engine. A "continue" the history
does not record did not happen — the extended game would evaporate on the next reload, Undo or
server restart.
So: a fourth status, awaitingExtension, and the vote is a real intent (game.extend).
Three bugs found while testing, all of which would have shipped
A saved game with a vote in it could not be resumed — NO_ACTOR. A history is a flat Intent[] with no seat written down; the replay derives who acted from the turn order. That works
for every other intent, mainline.clearance included, because there is exactly one seat it could
have been. Not here. game.extend now carries its voter, uniquely among intents, and the server
checks it against the seat it authenticated.
An all-bot game hung on the question for ever.driveBots loops on currentActor, which is
null the moment the game stops, so it cannot cast a vote; the bot-vote driver returned early when
there were no humans to follow, and nothing ever asked.
The balance harness became unbounded.randomBot picks uniformly among its legal options, so
once the two votes were among them it took another Day about half the time — and every seeded game
ran to playGame's 50,000-turn cap instead of a couple of hundred. test/sim.test.ts went from
under a second to never finishing. Fixed in the driver rather than in a policy, so the guarantee
holds for bots not yet written.
All three have regression tests. 832 tests pass, against 793 before this change.
Still open
Nobody has played this at a real table (TODO.md#35). The multiplayer vote has only been driven
through session.intent, never through two browsers — what a second player sees while waiting on a
first, and whether "waiting on Carol" is legible once Carol has closed her laptop, are both
unanswered. Worth being the first thing the next play session does.
The results screen half is #16, and it stays reachable after play continues — a button that reopens
it, so carrying on never costs you the results.
Built in **v0.7.3**, commit `45580d8` — "a game that asks before it ends, and a results screen
worth reading". Not on the 0.4.9 playtest line, per your call that it may be complete and this is
not a fix people mid-playtest need.
### The rule as built
Your answers, 2026-08-28/29, and what each turned into:
- **The official result is settled at the original game length.** In a five-Day game extended to
eight, the winner is whoever led at the end of Day 5. `state.official` is written at the first
ending and never rewritten; `config.days` is deliberately never touched, so "the winner is decided
at the original length" is a fact about the code rather than a comment on it. `extraDays` counts
the borrowed Days.
- **One Day at a time**, and the question is put again at the end of it. Solitaire the player decides
alone; multiplayer is unanimous, and one refusal ends it immediately — nobody waits on somebody who
has already said no.
- **Days-based endings only.** `daysElapsed` and `revenueFloor` offer another Day. A §3.4 collision
breach is final, and a breach *during* an extended Day ends play at once without touching the
recorded result: a railroad declared unsafe on Day 9 does not retract who won on Day 5.
- **Bots never lead.** They agree only once every human has agreed. With no humans at the table their
own answer stands, which is no — so a bot-only game ends on the timetable it was dealt.
### Why it could not be a client-side change
Three independent reasons, each ruling out the others' shortcuts:
1. `apply.ts` refused *every* intent once `status` left `active`.
2. The server never loads a `finished` game back into memory (`src/server/index.ts`).
3. **A save is `{ seed, config, history }` replayed through the engine.** A "continue" the history
does not record did not happen — the extended game would evaporate on the next reload, Undo or
server restart.
So: a fourth status, `awaitingExtension`, and the vote is a real intent (`game.extend`).
### Three bugs found while testing, all of which would have shipped
- **A saved game with a vote in it could not be resumed** — `NO_ACTOR`. A history is a flat
`Intent[]` with no seat written down; the replay derives who acted from the turn order. That works
for every other intent, `mainline.clearance` included, because there is exactly one seat it could
have been. Not here. `game.extend` now carries its voter, uniquely among intents, and the server
checks it against the seat it authenticated.
- **An all-bot game hung on the question for ever.** `driveBots` loops on `currentActor`, which is
null the moment the game stops, so it cannot cast a vote; the bot-vote driver returned early when
there were no humans to follow, and nothing ever asked.
- **The balance harness became unbounded.** `randomBot` picks uniformly among its legal options, so
once the two votes were among them it took another Day about half the time — and every seeded game
ran to `playGame`'s 50,000-turn cap instead of a couple of hundred. `test/sim.test.ts` went from
under a second to never finishing. Fixed in the driver rather than in a policy, so the guarantee
holds for bots not yet written.
All three have regression tests. 832 tests pass, against 793 before this change.
### Still open
**Nobody has played this at a real table** (`TODO.md` #35). The multiplayer vote has only been driven
through `session.intent`, never through two browsers — what a second player sees while waiting on a
first, and whether "waiting on Carol" is legible once Carol has closed her laptop, are both
unanswered. Worth being the first thing the next play session does.
The results screen half is #16, and it stays reachable after play continues — a button that reopens
it, so carrying on never costs you the results.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
When game ends allow players to continue playing if they wish... don't force end
End of Game Summaryto End of Game Allow Extended Play (and update Summary/Win screen)End of Game Allow Extended Play (and update Summary/Win screen)to End of Game Allow Extended PlayNot in v0.7.1 / v0.4.9g — noting where the code stands, and the rules questions this needs answered
before it can be built, because they change the implementation substantially.
Where a game ends today
advance.tssetsstatus = 'finished'in two places, and both are real endings rather than a screenbeing shown:
checkVictory— fires the momentdaysElapsed >= config.days. It sets the status, thendecides the outcome (revenue floor missed → loss; co-op → win with no winner; otherwise the
highest revenue wins).
Once
statusisfinished, the client's action menu returns early (main.ts) and offers exactlyone button,
new game. Nothing is disabled or hidden — the game genuinely stopped.The questions I can't answer for you
"Don't force end" is clear as an instruction and ambiguous as a rule, and each of these leads
somewhere different:
the moment it ended. If play continues and someone overtakes the leader, did they win? If the
outcome freezes, extended play is an exhibition and the result stands. If it re-evaluates, the
Day count stops being what decides the game and the ending becomes "whenever everyone stops",
which needs its own rule.
currently reads "N of TARGET · D Days left", and past the last Day both halves of that sentence
are false.
everyone, an agreement, or another Day limit — in multiplayer this is a table decision, not one
player's.
different case from one that ran out of Days, and "carry on regardless" may not be wanted there.
One constraint worth knowing before choosing
The multiplayer server does not resume finished games. On restart it skips any game whose status
is
finished— they are left on disk for replay and never loaded back into memory (src/server/index.ts).So extended play cannot simply be "the client keeps letting you click past the end": either the game
stays
activewith the outcome recorded separately, or the server has to learn a third state. Thatis the difference between a client-side change and a persistence-format one, so it is worth deciding
early.
My suggestion, if it helps: keep
statusactive and add a recorded-but-not-final outcome, freeze theresult at the moment the last Day ended, and let extended play be explicitly an exhibition. That
answers 1, 2 and the server constraint at once, and leaves 3 as the only genuine rules decision. But
this is your call, not mine.
Related: #16 is the summary screen half, which was split out of the original issue. Whatever is
decided here, the summary has to stay reachable after play continues — otherwise continuing costs
you the results screen.
Built in v0.7.3, commit
45580d8— "a game that asks before it ends, and a results screenworth reading". Not on the 0.4.9 playtest line, per your call that it may be complete and this is
not a fix people mid-playtest need.
The rule as built
Your answers, 2026-08-28/29, and what each turned into:
eight, the winner is whoever led at the end of Day 5.
state.officialis written at the firstending and never rewritten;
config.daysis deliberately never touched, so "the winner is decidedat the original length" is a fact about the code rather than a comment on it.
extraDayscountsthe borrowed Days.
alone; multiplayer is unanimous, and one refusal ends it immediately — nobody waits on somebody who
has already said no.
daysElapsedandrevenueFlooroffer another Day. A §3.4 collisionbreach is final, and a breach during an extended Day ends play at once without touching the
recorded result: a railroad declared unsafe on Day 9 does not retract who won on Day 5.
own answer stands, which is no — so a bot-only game ends on the timetable it was dealt.
Why it could not be a client-side change
Three independent reasons, each ruling out the others' shortcuts:
apply.tsrefused every intent oncestatusleftactive.finishedgame back into memory (src/server/index.ts).{ seed, config, history }replayed through the engine. A "continue" the historydoes not record did not happen — the extended game would evaporate on the next reload, Undo or
server restart.
So: a fourth status,
awaitingExtension, and the vote is a real intent (game.extend).Three bugs found while testing, all of which would have shipped
NO_ACTOR. A history is a flatIntent[]with no seat written down; the replay derives who acted from the turn order. That worksfor every other intent,
mainline.clearanceincluded, because there is exactly one seat it couldhave been. Not here.
game.extendnow carries its voter, uniquely among intents, and the serverchecks it against the seat it authenticated.
driveBotsloops oncurrentActor, which isnull the moment the game stops, so it cannot cast a vote; the bot-vote driver returned early when
there were no humans to follow, and nothing ever asked.
randomBotpicks uniformly among its legal options, soonce the two votes were among them it took another Day about half the time — and every seeded game
ran to
playGame's 50,000-turn cap instead of a couple of hundred.test/sim.test.tswent fromunder a second to never finishing. Fixed in the driver rather than in a policy, so the guarantee
holds for bots not yet written.
All three have regression tests. 832 tests pass, against 793 before this change.
Still open
Nobody has played this at a real table (
TODO.md#35). The multiplayer vote has only been driventhrough
session.intent, never through two browsers — what a second player sees while waiting on afirst, and whether "waiting on Carol" is legible once Carol has closed her laptop, are both
unanswered. Worth being the first thing the next play session does.
The results screen half is #16, and it stays reachable after play continues — a button that reopens
it, so carrying on never costs you the results.