There's already a minimal status that provides some minor detail.
GAME OVER — revenueFloor
final Revenue 0 against a target of 20
This would be a full end-of-game results. It should detail what the rules of the game were, what the actual results were, why did the game end?
Out of days?
Too many collisions on a day?
Too many collisions total?
What was the result of the game?
What was the revenue score per player?
If the game was solitary or co-op?
If there was a total victory?
If there was a multiplayer game, non-co-op, which player won?
What were the scores for all of the other players?
Any additional interesting information. I don't know if we keep statistics on:
what collisions were afterwards that we could report here
total number of trains that made it through the division
total number of loads loaded and unloaded within the overall division. Maybe create badges for anything interesting that happened. So a player who completes an entire game where every train that passed through did some switching on, gets a switching master badge. There should be a whole conversation brainstorming session on what are the things that might be interesting for people to be aware of at the end of the game for stuff that happened longest engine sat on a siding might be one. But I'm sure there's lots and lots of other things to be looked into.
This should be built as a first pass with everything that's already known, easy to calculate, and throw it up there.
There should be a second pass where we look for what are the ways to make it really exciting and interesting and extra stuff.
There's already a minimal status that provides some minor detail.
GAME OVER — revenueFloor
final Revenue 0 against a target of 20
This would be a full end-of-game results. It should detail what the rules of the game were, what the actual results were, why did the game end?
- Out of days?
- Too many collisions on a day?
- Too many collisions total?
- What was the result of the game?
- What was the revenue score per player?
- If the game was solitary or co-op?
- If there was a total victory?
- If there was a multiplayer game, non-co-op, which player won?
- What were the scores for all of the other players?
- Any additional interesting information. I don't know if we keep statistics on:
- what collisions were afterwards that we could report here
- total number of trains that made it through the division
- total number of loads loaded and unloaded within the overall division. Maybe create badges for anything interesting that happened. So a player who completes an entire game where every train that passed through did some switching on, gets a switching master badge. There should be a whole conversation brainstorming session on what are the things that might be interesting for people to be aware of at the end of the game for stuff that happened longest engine sat on a siding might be one. But I'm sure there's lots and lots of other things to be looked into.
This should be built as a first pass with everything that's already known, easy to calculate, and throw it up there.
There should be a second pass where we look for what are the ways to make it really exciting and interesting and extra stuff.
Splitting this out was the right call — this half is well-defined and mostly assembled already.
Answering the open questions in the description, in particular "I don't know if we keep statistics on".
What the four lines you quoted actually are
GAME OVER — revenueFloor is not a message; it is the raw enum value being printed. main.ts
interpolates outcome.reason straight into the page, so a player reads an internal identifier. The
possible values are daysElapsed, collisionFloor, revenueFloor on main, plus targetReached
on the 0.4.9 line, which still has a first-to-target victory that main dropped. That alone is worth
fixing whatever else this issue becomes — it answers your "why did the game end?" for free, once each
reason has a sentence.
Most of the first pass already exists
The Day-end dialog built for #10 assembles nearly this exact thing from the Frame — standings sorted
by revenue with the viewer marked, the target reported against combined revenue, the Days elapsed and
the pace. Pointing it at the finished state rather than the rollover is a small piece of work, and
the two screens should share one implementation so they cannot disagree.
Available on the Frame with no new tracking at all: mode, the full rule set that was in play
(houseRules, and on main the victory dials), per-player revenue, the winner, the target, Days elapsed, and the reason. That covers the rules-of-the-game, result, per-player scores,
solitaire/co-op, and which player won — most of your bullet list.
Collisions are the exception and they differ by line: main tracks collisionsToday and collisionsTotal against configurable per-Day and total limits, so "too many collisions on a day"
and "too many collisions total" are both reportable there. The 0.4.9 line has no collision scoring at
all, so that section simply has nothing to say on the playtest build.
On statistics: nothing is being kept, and nothing needs to be
The engine keeps no running counters beyond revenue and collisions — but it emits 49 distinct event
types, and a save is a replay. The stored game is { seed, config, history }; the board and the
whole event stream are recomputed from it. So any statistic can be counted after the fact, by
replaying and tallying — including for games that have already been played and saved, which means
this does not have to be designed correctly up front.
Events that answer your list directly:
trains through the Division — trainCompleted
loads made up and broken — loadStarted, loadAdvanced, loadCompleted, unloadBegan, unloadCompleted
passenger work — passengersBoarded, passengersDetrained
collisions afterwards — trainsDestroyed
longest an engine sat — trainStoodStill, which is emitted per Stage, so a run of them is
exactly the "sat on a siding" streak you describe
And for badge material, the vocabulary is richer than the scoring is: flyingSwitch, dispatchBonusUsed, officeUpgraded, facilityUnjammed, expediteFault, trainHeld, trainDiverted, secondSectionOrdered, clearanceRequested / clearanceGiven. Your "switching
master" badge — every train that passed through did some switching — is trainCompleted joined
against carsCoupled / carsDropped per train number.
Suggested shape for the first pass
Everything above except badges, sharing the #10 dialog's implementation, plus a sentence per outcome
reason so nobody reads revenueFloor again. A stat block computed by replaying the finished game's
own history, which needs no engine change and no new persistence. That leaves the brainstorm you
describe entirely to the second pass, where it belongs — and because the stats are derived rather
than recorded, a second pass can add any of them retroactively to games already played.
Related: #11 is the extended-play half. Whichever way that is ruled, this screen needs to stay
reachable after play continues.
Splitting this out was the right call — this half is well-defined and mostly assembled already.
Answering the open questions in the description, in particular "I don't know if we keep statistics on".
### What the four lines you quoted actually are
`GAME OVER — revenueFloor` is not a message; it is the raw enum value being printed. `main.ts`
interpolates `outcome.reason` straight into the page, so a player reads an internal identifier. The
possible values are `daysElapsed`, `collisionFloor`, `revenueFloor` on `main`, plus `targetReached`
on the 0.4.9 line, which still has a first-to-target victory that main dropped. That alone is worth
fixing whatever else this issue becomes — it answers your "why did the game end?" for free, once each
reason has a sentence.
### Most of the first pass already exists
The Day-end dialog built for #10 assembles nearly this exact thing from the Frame — standings sorted
by revenue with the viewer marked, the target reported against combined revenue, the Days elapsed and
the pace. Pointing it at the finished state rather than the rollover is a small piece of work, and
the two screens should share one implementation so they cannot disagree.
Available on the Frame with no new tracking at all: **mode**, **the full rule set that was in play**
(`houseRules`, and on main the victory dials), **per-player revenue**, **the winner**, **the target**,
**Days elapsed**, and **the reason**. That covers the rules-of-the-game, result, per-player scores,
solitaire/co-op, and which player won — most of your bullet list.
Collisions are the exception and they differ by line: `main` tracks `collisionsToday` and
`collisionsTotal` against configurable per-Day and total limits, so "too many collisions on a day"
and "too many collisions total" are both reportable there. The 0.4.9 line has no collision scoring at
all, so that section simply has nothing to say on the playtest build.
### On statistics: nothing is being kept, and nothing needs to be
The engine keeps no running counters beyond revenue and collisions — **but it emits 49 distinct event
types, and a save is a replay**. The stored game is `{ seed, config, history }`; the board and the
whole event stream are recomputed from it. So any statistic can be counted after the fact, by
replaying and tallying — including for games that have **already been played and saved**, which means
this does not have to be designed correctly up front.
Events that answer your list directly:
- **trains through the Division** — `trainCompleted`
- **loads made up and broken** — `loadStarted`, `loadAdvanced`, `loadCompleted`, `unloadBegan`,
`unloadCompleted`
- **passenger work** — `passengersBoarded`, `passengersDetrained`
- **collisions afterwards** — `trainsDestroyed`
- **longest an engine sat** — `trainStoodStill`, which is emitted per Stage, so a run of them is
exactly the "sat on a siding" streak you describe
And for badge material, the vocabulary is richer than the scoring is: `flyingSwitch`,
`dispatchBonusUsed`, `officeUpgraded`, `facilityUnjammed`, `expediteFault`, `trainHeld`,
`trainDiverted`, `secondSectionOrdered`, `clearanceRequested` / `clearanceGiven`. Your "switching
master" badge — every train that passed through did some switching — is `trainCompleted` joined
against `carsCoupled` / `carsDropped` per train number.
### Suggested shape for the first pass
Everything above except badges, sharing the #10 dialog's implementation, plus a sentence per outcome
reason so nobody reads `revenueFloor` again. A stat block computed by replaying the finished game's
own history, which needs no engine change and no new persistence. That leaves the brainstorm you
describe entirely to the second pass, where it belongs — and because the stats are derived rather
than recorded, a second pass can add any of them retroactively to games already played.
Related: #11 is the extended-play half. Whichever way that is ruled, this screen needs to stay
reachable after play continues.
Built in v0.7.3, commit 45580d8, as the first pass you asked for. Not on the 0.4.9 playtest
line, per your call.
The four lines you quoted are gone
GAME OVER — revenueFloor was never a message — it was outcome.reason, an internal enum
interpolated straight into the page at the one moment the game has the player's whole attention.
Every reason now has a sentence with this game's own numbers in it, which answers your "why did the
game end?" outright.
What the screen reports
Everything from your list that the game already knew, plus the statistics it did not:
why it ended, in a sentence; the result and the winner; standings, every player; combined Revenue against the target; collisions, but only in a game actually scored on them
the rules that were in play — mode, timetable, revenue floor, collision limits, pay rates,
where Extras may start, whether trains may be discarded, optional rules
who did what — per player: Revenue, loads, unloads, passenger work, cards played, collisions
the railroad — trains through the Division and how many of them did any switching en route,
loads made up and broken, passengers boarded and detrained, cars coupled and set out, Extras run,
second sections, flying switches, offices upgraded, facilities unjammed, trains held and diverted,
expedite faults, dispatch bonuses, clearances allowed of those asked, and trains destroyed with the
cars they took with them
It shares the Day-end dialog's standings, target and collision blocks rather than reimplementing
them — two screens reporting the same game must not be able to disagree. It opens itself once at each
ending and leaves a button to reopen it, so #11's extended play never costs you the results.
"I don't know if we keep statistics on…"
Nothing was being kept. Now state.tally is, folded from the event stream rather than recorded —
hooked at applyIntent and at advance, because reduce never sees the phase driver's events and
those are exactly the interesting ones (trainCompleted, trainsDestroyed). Nothing in the rules
reads it, so adding a counter is always safe; it rides the Frame, so multiplayer gets the same
numbers as solitaire from one implementation. The test that matters asserts exactly once: it
plays real games, counts the event stream independently, and checks the tally against that count, so
a fold hooked twice or nowhere fails whatever the seed.
One thing I could not build, and it is a correction to my own earlier comment
I said here that trainStoodStill "is emitted per Stage, so a run of them is exactly the 'sat on a
siding' streak you describe". That was wrong, and I found it only by reading advance.ts while
building the tally rather than trusting the comment.
It fires once in a game, and only for a train whose profile sets stopEarnsPoint — the X18
Circus, the one card in the deck that pays for standing still — with tray.stopPointClaimed making
sure it can never fire twice. I built the streak, watched it render 1 Stage at (0,0), and took it
out again. The engine has no per-Stage "this train did not move" signal at all, so "longest an
engine sat on a siding" needs one before it can be answered. TODO.md#36 records what it would take
(a new per-Stage event, or sampling live state on the Stage boundary — the tally cannot do the latter
today, because it folds a batch of events afteradvance has mutated past the moment they
describe). What is reported instead is the Circus set-up itself, which is a real thing that happened.
The second pass is left to you, deliberately
No badges. You asked for "a whole conversation brainstorming session on what are the things that might
be interesting", and that is where they belong — TODO.md#33. The raw material is already being
kept: trainsCompletedWithWork is exactly your switching-master join (every train that passed
through did some switching), and the vocabulary is there for the rest.
And the part worth knowing before that conversation: because the statistics are derived rather than
recorded, a second pass can add any of them retroactively to games already played and saved. A save
is a replay, so nothing has to be designed correctly up front.
Built in **v0.7.3**, commit `45580d8`, as the first pass you asked for. Not on the 0.4.9 playtest
line, per your call.
### The four lines you quoted are gone
`GAME OVER — revenueFloor` was never a message — it was `outcome.reason`, an internal enum
interpolated straight into the page at the one moment the game has the player's whole attention.
Every reason now has a sentence with this game's own numbers in it, which answers your "why did the
game end?" outright.
### What the screen reports
Everything from your list that the game already knew, plus the statistics it did not:
- **why it ended**, in a sentence; **the result and the winner**; **standings**, every player;
**combined Revenue against the target**; **collisions**, but only in a game actually scored on them
- **the rules that were in play** — mode, timetable, revenue floor, collision limits, pay rates,
where Extras may start, whether trains may be discarded, optional rules
- **who did what** — per player: Revenue, loads, unloads, passenger work, cards played, collisions
- **the railroad** — trains through the Division *and how many of them did any switching en route*,
loads made up and broken, passengers boarded and detrained, cars coupled and set out, Extras run,
second sections, flying switches, offices upgraded, facilities unjammed, trains held and diverted,
expedite faults, dispatch bonuses, clearances allowed of those asked, and trains destroyed with the
cars they took with them
It shares the Day-end dialog's standings, target and collision blocks rather than reimplementing
them — two screens reporting the same game must not be able to disagree. It opens itself once at each
ending and leaves a button to reopen it, so #11's extended play never costs you the results.
### "I don't know if we keep statistics on…"
Nothing was being kept. Now `state.tally` is, **folded from the event stream rather than recorded** —
hooked at `applyIntent` and at `advance`, because `reduce` never sees the phase driver's events and
those are exactly the interesting ones (`trainCompleted`, `trainsDestroyed`). Nothing in the rules
reads it, so adding a counter is always safe; it rides the `Frame`, so multiplayer gets the same
numbers as solitaire from one implementation. The test that matters asserts **exactly once**: it
plays real games, counts the event stream independently, and checks the tally against that count, so
a fold hooked twice or nowhere fails whatever the seed.
### One thing I could not build, and it is a correction to my own earlier comment
I said here that `trainStoodStill` "is emitted per Stage, so a run of them is exactly the 'sat on a
siding' streak you describe". **That was wrong**, and I found it only by reading `advance.ts` while
building the tally rather than trusting the comment.
It fires **once in a game, and only for a train whose profile sets `stopEarnsPoint`** — the X18
Circus, the one card in the deck that pays for standing still — with `tray.stopPointClaimed` making
sure it can never fire twice. I built the streak, watched it render `1 Stage at (0,0)`, and took it
out again. The engine has **no per-Stage "this train did not move" signal at all**, so "longest an
engine sat on a siding" needs one before it can be answered. `TODO.md` #36 records what it would take
(a new per-Stage event, or sampling live state on the Stage boundary — the tally cannot do the latter
today, because it folds a batch of events *after* `advance` has mutated past the moment they
describe). What is reported instead is the Circus set-up itself, which is a real thing that happened.
### The second pass is left to you, deliberately
No badges. You asked for "a whole conversation brainstorming session on what are the things that might
be interesting", and that is where they belong — `TODO.md` #33. The raw material is already being
kept: `trainsCompletedWithWork` is exactly your switching-master join (every train that passed
through did some switching), and the vocabulary is there for the rest.
And the part worth knowing before that conversation: **because the statistics are derived rather than
recorded, a second pass can add any of them retroactively to games already played and saved.** A save
is a replay, so nothing has to be designed correctly up front.
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.
There's already a minimal status that provides some minor detail.
GAME OVER — revenueFloor
final Revenue 0 against a target of 20
This would be a full end-of-game results. It should detail what the rules of the game were, what the actual results were, why did the game end?
This should be built as a first pass with everything that's already known, easy to calculate, and throw it up there.
There should be a second pass where we look for what are the ways to make it really exciting and interesting and extra stuff.
Splitting this out was the right call — this half is well-defined and mostly assembled already.
Answering the open questions in the description, in particular "I don't know if we keep statistics on".
What the four lines you quoted actually are
GAME OVER — revenueFlooris not a message; it is the raw enum value being printed.main.tsinterpolates
outcome.reasonstraight into the page, so a player reads an internal identifier. Thepossible values are
daysElapsed,collisionFloor,revenueFlooronmain, plustargetReachedon the 0.4.9 line, which still has a first-to-target victory that main dropped. That alone is worth
fixing whatever else this issue becomes — it answers your "why did the game end?" for free, once each
reason has a sentence.
Most of the first pass already exists
The Day-end dialog built for #10 assembles nearly this exact thing from the Frame — standings sorted
by revenue with the viewer marked, the target reported against combined revenue, the Days elapsed and
the pace. Pointing it at the finished state rather than the rollover is a small piece of work, and
the two screens should share one implementation so they cannot disagree.
Available on the Frame with no new tracking at all: mode, the full rule set that was in play
(
houseRules, and on main the victory dials), per-player revenue, the winner, the target,Days elapsed, and the reason. That covers the rules-of-the-game, result, per-player scores,
solitaire/co-op, and which player won — most of your bullet list.
Collisions are the exception and they differ by line:
maintrackscollisionsTodayandcollisionsTotalagainst configurable per-Day and total limits, so "too many collisions on a day"and "too many collisions total" are both reportable there. The 0.4.9 line has no collision scoring at
all, so that section simply has nothing to say on the playtest build.
On statistics: nothing is being kept, and nothing needs to be
The engine keeps no running counters beyond revenue and collisions — but it emits 49 distinct event
types, and a save is a replay. The stored game is
{ seed, config, history }; the board and thewhole event stream are recomputed from it. So any statistic can be counted after the fact, by
replaying and tallying — including for games that have already been played and saved, which means
this does not have to be designed correctly up front.
Events that answer your list directly:
trainCompletedloadStarted,loadAdvanced,loadCompleted,unloadBegan,unloadCompletedpassengersBoarded,passengersDetrainedtrainsDestroyedtrainStoodStill, which is emitted per Stage, so a run of them isexactly the "sat on a siding" streak you describe
And for badge material, the vocabulary is richer than the scoring is:
flyingSwitch,dispatchBonusUsed,officeUpgraded,facilityUnjammed,expediteFault,trainHeld,trainDiverted,secondSectionOrdered,clearanceRequested/clearanceGiven. Your "switchingmaster" badge — every train that passed through did some switching — is
trainCompletedjoinedagainst
carsCoupled/carsDroppedper train number.Suggested shape for the first pass
Everything above except badges, sharing the #10 dialog's implementation, plus a sentence per outcome
reason so nobody reads
revenueFlooragain. A stat block computed by replaying the finished game'sown history, which needs no engine change and no new persistence. That leaves the brainstorm you
describe entirely to the second pass, where it belongs — and because the stats are derived rather
than recorded, a second pass can add any of them retroactively to games already played.
Related: #11 is the extended-play half. Whichever way that is ruled, this screen needs to stay
reachable after play continues.
Built in v0.7.3, commit
45580d8, as the first pass you asked for. Not on the 0.4.9 playtestline, per your call.
The four lines you quoted are gone
GAME OVER — revenueFloorwas never a message — it wasoutcome.reason, an internal enuminterpolated straight into the page at the one moment the game has the player's whole attention.
Every reason now has a sentence with this game's own numbers in it, which answers your "why did the
game end?" outright.
What the screen reports
Everything from your list that the game already knew, plus the statistics it did not:
combined Revenue against the target; collisions, but only in a game actually scored on them
where Extras may start, whether trains may be discarded, optional rules
loads made up and broken, passengers boarded and detrained, cars coupled and set out, Extras run,
second sections, flying switches, offices upgraded, facilities unjammed, trains held and diverted,
expedite faults, dispatch bonuses, clearances allowed of those asked, and trains destroyed with the
cars they took with them
It shares the Day-end dialog's standings, target and collision blocks rather than reimplementing
them — two screens reporting the same game must not be able to disagree. It opens itself once at each
ending and leaves a button to reopen it, so #11's extended play never costs you the results.
"I don't know if we keep statistics on…"
Nothing was being kept. Now
state.tallyis, folded from the event stream rather than recorded —hooked at
applyIntentand atadvance, becausereducenever sees the phase driver's events andthose are exactly the interesting ones (
trainCompleted,trainsDestroyed). Nothing in the rulesreads it, so adding a counter is always safe; it rides the
Frame, so multiplayer gets the samenumbers as solitaire from one implementation. The test that matters asserts exactly once: it
plays real games, counts the event stream independently, and checks the tally against that count, so
a fold hooked twice or nowhere fails whatever the seed.
One thing I could not build, and it is a correction to my own earlier comment
I said here that
trainStoodStill"is emitted per Stage, so a run of them is exactly the 'sat on asiding' streak you describe". That was wrong, and I found it only by reading
advance.tswhilebuilding the tally rather than trusting the comment.
It fires once in a game, and only for a train whose profile sets
stopEarnsPoint— the X18Circus, the one card in the deck that pays for standing still — with
tray.stopPointClaimedmakingsure it can never fire twice. I built the streak, watched it render
1 Stage at (0,0), and took itout again. The engine has no per-Stage "this train did not move" signal at all, so "longest an
engine sat on a siding" needs one before it can be answered.
TODO.md#36 records what it would take(a new per-Stage event, or sampling live state on the Stage boundary — the tally cannot do the latter
today, because it folds a batch of events after
advancehas mutated past the moment theydescribe). What is reported instead is the Circus set-up itself, which is a real thing that happened.
The second pass is left to you, deliberately
No badges. You asked for "a whole conversation brainstorming session on what are the things that might
be interesting", and that is where they belong —
TODO.md#33. The raw material is already beingkept:
trainsCompletedWithWorkis exactly your switching-master join (every train that passedthrough did some switching), and the vocabulary is there for the rest.
And the part worth knowing before that conversation: because the statistics are derived rather than
recorded, a second pass can add any of them retroactively to games already played and saved. A save
is a replay, so nothing has to be designed correctly up front.