End of Game Allow Extended Play #11

Closed
opened 2026-08-24 11:42:35 +00:00 by Jesse.Markowitz · 2 comments
Owner

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 Play 2026-08-25 16:43:06 +00:00
Author
Owner

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.

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.
Author
Owner

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.

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.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Jesse.Markowitz/station-master#11