adding classification and division yard updates, train collisions, engine and car updates, turnout improvements.
This commit is contained in:
+231
@@ -10,6 +10,237 @@ The target is 20 Revenue over 5 Days.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Q13 — a train that catches the one ahead runs into it
|
||||
|
||||
Answered, and implemented as option B: **collide on catching up**, which is the version that rewards
|
||||
judging the gap.
|
||||
|
||||
§10 makes a Mainline collision the Superintendent's fault and removes both trains, and ABS Signals
|
||||
exists to stop trains rear-ending each other — but §8.3's trigger list never named one and nothing
|
||||
was implemented, so granting clearance was FREE: both trains survived, no penalty, and ABS Signals
|
||||
protected against nothing. Now a following train that closes on the one ahead runs into it, and one
|
||||
that never closes is fine, so clearance is a bet on relative speed rather than a formality. §2.1
|
||||
divides the card into two regions, and sharing one is what "caught up" means. ABS Signals does what
|
||||
it prints instead: the follower stops short and holds.
|
||||
|
||||
**Not on a card that prints "trains may pass".** The first version fired 0.41 times a game while the
|
||||
bot never once granted clearance, which is the tell — those were all Double Track and Uncontrolled
|
||||
Siding, cards that hold two trains because they HAVE two roads. Catching up there means going past,
|
||||
which is what the card is for.
|
||||
|
||||
With that corrected the mechanic is invisible to the current bot, because it always denies
|
||||
clearance. That is the right shape, and the teeth are real:
|
||||
|
||||
| Superintendent | revenue | rear-enders | ABS holds |
|
||||
| --- | --- | --- | --- |
|
||||
| always denies (the bot) | 7.34 | 0.00/game | 0 |
|
||||
| **always allows** | **−5.13** | **2.20/game** | 19 |
|
||||
|
||||
So the bot's always-deny policy — a deliberate choice made when clearance was free and the
|
||||
arithmetic only guessed at — turns out to be correct, and is now correct for a measured reason.
|
||||
|
||||
Tested deterministically rather than through the bot: two trains built onto one single-track card
|
||||
with the follower closing, which collides whichever order the phase processes them in, and the same
|
||||
pair again with ABS Signals to prove it holds instead.
|
||||
|
||||
### The engine has a place in the train, and the yards are visible
|
||||
|
||||
**The engine had a position the game recorded and never used.** `engineFront` was a boolean, written
|
||||
in three places and read in none — so every consist was drawn as an anonymous row of cars. It is now
|
||||
`engineAt`, an index into the consist, because a Crew Tray is an engine plus its Rolling Stock and
|
||||
the engine may be PULLING (ahead of everything), PUSHING (behind everything), or in the middle doing
|
||||
both at once. A boolean cannot say the third thing. Consists are drawn with `ENG` where it sits.
|
||||
|
||||
The engine is deliberately NOT one of the `consist` entries: §8.2 counts the consist as Rolling
|
||||
Stock, and the four-car limit (§A.4) is a limit on cars, not on the locomotive hauling them.
|
||||
|
||||
**Both yards are now on the page**, by car type and split loaded / empty, with the Division Yard
|
||||
outlined the moment it goes bare. This matters more than it did: the Classification Yard returns to
|
||||
service only when the Division Yard is empty, so the supply genuinely runs down, and a game that
|
||||
never showed either yard gave no warning at all.
|
||||
|
||||
It shows the pressure immediately. In a finished game the Division Yard held 30 cars — hoppers,
|
||||
tanks and cabooses — and **no boxcars, coaches or reefers at all**, while 19 of them sat in
|
||||
Classification unable to come back, because the Division Yard was not bare.
|
||||
|
||||
Crew Tray scarcity was already implemented and is left alone: a train with no free tray is held
|
||||
(`trainHeld`), which is §7.
|
||||
|
||||
### The Classification Yard rule, from the source — and my guess was worth 2.4 Revenue it should not have been
|
||||
|
||||
Answered: used Rolling Stock is set out in the Classification Yard, used engines and cabooses go
|
||||
straight back to the Division Yard, and **the Classification Yard empties only when the Division
|
||||
Yard is bare** — then all of it returns at once.
|
||||
|
||||
That is a much harder rule than the one I invented. Returning cars at every DAY boundary keeps the
|
||||
yard topped up continuously; this lets it run down to nothing and refill in one go, which is the
|
||||
whole of the supply pressure the game is meant to have. Measured paired over 400 seeds:
|
||||
|
||||
my Day-boundary guess 9.67 the real rule 7.25
|
||||
paired change -2.42 ± 0.49 (t -9.67) 235 of 400 seeds affected
|
||||
|
||||
So the +2.32 celebrated when the Classification Yard was first made readable was very largely an
|
||||
artefact of getting the trigger wrong. The refill is now checked wherever a car leaves the Division
|
||||
Yard, so it fires the moment the yard empties rather than at the next convenient tick.
|
||||
|
||||
**Poling is dealt zero copies** rather than deleted. Its effect is "TBD in the source", so there is
|
||||
nothing to implement and a card that cannot be played is worse in a hand than absent from the deck.
|
||||
The catalogue entry stays so the gap remains visible. Deck 140 → 139, solitaire 118 → 117.
|
||||
|
||||
**Heavy Grade orientation stays rolled from the seed**, and is now documented as temporary in the
|
||||
code rather than only in TODO: the card says the player sets it, but it is dealt during setup and
|
||||
setup has no decision point — `createGame` is a pure function of the seed, which is also what makes
|
||||
a save portable.
|
||||
|
||||
**A counting question this raised, and could not answer.** A census of every holder of rolling stock
|
||||
comes to 92 against the 80 dealt. That is not proof of duplication: `outboundBox`, `inboundBox` and
|
||||
`menAtWork` all hold `RollingStock`, and stocking a green box takes a LOADED CAR out of the Division
|
||||
Yard — so some of those objects are cargo in transit rather than cars, and nothing distinguishes
|
||||
them. An accounting test was written and then withdrawn, because it could not tell the two apart.
|
||||
Logged: until a load is its own type, "is any stock being created or destroyed?" is unanswerable.
|
||||
|
||||
### The freight figures were counting one half of freight
|
||||
|
||||
A measurement fix, not a game fix — but it is the instrument every balance decision is read from.
|
||||
|
||||
`rev.freightUnload` was assigned `eventCounts['unloadBegan']`: unloads STARTED, not Revenue EARNED,
|
||||
and the two differ by every unload that never finished. `grossFreight` then used `freightLoad`
|
||||
alone, so `freightShare` omitted the unload half outright. A completed load and a completed unload
|
||||
each earn a point, on two distinct `revenueChanged` reasons.
|
||||
|
||||
The comment that stood there claimed "an unload scores through the same event as a load completion
|
||||
in the reducer". It does not — `apply.ts` emits `freightUnload` separately. A comment asserting a
|
||||
fact about code a few lines away, and wrong.
|
||||
|
||||
freight share of gross: 39% -> 49%
|
||||
|
||||
The harness now prints both halves. `strategyBuckets` counted "a scoring game" the same wrong way,
|
||||
and so did the test guarding it — so a game that scored only by unloading was bucketed but not
|
||||
counted, which is how the fix first showed up as a failure.
|
||||
|
||||
This matters backwards as well as forwards: the "freight is only 13-18% of gross" finding was read
|
||||
from this number, and it is what drove the Gap 12 industry-density change. That decision was taken
|
||||
against an instrument reading roughly 60% low.
|
||||
|
||||
### Turnouts that go nowhere — reported, confirmed, and mostly not the problem
|
||||
|
||||
Reported from two replays: of 8 turnouts off the Running Track, 2 formed a run-around, 2 served
|
||||
industries and 4 went nowhere. Measured across 120 games, that holds exactly:
|
||||
|
||||
| what a turnout leads to | |
|
||||
| --- | --- |
|
||||
| part of a run-around | 30% |
|
||||
| a stub, but serves an industry | 12% |
|
||||
| a stub ending in bare track | 44% |
|
||||
| **nothing below it at all** | **15%** |
|
||||
|
||||
**4.61 wasted turnouts a game.** Then three attempts to stop it, each measured paired over 400
|
||||
seeds, and each WORSE than leaving it alone:
|
||||
|
||||
| attempt | paired change | |
|
||||
| --- | --- | --- |
|
||||
| no new way down while one leads nowhere | run-arounds → **0** | deadlocked: a run-around needs TWO ways down, and the second cannot be justified by what hangs off the first |
|
||||
| first two free, gate the rest | **−0.84 ± 0.53** (t −3.08) | track spend collapsed 15.4 → 4.8 |
|
||||
| forbid rail that butts an incompatible card | **−0.62 ± 0.57** (t −2.11) | |
|
||||
|
||||
The reason is that a turnout is not only a way DOWN. It is also a way UP, and both the east-west
|
||||
extension and the closing arc are gated on one existing beyond them — so cutting the turnouts cuts
|
||||
the places a siding can rejoin, and the sidings stop forming too. The apparent waste is optionality.
|
||||
This also re-confirms, with proper statistics, a note left in the code by an earlier attempt.
|
||||
|
||||
### Rail that can never go anywhere
|
||||
|
||||
The one that did work, and only as a tie-breaker.
|
||||
|
||||
A port facing an EMPTY square is a promise: something may be built there later. A port butting an
|
||||
OCCUPIED square whose card has no matching port is not — that square is taken, so the rail stops
|
||||
dead and always will. Reported from seed 618682, where an arc came off a turnout with its far end
|
||||
jammed into a curve that could not accept it. Measured: **28% of all pieces laid, 4.26 a game.**
|
||||
|
||||
Forbidding it cost 0.62 revenue a game. Applying it as a **tie-breaker on the distance score** —
|
||||
never able to veto a piece, only to choose between two the heuristics rate equally — measured
|
||||
**+0.43 ± 0.49 (t 1.74)**, with 108 seeds better against 67, and cut these from 28% of pieces to 7%.
|
||||
Not significant on its own, but it is the only one of four attempts pointing the right way, and the
|
||||
mechanism is sound.
|
||||
|
||||
**And it fixed the reported problem after all — sideways.** Re-running the turnout taxonomy:
|
||||
|
||||
| what a turnout leads to | before | after |
|
||||
| --- | --- | --- |
|
||||
| part of a run-around | 30% | **66%** |
|
||||
| a stub, but serves an industry | 12% | 13% |
|
||||
| a stub ending in bare track | **44%** | **2%** |
|
||||
| nothing below it at all | 15% | 18% |
|
||||
| **wasted per game** | **4.61** | **1.65** |
|
||||
|
||||
Bare stubs all but gone and run-arounds more than doubled, without ever refusing a turnout. Refusing
|
||||
them directly had destroyed the run-arounds; declining to lay rail INTO a dead end leaves the bot
|
||||
free to cut every turnout it likes and quietly stops it building the stubs. The remaining waste is
|
||||
the last turnouts of a game, cut with no turns left to build beneath them.
|
||||
|
||||
Also added, and honest about it: a turnout is not cut when no arc remains to hang beneath it.
|
||||
Measured at **0%** today — the bot lays the arc immediately after the turnout and never runs the
|
||||
supply dry — so it is a guard against the supply changing rather than a fix for anything happening
|
||||
now. Its test constructs the situation by draining the arcs.
|
||||
|
||||
### A source file git was hiding
|
||||
|
||||
Found while checking the above: **`src/web/replays.html` had never been committed.** `.gitignore`
|
||||
carried `replay*.html` to catch the throwaway files generated at the repo root, and unanchored it
|
||||
also matched a source page — the website's replay viewer. `git archive HEAD` confirms it: a fresh
|
||||
clone does not contain that file, and `build-web.ts` copies it unconditionally, so the build would
|
||||
have failed for anyone but this working copy.
|
||||
|
||||
The pattern is anchored to the root now (`/replay*.html`), which still ignores the generated files
|
||||
and no longer ignores the source. A test asks GIT — not `.gitignore` — whether each source page
|
||||
would survive a clone, because that is the actual question.
|
||||
|
||||
### Three places draw a game, and they had drifted
|
||||
|
||||
Reported from playtesting: the replay had lost its "extra slow" speed, and neither the sound nor the
|
||||
auto-hide could be found. Both true, and the same cause — a game is drawn in THREE places and only
|
||||
some of them had kept up:
|
||||
|
||||
| | speeds | sound | auto-hide |
|
||||
| --- | --- | --- | --- |
|
||||
| the playable page | — | yes | yes |
|
||||
| the standalone replay file | 5 | yes | yes |
|
||||
| **the website's replay viewer** | **3** | **no** | **no** |
|
||||
|
||||
The website viewer is its own implementation — it replays a save through the engine in the browser
|
||||
rather than reading a rendered file — and it never got what the other two grew. It now has all five
|
||||
speeds (extra slow through very fast), the sound, and the auto-hide, using the same shared `cuesFor`
|
||||
and `playCue` as everywhere else.
|
||||
|
||||
Three tests hold them together from now on: the two viewers must offer the SAME set of speeds, all
|
||||
three pages must carry the sound and auto-hide controls, and `replays.ts` may not ask for an element
|
||||
its page does not have — the same total check the playable page already had, and the one that would
|
||||
have caught this. Verified by removing a speed and a control and watching them fail.
|
||||
|
||||
### The published replays had stopped replaying
|
||||
|
||||
Both saves in `public/replays/` were dead. A save is a seed plus the intents, replayed through the
|
||||
real engine — so it cannot describe a position the rules could not produce, and an intent that no
|
||||
longer applies stops the replay rather than being forced. That is the safe direction, but it is
|
||||
silent: `seed-202` got 42 intents into 360 before halting, and `seed-430` managed 4 of 338.
|
||||
|
||||
They were recorded before this run's rules work — Modifier hosts, the industry-to-car mapping, the
|
||||
Classification Yard — so most of what they described is no longer legal. Replaced with three games
|
||||
generated against the rules as they stand, each verified to replay every intent to the final Day:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| A winning run | 36 Revenue, seed 1038389 |
|
||||
| A strong run | 32 Revenue, seed 618682 |
|
||||
| Collisions | 10 Revenue and 27 smashes, seed 919604 |
|
||||
|
||||
The third is deliberately a bad game: a full Office is a collision, and it costs more than the
|
||||
freight was worth.
|
||||
|
||||
This is the "save/restore is not version-aware" item in TODO doing exactly what it warns about. The
|
||||
saves are cheap to regenerate, so the fix is not to freeze them — it is for a stale save to say so
|
||||
instead of quietly ending early.
|
||||
|
||||
### The replay behaves like the game it is replaying
|
||||
|
||||
Sound and the district auto-hide were built for the playable page and the replay had neither, which
|
||||
|
||||
Reference in New Issue
Block a user