v0.8.0.9 — the bot plans its switching turn, stops wasting its draws, and the engine walks each route once
The developer bot, re-measured decision by decision against the bot before it, goes from about -0.3 revenue a game to about 4.8: - plans the whole switching turn before its first Move (sim/switch-planner.ts), +2.89 over 1600 paired seeds; closes TODO #53 - takes a face-up card only if it could play it, +1.52 over 1600 seeds - stops running Second Sections by accident in the New Train phase, +0.32 - lays track by what the district can do afterwards, +0.12 over 6400 seeds, run-arounds in 22 of 60 districts against 9 The engine is 2.8x faster with play proven identical: a route cache scoped to one unchanged position, applyIntent split into prepareIntent + commitEvents, and less allocation in exploreMoves. npm test now leaves out the bot simulations, which run as npm run test:sim. No rule changed; games in progress resume. Rejected candidates and the Second Section card question are in CHANGELOG.md and TODO.md (#104-#106). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX
This commit is contained in:
co-authored by
Claude Opus 5
parent
072029b1f7
commit
76c6e103b3
+319
@@ -19,6 +19,325 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.8.0.9 — 2026-09-15
|
||||
|
||||
**The developer bot, re-measured decision by decision — and an engine 2.8× faster.** Across the changes
|
||||
adopted below, each paired against the bot before it, the bot went from about −0.3 revenue a game to
|
||||
about 4.8: the switching planner +2.89, taking a face-up card only if it could be played +1.52, the
|
||||
deliberate New Train fallback +0.32, and laying track by what the district can do afterwards +0.12.
|
||||
|
||||
### Games in progress
|
||||
|
||||
**Resume.** No rule changed, and no legality check changes its answer. The engine's speed-ups were proven
|
||||
to leave play identical — every event and intent of 32 seeded games hashed before and after each step —
|
||||
and `test/route-cache.test.ts` pins the new check/commit split. Only the developer bot plays differently,
|
||||
and a bot's past moves are already in the save.
|
||||
|
||||
### The bot plans its switching turn instead of choosing one Move at a time
|
||||
|
||||
The switching branch was a ladder of rules picking ONE Move, and its own comment named the gap: "a
|
||||
strong player would use the six Moves to re-order the consist — that is the game's central switching
|
||||
puzzle, and this bot does not attempt it." `sim/switch-planner.ts` attempts it, for one turn.
|
||||
|
||||
**Why search is fair.** A switching turn draws no card and rolls no die, so trying sequences on a copy
|
||||
of the game is what a player does by looking at the board. The score reads only what a player can see
|
||||
— the district, the cars on the trains, the facilities — and never the deck. Jesse's line
|
||||
(2026-09-14): no non-player advantage for the bot.
|
||||
|
||||
**Why not every sequence.** Measured over 30 switching turns from bot games: a median turn reaches
|
||||
229 distinct positions, but 11 of 30 passed 20,000, because **setting cars out costs no Move** and a
|
||||
crew can leave them in a great many places. So the search keeps the best 48 positions at each step
|
||||
and stops at 3,000 tried; small turns are searched completely inside that.
|
||||
|
||||
**The score is of where the turn ends**, starting from Jesse's ruling that players deliver and pick up
|
||||
cars even when it delays trains. A car an industry can work is +1 (+0.25 past its box count); a car it
|
||||
cannot is −0.5 on its track; a finished car left on it −0.15; a wanted car aboard +0.35 or staged on
|
||||
plain track +0.2; a coach kept with its train +0.3, stranded −0.3; anything fouling the Office −3; a
|
||||
train away from the Office −0.25, −1 more if expedited; a train §8.2 would refuse (`badlyMadeUp`, now
|
||||
exported rather than copied) −0.6. A load made in this district scores nothing at a receiver here.
|
||||
|
||||
**The copy is partial.** `forkForSwitching` copies only the arrays a switching intent writes — cars
|
||||
standing on cards, industry tracks, the district's A/D and held lists, the trays' consists, this turn
|
||||
and the tally — and shares everything else. It began as a `structuredClone` of the district, which a
|
||||
profile put at **44% of all planning time**; the targeted copy made a position about three times
|
||||
cheaper to try. `test/switch-planner.test.ts`
|
||||
proves across seeded games that planning leaves the real game byte-identical, and that every plan
|
||||
replays through `applyIntent` on a FULL copy to the exact position it promised.
|
||||
|
||||
**Measured, 1600 paired seeds: +2.89 ± 0.18 revenue a game (t = 15.79)**, 733 seeds better, 21 worse.
|
||||
|
||||
| | rules | planner |
|
||||
| --- | --- | --- |
|
||||
| revenue | −0.05 | 2.83 |
|
||||
| freight loads + unloads | 0.22 | 1.53 |
|
||||
| collisions | 0.12 | 0.12 |
|
||||
| Cargo phases with a car spotted | 5% | 18% |
|
||||
| … with green box, car and Laborer at one industry | 3% | 11% |
|
||||
|
||||
**Split by source over 400 seeds**, because the biggest wins were rescues: freight **+1.21** (t = 13.9),
|
||||
passengers +0.26, expedite faults **+1.07** (19 of 400 games faulted under the rules, none under the
|
||||
planner — TODO #53, closed by this), collisions unchanged. **Without the fault rescue it is still
|
||||
+1.45 ± 0.12 (t = 12.2)** — the switching itself got better, not just the catastrophes rarer. It also
|
||||
sets out half as many cars (3.6 against 7.0): the ladder was setting cars down and picking them up.
|
||||
|
||||
**The worst seeds, traced.** Two of the three lost to "no free A/D track" collisions with both tracks
|
||||
already held by trains standing at the Office — not trains left away, which is what the score's
|
||||
`trainAway` weight would have explained. The third never had a coach train at the Office in a
|
||||
Load/Unload phase at all: a different switching turn draws different cards, and the game diverges.
|
||||
Neither is a scoring defect found; the full-Office collisions are worth watching.
|
||||
|
||||
**How far to search.** The search size was measured rather than guessed, paired over 400 seeds against
|
||||
the 3000-position, beam-48 search the gain above was measured with:
|
||||
|
||||
| budget / beam | revenue against 3000/48 | median / worst per turn |
|
||||
| --- | --- | --- |
|
||||
| 3000 / 48 | — | 150 ms / 445 ms |
|
||||
| **2000 / 32 (adopted)** | −0.02 ± 0.01 (t = −1.68), inside the noise | **69 ms / 222 ms** |
|
||||
| 1000 / 24 | −0.06 ± 0.02 (t = −2.65) | 53 ms / 104 ms |
|
||||
|
||||
Times are from 51 switching turns with other simulations sharing the CPU, so read them as relative.
|
||||
|
||||
**A switching-only legal list.** `legal.ts` now exports `legalSwitchingActions`, the switching half of
|
||||
the Local Operations candidates run through the same `check`, in the same order — so the planner stops
|
||||
paying for every draw and Freight Agent candidate at each position it tries. A test asserts it equals
|
||||
`legalActions`' switching subset across real games. It contains no rule; `check` still decides.
|
||||
|
||||
**Nothing the simulation tests measure moved backwards.** `sim.test.ts` passes all 35 tests with the
|
||||
planner as the default, floors unchanged.
|
||||
|
||||
**Cost.** A turn is planned once and then played a step per decision, replanned if the position is
|
||||
ever not the one expected. At a live table that is a synchronous pause inside `driveBots`, well inside
|
||||
Jesse's bar of half a second before a switch.
|
||||
|
||||
**The price is test time.** A standard solitaire game takes ~400 ms with the planner against ~108 ms
|
||||
without, and `npm test` — which plays well over a thousand bot games in `sim.test.ts` — went from about
|
||||
150 s to **8 min 6 s** (992 of 992 passing, measured with nothing else running). What is left of a
|
||||
planned turn's time is the engine itself: applying a move is about half of it and `check` a third.
|
||||
|
||||
**And the take rule tripled it again.** With both defaults in, `npm test` passes 992 of 992 in
|
||||
**21 min 21 s** with nothing else running — the bigger districts the take rule builds (14 → 24 cards,
|
||||
29 cards played a game) make every game longer to play and every switching turn wider to search.
|
||||
|
||||
`noPlanSwitching=1` is the ablation.
|
||||
|
||||
### Track goes where it lets the district do something
|
||||
|
||||
Jesse's third area, after switching and industries: track for the run-around. With the draw no longer
|
||||
wasted, ~13 of ~16 pieces a game were still being laid by the draw turn's fallback at the first legal
|
||||
square, and both fixes to that fallback failed (holding −0.83, `bestTrackLay`'s own score +0.07). So the
|
||||
limit was the scoring: `bestTrackLay` scores the PIECE, and cannot tell one that opens an industry site
|
||||
or closes a run-around from one that fills a square.
|
||||
|
||||
`bestValuedLay` scores the LAYOUT the piece would leave instead. Each legal lay is placed on a copy of
|
||||
the district exactly as the reducer places it — `protoCard` and `extendLimitsIfNeeded`, now exported
|
||||
rather than copied — and worth the change it makes to `layoutValue`: industry sites a crew can reach
|
||||
(`canPlaceAt`), a closed run-around (the bot's own `descendFrom` walk), ways off the main and reachable
|
||||
siding, a Running Track straight for Interlocking, and a penalty for a turnout on the main whose leg
|
||||
joins nothing. `bestTrackLay`'s slot takes the best lay that gains something; the fallback still lays as
|
||||
often as before — holding starved the district — but the best lay rather than the first.
|
||||
|
||||
**+0.118 ± 0.029 revenue a game (t = 4.14) over 6400 paired seeds**, 1,667 better against 1,430 worse.
|
||||
It needed that many: 400 seeds read +0.17 (t = 1.48) and 1600 read +0.14 (t = 2.58). What it builds is
|
||||
the larger change — **closed run-arounds in 22 of 60 districts against 9**, industries 1.78 → 1.87, freight
|
||||
2.54 → 2.65, collisions unchanged, the district 24.2 → 21.9 cards because squares stop being filled with
|
||||
pieces that build nothing. No slower: a standard game measured faster, the smaller districts leaving
|
||||
less to search. `noValueLays=1` is the ablation.
|
||||
|
||||
**Why so many run-arounds buy so little.** Traced over 40 games: the planner uses the loop — a move ends
|
||||
on it in 63 of 131 plans where one exists, 35 run it both ways — but its planned gain per turn is the
|
||||
same with a run-around as without (0.28 against 0.27). A run-around is for putting a train's cars in a
|
||||
different order, which pays off in the turns AFTER; a one-turn planner cannot value it. That is TODO #105.
|
||||
|
||||
**How 6400 seeds were measured.** `compare.ts` keeps every game's full statistics for both sides, and the
|
||||
run was stopped for low memory — as was a first lean attempt, by the machine's background-task guard
|
||||
rather than by the process: a probe showed no leak (heap flat at 9 MB after GC across 300 paired games).
|
||||
The figure above comes from a paired script with the same seeds and configuration that keeps only each
|
||||
seed's revenue difference, run in resumable foreground chunks; its first 1600 seeds read +0.143, matching
|
||||
`compare.ts`'s +0.14.
|
||||
|
||||
### The engine walks each position's routes once — 2.8× faster, every game identical
|
||||
|
||||
Jesse's call (2026-09-15): speed up the engine's move checking and applying first, because the live
|
||||
server runs the same `legalActions` and `applyIntent` for every bot and every player. A CPU profile
|
||||
with inlining off put the route walk (`reachableDestinations`) at a third of all time and garbage
|
||||
collection at another third, and most of the walking was repeated work.
|
||||
|
||||
- **A route cache scoped to one unchanged position** (`withRouteCache`, `apply.ts`). `legal.ts` walked a
|
||||
tray's routes to list its moves, `check` walked them again for every one of those moves, and
|
||||
`execute` walked the chosen move a third time. Now a legal-action listing, and the check-and-execute
|
||||
of one intent, walk each route once. The cache is keyed to the state object and the walk's inputs
|
||||
and dropped before `reduce` changes anything, so a hit is exactly what a fresh walk returns.
|
||||
- **`applyIntent` = `prepareIntent` + `commitEvents`.** The planner decides every candidate against one
|
||||
position inside one cache and commits each to its own copy; deciding on the copy had re-walked every
|
||||
route the listing had just walked.
|
||||
- **Less garbage in the walk itself** (`exploreMoves`): the blocked-square explanations are not built
|
||||
when only destinations are wanted, the queue is read by index instead of `shift()`, and "has this route
|
||||
been here" reads the route's own path instead of copying a Set at every step.
|
||||
|
||||
**A standard solitaire bot game, 561 ms → 203 ms; a short one, 141 ms → 65 ms.** Proved identical by
|
||||
hashing every event and intent of 32 seeded games (20 solo standard, 6 three-player standard, 6
|
||||
two-player short) before and after each change, and pinned by `test/route-cache.test.ts`, which checks
|
||||
at every decision of a seeded game that `prepareIntent` writes nothing and that committing its events
|
||||
to a copy equals `applyIntent` in place.
|
||||
|
||||
### The test suite is split
|
||||
|
||||
Jesse (2026-09-15): `npm test` runs after every change, so it has to stay fast; reducing the simulations'
|
||||
game counts is not acceptable. `npm test` now runs every file except `test/sim.test.ts`, and
|
||||
`npm run test:sim` runs the bot simulations on their own, typechecked first.
|
||||
|
||||
**Measured with nothing else running, after the engine speed-up: `npm test` 958 of 958 in 1 min 45 s;
|
||||
`npm run test:sim` 35 of 35 in 6 min 58 s** — against 21 min 21 s for the two together before it.
|
||||
|
||||
### Where industries go was not the problem — there was nowhere, and the draw was going in circles
|
||||
|
||||
The next thing Jesse named after switching was where industries and enhancements go, then track for the
|
||||
run-around. Measured before touching either, 30 standard games, and it turned both round:
|
||||
|
||||
- **The bot places an industry every time it legally can.** It held an industry card at 1,441 draw
|
||||
decisions and a legal square existed at 24 of them (2%); it played the industry at all 24 and never
|
||||
discarded one that had somewhere to go. 0.70 industries stand on a board at game end.
|
||||
- **Why there was nowhere.** An industry is a plain east-west piece that must join existing track and
|
||||
may not sit on the Running Track. Asked of `check` square by square, the closest a held card got was
|
||||
NOT_CONNECTED 1,681 times, OUTSIDE_LIMITS 398, legal 28 — never locked out. Printed districts show why:
|
||||
the row beside the main fills with 45° curves and turnouts whose east-west ends face occupied squares.
|
||||
- **And track was not the lever either — its supply was.** At 1,334 decisions holding an industry with
|
||||
no site, a track lay was legal at only 54; a lay that would open a site existed at 22 and the bot
|
||||
already chose one at 17. The hand had no track in it.
|
||||
- **Because the draw was cycling.** The bot took 20.1 timetabled trains and 11.9 industries a game off
|
||||
the Departments and discarded 20.2 and 11.8: `takingRank` ranked a face-up train 2 whether or not the
|
||||
A/D cap would let it be played, and a face-up industry 1 whether or not it had a site (one did at 0.17
|
||||
of those takes). The same card was taken again 28.8 times a game, against 19.6 blind draws.
|
||||
|
||||
### The bot takes a face-up card only if it could play it
|
||||
|
||||
`takingRank` now ranks a face-up train card 0 when the A/D cap would hold it, and a face-up industry 0
|
||||
when it is locked out or has no legal site — both asked of the rules the play itself is checked by
|
||||
(`trainWouldOverfillTheOffice`, and `isLockedOut` with `canPlaceAt` in `industrySiteExists`). Nothing
|
||||
here is hidden information: the Departments are face up.
|
||||
|
||||
**+1.52 ± 0.10 revenue a game (t = 15.59)** over 1600 paired seeds, measured as the ablation
|
||||
`noPlayableTakes=1` against the new default: 880 seeds worse without it, 211 better. (The first
|
||||
400-seed read was +1.63 ± 0.19.) Cards played 15.8 → 29.0 a game and the district grows 14.2 → 24.2
|
||||
cards, because the draws it stopped wasting now bring in track: trains scheduled 1.74 → 2.30, freight
|
||||
1.51 → 2.54, passengers 1.89 → 2.94. **Collisions rose, 0.12 → 0.24**, with the extra trains — see
|
||||
TODO #106.
|
||||
|
||||
### Rejected: holding track the run-around scoring declined
|
||||
|
||||
Traced first: of ~16.4 track pieces a standard game lays, only ~3.3 came from `bestTrackLay`. The
|
||||
draw turn's "play what is in hand" fallback laid the other ~13 at the FIRST legal square — pieces
|
||||
`bestTrackLay` had just declined, including 4.0 turnouts and 4.3 straights a game on the main and 1.5
|
||||
turnouts a row off it. The candidate let the fallback lay track only when nothing else could bring the
|
||||
hand under the limit. **−0.83 ± 0.16 (t = −5.17)**, 172 worse against 90 better: the district fell
|
||||
24.1 → 11.7 cards and freight 2.41 → 1.77. `bestTrackLay` declines most pieces, so holding them
|
||||
starves the district — #59's finding, again, with a fuller hand to hold them in.
|
||||
|
||||
### Rejected: laying fallback track where it scores best instead of first
|
||||
|
||||
The other half of the same trace: keep laying the ~13 fallback pieces a game, but at `bestTrackLay`'s
|
||||
best-scoring square (bonus or not) rather than the first legal one. **+0.07 ± 0.13 (t = 0.60), inside
|
||||
the noise**; freight 2.41 → 2.56 but the district 24.1 → 21.0 cards. With both halves measured, the
|
||||
limit is `bestTrackLay`'s SCORING — it cannot tell a piece that opens an industry site or advances a
|
||||
run-around from one that fills a square — not which branch places the piece. Code removed.
|
||||
|
||||
### The bot stops running Second Sections by accident
|
||||
|
||||
TODO #106 traced under today's defaults: of 20 "no free A/D track" collisions in 60 standard games, no
|
||||
train had been held before any of them and only 8 of the destroyed were Extras. Six destroyed a train of
|
||||
the same number as a Second Section run within two Stages, and **all 26 Second Sections the bot ran were
|
||||
an accident**: when no car was on offer, the New Train phase fell back to `options[0]`, and `legalActions`
|
||||
lists `newTrain.secondSection` ahead of the Extra starts — so a waiting Extra became a doubled train due
|
||||
out, into an Office that never had an A/D track to spare. The fallback now takes a car, a pass or the
|
||||
Extra's start, and never a Second Section or a Red Flag merely because it was listed first.
|
||||
|
||||
**+0.32 ± 0.09 revenue a game (t = 3.64)** over 400 paired seeds, 30 better against 12 worse; collisions
|
||||
0.24 → 0.19. `noDeliberateNewTrain=1` is the ablation.
|
||||
|
||||
**Found on the way, for Jesse — a rules question, not changed.** Q9 defines the Second Section as a card
|
||||
played on a train due out and `content.ts` gives `SECOND_SECTION` one copy, but `buildDeck` never deals it
|
||||
and `check` asks for no card, so any player can run one for free on every train due out.
|
||||
|
||||
### Rejected: planning two switching turns ahead (TODO #105)
|
||||
|
||||
Built as `planTwoTurns`: keep the six best ends of a switching turn, remove the trains that highball in
|
||||
the Mainline Phase in between (on the Office square and made up — their cars leave with them), reset the
|
||||
Moves, plan the next turn from each, and choose by the position after departures plus 0.8 of what the
|
||||
next turn adds. **−0.28 ± 0.11 (t = −2.58)**; corrected to charge the expedite fault the gap cannot undo
|
||||
and a Stage for every train left away, **−0.14 ± 0.06 (t = −2.36)** — 400 paired seeds each. A second turn
|
||||
has little to find: only 49% of switching turns keep their train for the next, a further turn could have
|
||||
spotted just 0.7 of the 6.1 wanted cars a game that leave aboard departing trains, and trains left away
|
||||
cost Stages walking crews home. Dropped at Jesse's call; the measurements are in TODO #105.
|
||||
|
||||
### Rejected: starting an Extra the Office cannot take where its run never arrives
|
||||
|
||||
TODO #106, re-measured under today's defaults: 20 collisions in 60 standard games (−1.67 revenue a
|
||||
game), every one "no free A/D track", and 26 Extras forced out by a full hand of them. The bot also took
|
||||
the first legal start for EVERY Extra — the western Division Point, 158 of 158 — so the candidate chose,
|
||||
when the committed trains exceeded the A/D tracks, a start whose run never reaches the Office: an Extra
|
||||
runs away from its start (`resolveExtraStart`), and at the Interchange one way may miss the Office.
|
||||
**+0.10 ± 0.05 (t = 1.82), 395 of 400 seeds identical**: such a start was on offer at 1 of 25 over-cap
|
||||
starts — most Divisions have no Interchange, and a Division Point always runs through the Office. Code
|
||||
removed.
|
||||
|
||||
### Rejected: refusing to draw into a forced Extra
|
||||
|
||||
With the take rule in, the bot's worst seeds all lost to "no free A/D track". Traced: every one of 23
|
||||
train plays past the A/D cap in 40 games was an EXTRA, played because the hand held four of them and
|
||||
the only legal action was `card.play` — §6.2 makes a player over the limit reduce the hand, and an Extra
|
||||
may never be discarded (`keepReason`), so the cap in `choose` yields rather than leave nothing legal.
|
||||
The candidate declined to choose the draw option with a full hand of such cards when switching or the
|
||||
Freight Agent was on offer. **−0.03 ± 0.11 (t = −0.30), inside the noise**: collisions fell
|
||||
0.26 → 0.20, and development fell with them (cards played 29.0 → 25.6) — the Stage spent avoiding the
|
||||
draw was a Stage not spent building. Code removed; the trap itself is real and filed.
|
||||
|
||||
### Rejected: discarding the card least likely to become playable
|
||||
|
||||
When a discard was forced, the candidate picked WHICH card by a keep-value (next Office tier, then
|
||||
Enhancements, track, trains, and industries with no site or Modifiers with no industry last), weighted
|
||||
so no pile preference could overturn it. **−1.16 ± 0.12 (t = −9.27)**, 16 better against 119 worse,
|
||||
cards played 15.7 → 10.4. Not traced; the likeliest reason is that the weight overrode `bestDiscard`'s
|
||||
pile choice, which exists to avoid burying a face-up card the bot wants. Code removed.
|
||||
|
||||
### Flying Switch is searched, and cannot be measured
|
||||
|
||||
The planner now considers Flying Switch alongside Moves, set-outs and sorts — its partial copy of the
|
||||
game carries the hand and the Salvage Yard, which `spendCard` writes, and spending the card costs a
|
||||
tenth of a point so it is played only when it buys something. **Over 400 paired seeds it changed
|
||||
nothing, because the card is dealt 0 copies** (not in sheet 5; Jesse, 2026-08-26): across 30 standard
|
||||
games no Flying Switch was ever drawn. TODO #58's "never fires" is therefore a deck fact, not a bot
|
||||
one. On by default, so the planner uses the card the day it is dealt again.
|
||||
|
||||
### Rejected: letting the planned gain decide whether to switch at all
|
||||
|
||||
Whether a Stage goes to switching is decided by `usefulSwitching`, a yes/no reading of the district.
|
||||
The candidate replaced it with the planner's own answer — plan the turn on a fork before the option is
|
||||
chosen, and switch only if it gains at least a threshold. Paired over 400 seeds against the planner:
|
||||
|
||||
| threshold | revenue | seeds better / worse / identical |
|
||||
| --- | --- | --- |
|
||||
| 0.5 | **−0.33 ± 0.06 (t = −5.06)** | 15 / 70 / 315 |
|
||||
| 0.25 | −0.01 ± 0.05 (t = −0.21) | 21 / 22 / 357 |
|
||||
| 0.1 | +0.06 ± 0.03 (t = 1.79) | 20 / 13 / 367 |
|
||||
|
||||
At 0.5 it refuses turns that only COLLECT — a wanted car picked up scores +0.35 — and those pickups feed
|
||||
the deliveries after them. Below that it agrees with `usefulSwitching` almost everywhere, because that
|
||||
rule already says yes exactly when there is a car to deliver or lift, which is when a plan gains. The
|
||||
decision that would matter is switching against DRAWING or the Freight Agent, and the bot has no value
|
||||
for either to compare with; that is the next piece of work, not a threshold. It also cost a full
|
||||
search at nearly every Local Operations decision. Code removed.
|
||||
|
||||
### Rejected: discounting a car its industry cannot work yet
|
||||
|
||||
A spotted car scored half when its shipper had no load staged or its receiver's red box was full.
|
||||
**−0.29 ± 0.05 (t = −5.52)** over 400 seeds, 47 worse against 4 better; green boxes stocked fell
|
||||
15% → 11% of Cargo phases. The reason is the order the bot works in: the Freight Agent stocks a box
|
||||
only once a car is spotted to receive it (`canStockProductively`), so an industry is "not ready"
|
||||
precisely because nothing has been delivered — the discount withheld the delivery that makes it ready.
|
||||
Code removed.
|
||||
|
||||
---
|
||||
|
||||
## 0.8.0.8 — 2026-09-10
|
||||
|
||||
**A played train does not come back. Gitea#23, ruled and closed.**
|
||||
|
||||
Reference in New Issue
Block a user