v0.4.3 fixed load/unload bugs. empty cars must be staged prior to loading. rolling stock is now conserved.
This commit is contained in:
@@ -21,6 +21,76 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.4.3 — 2026-08-14
|
||||
|
||||
### A load has to have somewhere to go
|
||||
|
||||
Jesse walked through how loading an industry is meant to work at the table, and the engine got four of
|
||||
the five steps exactly right. It got the first one wrong, in both directions.
|
||||
|
||||
**You could stage cargo against no car.** §9.3 requires an empty car of the right type standing on the
|
||||
industry's track before anything else happens — *"otherwise you are just dropping cargo onto the
|
||||
tracks, pointless waste"*. The engine checked for that car only at the **last** step, when the load
|
||||
came off WORK. So you could fetch cargo with the Freight Agent, walk it M→A→W across three Stages and
|
||||
three Laborers, and only then find there was nowhere to put it. Worse than waste: from the moment the
|
||||
load lands on M the industry track is **locked**, so no train can come in to spot the car you now need.
|
||||
The only way out was a Freight Agent unjam — another whole turn, to undo a move the rules should never
|
||||
have offered.
|
||||
|
||||
Now gated in three places, all counted rather than merely present:
|
||||
|
||||
- **`freightAgent.stockOutbound`** — no cargo is fetched unless a matching empty is spotted and not
|
||||
already promised to a load already in the box or on the sign.
|
||||
- **`laborer.startLoad`** — the same check again, because it is not redundant: coupling is mandatory,
|
||||
so a crew running over that industry track *must* pick up the spotted empty, and the cargo you
|
||||
staged an hour ago can be left with nothing.
|
||||
- **`laborer.beginUnload`** — the mirror. The red Inbound box is where an inbound load lands, and it
|
||||
was checked only at the final step too. Counted, because only the W box must be free to begin: once
|
||||
a load moves W→A a second can start behind it, and every red box in play holds exactly one car.
|
||||
|
||||
**Strict type matching throughout** — a hopper load cannot be swapped onto a tank. And `startLoad` no
|
||||
longer always takes `outboundBox[0]`: it starts the first load that has a car to land on, so a Power
|
||||
Plant holding a hopper load and a tank load against one spotted tank works the tank instead of
|
||||
reporting the whole facility blocked.
|
||||
|
||||
**Passengers are deliberately exempt and it is written down.** People can wait on a platform for a
|
||||
train that has not arrived, so a Passenger Facility still stocks freely. Freight cannot: a crate on the
|
||||
ground is not a shipment.
|
||||
|
||||
**Measured before building it, which is why it was worth building.** Requiring the car removes **78% of
|
||||
the `stockOutbound` moves the menu offered** (704 → 157 across 200 games) — but **99.3% of the moves the
|
||||
bot actually took survive** (138 of 139). The rule deletes offers a competent player would never have
|
||||
used. Bot unchanged at 7.3 mean. On the unload side it removes 2.2% of offers, and the jam it prevents
|
||||
was caught happening three times in 200 games.
|
||||
|
||||
### The Blocked panel says what to do, and in what order
|
||||
|
||||
The refusal codes never reach a player, because the menu simply stops offering the action — so with
|
||||
78% of stocking moves gone the panel was the only thing that could explain it. It said *"green box
|
||||
empty — nothing to load (needs a Freight Agent action)"*, which is step two told to someone who has not
|
||||
done step one. It now says to bring a car in first, names the commodity, and distinguishes "MEN is
|
||||
occupied" from "there is nothing here to load onto", which send you to fix quite different things.
|
||||
|
||||
### Rolling stock is exactly conserved
|
||||
|
||||
`TODO.md` recorded the inbound path minting ~1.3 cars a game and blocked tuning `ROLLING_STOCK_SUPPLY`
|
||||
until it was settled. Re-audited: **exactly conserved, 100 games out of 100, range 0..0.** The
|
||||
asymmetry had already been closed from the other end when `unloadBegan` and `passengersDetrained`
|
||||
started taking their replacement empty out of the Division Yard instead of conjuring it — a load is a
|
||||
car that moved, not a car that appeared, which is exactly the tabletop procedure Jesse described.
|
||||
|
||||
Both conjuring fallbacks now **throw** instead of minting, so the leak cannot come back quietly;
|
||||
neither fired across the suite or the audit. `ROLLING_STOCK_SUPPLY` is unblocked for the rebalance.
|
||||
|
||||
(The original audit's arithmetic was off in the same way mine was on the first attempt: cars set out on
|
||||
a card live in `card.standing` and are easy to leave out of the count, which makes a conserved game
|
||||
look like a leaking one.)
|
||||
|
||||
### Replays
|
||||
|
||||
Two of five died on the rule change — they replayed 138/336 and 81/347, which on screen looks exactly
|
||||
like a game that ended early. Re-recorded; three published, all verified to their last intent.
|
||||
|
||||
## 0.4.2 — 2026-08-13
|
||||
|
||||
### A completed run pays the whole table
|
||||
|
||||
@@ -10,7 +10,7 @@ train into an occupied Subdivision. Get that wrong and two trains meet at speed.
|
||||
|
||||
## Status
|
||||
|
||||
**v0.4.2 — solitaire is playable in a browser.** The whole game runs client-side: the engine is pure,
|
||||
**v0.4.3 — solitaire is playable in a browser.** The whole game runs client-side: the engine is pure,
|
||||
imports nothing outside itself, and never touches `Math.random`, so a static host is all it needs.
|
||||
|
||||
- **Rules** — specified, with **three open questions** left. Thirteen gaps in the original prototype
|
||||
|
||||
@@ -122,24 +122,21 @@ Ordered within each section by how much it is currently costing us.
|
||||
present it that way — the supply has to change, not the player.** Options: give track its own
|
||||
hand or yard the way the prototype did, raise the hand limit for track specifically, or print a
|
||||
siding as a single card. Nothing else reaches it.
|
||||
- [ ] **CLEARING AN INBOUND BOX MINTS A CAR — measured at 1.29 a game against a supply of 80.**
|
||||
Answered, and it is duplication after all. The two directions are not symmetrical:
|
||||
- **Outbound is paid for.** `stockToOutbound` SPLICES a loaded car out of the Division Yard to
|
||||
become the load, and `loadCompleted` swaps the emptied car into the Classification Yard as
|
||||
the loaded one takes its place on the industry track. Objects in, objects out.
|
||||
- **Inbound is not.** `unloadBegan` (apply.ts) turns one loaded car into an empty car on the
|
||||
track **plus** a load on MEN|AT|WORK, and `passengersDetrained` does the same to a coach —
|
||||
one loaded coach becomes an empty coach in the train plus an object in the red box. Then
|
||||
`inboundCleared` pushes that object into the **Classification Yard as a car**. The cargo
|
||||
becomes rolling stock, while the car it came out of is already back in service.
|
||||
|
||||
Measured over 200 games: cars at the end minus 80, plus collision losses, equals the
|
||||
`inboundCleared` count in 71/200 games exactly and 258 against 279 in total — the rest is
|
||||
loads still in flight at the final whistle. So the supply inflates by about 1.3 cars a game.
|
||||
That is the number `ROLLING_STOCK_SUPPLY` is supposed to control, so **no supply figure below
|
||||
can be tuned until this is settled**. The fix is a decision, not a patch: either a load stops
|
||||
being a `RollingStock` and becomes its own type, or `inboundCleared` discards rather than
|
||||
banking. Found by a conservation audit, not by a failing test.
|
||||
- [x] **~~CLEARING AN INBOUND BOX MINTS A CAR — measured at 1.29 a game against a supply of 80.~~
|
||||
Re-audited in v0.4.3: rolling stock is EXACTLY CONSERVED, 100 games out of 100, range 0..0.**
|
||||
The old audit's premise was right — the two directions were not symmetrical — but the asymmetry
|
||||
has since been closed from the other end. `unloadBegan` and `passengersDetrained` now take their
|
||||
replacement empty OUT of the Division Yard rather than conjuring it, so a load is a car that
|
||||
moved rather than a car that appeared: one leaves the yard, one arrives in Classification.
|
||||
Jesse's description of the tabletop procedure confirms this is the intended model — the token
|
||||
you push along the MEN|AT|WORK sign IS the car, fetched from the yard by the Freight Agent and
|
||||
swapped onto the industry track at the end.
|
||||
Both conjuring fallbacks now **throw** rather than minting, so the leak cannot silently return;
|
||||
neither fired across the suite or 100 audited games.
|
||||
**`ROLLING_STOCK_SUPPLY` is therefore unblocked** — it was waiting on this and can now be tuned
|
||||
in the rebalance pass. (The first audit's own arithmetic was off in the same way mine was on the
|
||||
first attempt: cars set out on a card, in `card.standing`, are easy to leave out of the count and
|
||||
make a conserved game look like a leaking one.)
|
||||
- [x] **`state = fold(events)` was not true, and the docs said it was. Settled: the INTENTS are
|
||||
canonical.** Measured before deciding — `advance.ts` never calls `reduce`, so **14 of the 46
|
||||
event types are never reduced**: the clock, and the entire Mainline phase, which is every train
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ must do.
|
||||
|
||||
## Current status
|
||||
|
||||
**v0.4.2.** Rules formalized, card faces specified, architecture documented, and the game playable
|
||||
**v0.4.3.** Rules formalized, card faces specified, architecture documented, and the game playable
|
||||
solitaire in a browser. See [`../CHANGELOG.md`](../CHANGELOG.md) for what each version changed and
|
||||
[`../TODO.md`](../TODO.md) for what is open; this section is the shape of the project, not a
|
||||
running tally, because a hand-maintained tally is what drifted last time.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "station-master",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Station Master — a railroad operations game",
|
||||
|
||||
+308
-332
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"seed": 1022551,
|
||||
"title": "27 Revenue · seed 1022551",
|
||||
"note": "27 Revenue over 5 Days · 2 train(s) on the timetable · 21 cards down · no collisions · played by developer",
|
||||
"note": "27 Revenue over 5 Days · 2 train(s) on the timetable · 19 cards down · no collisions · played by developer",
|
||||
"history": [
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
@@ -864,12 +864,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1026,12 +1021,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1147,6 +1137,205 @@
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.board",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "laborer.startLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"carType": "coach"
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.startLoad",
|
||||
"at": {
|
||||
@@ -1184,216 +1373,6 @@
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c48",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.board",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.startLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"carType": "coach"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 1
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c61",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c163",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "newTrain.placeCar",
|
||||
"trayId": "tray2",
|
||||
@@ -1432,22 +1411,25 @@
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 0
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
"index": 0
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c48",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "newTrain.placeCar",
|
||||
@@ -1468,6 +1450,54 @@
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c61",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c163",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "switch"
|
||||
},
|
||||
{
|
||||
"type": "switch.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
@@ -1503,16 +1533,6 @@
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "switch"
|
||||
},
|
||||
{
|
||||
"type": "switch.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
@@ -1528,6 +1548,42 @@
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.board",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
@@ -1572,42 +1628,6 @@
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.board",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.clearInbound",
|
||||
"at": {
|
||||
"row": 0,
|
||||
"col": 0
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
@@ -1648,44 +1668,6 @@
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 1
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c61",
|
||||
"toSlot": 1
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 1
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c150",
|
||||
"toSlot": 1
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "newTrain.placeCar",
|
||||
"trayId": "tray3",
|
||||
@@ -1718,16 +1700,13 @@
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 1
|
||||
},
|
||||
{
|
||||
"type": "card.play",
|
||||
"cardId": "c205",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -5
|
||||
},
|
||||
"variant": 0
|
||||
"type": "card.discard",
|
||||
"cardId": "c61",
|
||||
"toSlot": 1
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
@@ -1752,16 +1731,13 @@
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 1
|
||||
},
|
||||
{
|
||||
"type": "card.play",
|
||||
"cardId": "c126",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -6
|
||||
},
|
||||
"variant": 0
|
||||
"type": "card.discard",
|
||||
"cardId": "c150",
|
||||
"toSlot": 1
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
@@ -1778,10 +1754,10 @@
|
||||
},
|
||||
{
|
||||
"type": "card.play",
|
||||
"cardId": "c136",
|
||||
"cardId": "c205",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": 6
|
||||
"col": -5
|
||||
},
|
||||
"variant": 0
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+122
-212
@@ -524,12 +524,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -544,22 +539,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.play",
|
||||
"cardId": "c76",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -3
|
||||
},
|
||||
"variant": 0
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -574,22 +557,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.play",
|
||||
"cardId": "c181",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -2
|
||||
},
|
||||
"variant": 0
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -610,9 +581,13 @@
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
"type": "card.play",
|
||||
"cardId": "c76",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -3
|
||||
},
|
||||
"variant": 0
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
@@ -687,19 +662,15 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c36",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -717,13 +688,16 @@
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
"type": "card.play",
|
||||
"cardId": "c181",
|
||||
"placement": {
|
||||
"row": 0,
|
||||
"col": -2
|
||||
},
|
||||
"variant": 0
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
@@ -778,12 +752,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "porter.detrain",
|
||||
@@ -879,12 +848,11 @@
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "draw.fromHomeOffice"
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -903,19 +871,15 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1041,12 +1005,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1061,19 +1020,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1088,19 +1038,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "newTrain.placeCar",
|
||||
@@ -1135,7 +1076,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1154,19 +1095,15 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c36",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "porter.board",
|
||||
@@ -1203,7 +1140,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1248,7 +1185,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1292,12 +1229,7 @@
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carType": "boxcar"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1312,19 +1244,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1339,19 +1262,10 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c36",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"type": "freightAgent.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1374,7 +1288,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"cardId": "c36",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1405,19 +1319,15 @@
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
"option": "freightAgent"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
"type": "freightAgent.stockOutbound",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
"carType": "boxcar"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
@@ -1440,7 +1350,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1467,7 +1377,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c36",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1528,7 +1438,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1555,7 +1465,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"cardId": "c36",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1598,6 +1508,60 @@
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
@@ -1633,60 +1597,6 @@
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.beginUnload",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"carIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
"row": -1,
|
||||
"col": -2
|
||||
},
|
||||
"box": 2
|
||||
},
|
||||
{
|
||||
"type": "loadUnload.end"
|
||||
},
|
||||
{
|
||||
"type": "localOps.choose",
|
||||
"option": "draw"
|
||||
},
|
||||
{
|
||||
"type": "draw.fromDepartment",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
"type": "draw.end"
|
||||
},
|
||||
{
|
||||
"type": "laborer.advanceLoad",
|
||||
"at": {
|
||||
@@ -1708,7 +1618,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c41",
|
||||
"cardId": "c40",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1762,7 +1672,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c36",
|
||||
"cardId": "c53",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
@@ -1781,7 +1691,7 @@
|
||||
},
|
||||
{
|
||||
"type": "card.discard",
|
||||
"cardId": "c40",
|
||||
"cardId": "c41",
|
||||
"toSlot": 2
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+91
-9
@@ -297,11 +297,56 @@ function workTrack(f: Facility): [Load | null, Load | null, Load | null] {
|
||||
return f.menAtWork;
|
||||
}
|
||||
|
||||
/**
|
||||
* HOW MANY EMPTY CARS OF THIS TYPE ARE SPOTTED AND NOT ALREADY PROMISED TO A LOAD.
|
||||
*
|
||||
* §9.3 — a load has to have somewhere to go. You cannot start the MEN | AT | WORK moves until an
|
||||
* empty car of the right type is standing on the industry's track, "otherwise you are just dropping
|
||||
* cargo onto the tracks — pointless waste". The final swap needs a car of the LOAD's type, so a
|
||||
* hopper load cannot be swapped onto a tank: strict matching, not "any empty car".
|
||||
*
|
||||
* Counted rather than merely present, because green boxes and industry tracks both grow past one
|
||||
* slot with Modifiers (measured: capacity > 1 on 24.5% of freight facilities and a track longer than
|
||||
* one on 41%). Two loads staged against one spotted car would put the second one on the sign with
|
||||
* nowhere to land, which is the jam this rule exists to prevent — just one step later.
|
||||
*
|
||||
* Loads already in the green box and already walking the sign both count as claims on a car.
|
||||
*/
|
||||
export function unclaimedEmpties(f: Facility, type: CarType): number {
|
||||
const spotted = f.industryTrack.cars.filter((c) => !c.loaded && c.type === type).length;
|
||||
const staged = f.outboundBox.filter((c) => c.type === type).length;
|
||||
const working = (f.menAtWork ?? []).filter((l) => l?.dir === 'out' && l.type === type).length;
|
||||
return spotted - staged - working;
|
||||
}
|
||||
|
||||
/**
|
||||
* The green-box load that may start down the sign, or null if none may.
|
||||
*
|
||||
* Not simply `outboundBox[0]`. A Power Plant takes hoppers AND tanks, so a green box holding a
|
||||
* hopper load and a tank load against one spotted empty tank must start the TANK — taking the first
|
||||
* entry regardless would report the whole facility as blocked while a perfectly legal load sat
|
||||
* beside it.
|
||||
*/
|
||||
export function startableLoad(f: Facility): number | null {
|
||||
const claims = new Map<CarType, number>();
|
||||
for (let i = 0; i < f.outboundBox.length; i++) {
|
||||
const type = f.outboundBox[i]!.type;
|
||||
// Each earlier entry of the same type has first claim on the spotted cars.
|
||||
const ahead = claims.get(type) ?? 0;
|
||||
const working = (f.menAtWork ?? []).filter((l) => l?.dir === 'out' && l.type === type).length;
|
||||
const spotted = f.industryTrack.cars.filter((c) => !c.loaded && c.type === type).length;
|
||||
if (spotted - working - ahead > 0) return i;
|
||||
claims.set(type, ahead + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canStartLoad(f: Facility): boolean {
|
||||
if (!f.menAtWork) return false;
|
||||
if (laborersLeft(f) < 1) return false;
|
||||
if (f.outboundBox.length === 0) return false;
|
||||
return f.menAtWork[0] === null;
|
||||
if (f.menAtWork[0] !== null) return false;
|
||||
return startableLoad(f) !== null;
|
||||
}
|
||||
|
||||
/** §9.2 — boarding needs a loaded coach in a green slot and a train with an empty coach. */
|
||||
@@ -741,6 +786,15 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
|
||||
if (!s.yards.divisionYard.some((c) => c.type === i.carType && c.loaded)) {
|
||||
return 'NO_SUITABLE_CAR';
|
||||
}
|
||||
/**
|
||||
* §9.3 — FREIGHT NEEDS A CAR AT THE DOCK BEFORE THE CARGO IS FETCHED. Staging a load against
|
||||
* no car is fetching cargo you cannot ship.
|
||||
*
|
||||
* PASSENGERS ARE DELIBERATELY DIFFERENT and this is not an oversight: people can wait on the
|
||||
* platform for a train that has not arrived, so a Passenger Facility stocks freely. Freight
|
||||
* cannot — a crate on the ground is not a shipment.
|
||||
*/
|
||||
if (f.kind === 'freight' && unclaimedEmpties(f, i.carType) < 1) return 'NO_EMPTY_CAR_SPOTTED';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -844,7 +898,13 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
|
||||
const f = facilityAt(s, player, i.at);
|
||||
if (!f) return 'NO_SUCH_FACILITY';
|
||||
if (laborersLeft(f) < 1) return 'RESOURCE_SPENT';
|
||||
return canStartLoad(f) ? null : 'BOX_EMPTY';
|
||||
if (!f.menAtWork) return 'NO_SUCH_FACILITY';
|
||||
if (f.outboundBox.length === 0) return 'BOX_EMPTY';
|
||||
if (f.menAtWork[0] !== null) return 'BOX_FULL';
|
||||
// The car can be coupled away after the cargo was staged — running over an industry track
|
||||
// couples whatever stands on it, mandatorily (§A.4) — so the Freight Agent's check is not
|
||||
// enough on its own. Both gates are required.
|
||||
return startableLoad(f) !== null ? null : 'NO_EMPTY_CAR_SPOTTED';
|
||||
}
|
||||
|
||||
case 'laborer.advanceLoad': {
|
||||
@@ -874,7 +934,20 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
|
||||
// The load is placed on WORK, the last box, so that box must be free — and a Passenger
|
||||
// Facility has no such box, so there is nothing to unload into.
|
||||
if (!f.menAtWork) return 'NO_SUCH_FACILITY';
|
||||
return f.menAtWork[f.menAtWork.length - 1] === null ? null : 'BOX_FULL';
|
||||
if (f.menAtWork[f.menAtWork.length - 1] !== null) return 'BOX_FULL';
|
||||
/**
|
||||
* THE MIRROR OF THE LOAD RULE: a load coming IN needs somewhere to land too, and its
|
||||
* destination is the red Inbound box. This was checked only at the last step, so an unload
|
||||
* could be begun into a full box and walked W→A→M over three Stages before discovering it had
|
||||
* nowhere to go — jamming the industry, which is then locked and needs a Freight Agent turn to
|
||||
* clear. Measured: rare (2.2% of offers) but real, and seen jamming three times in 200 games.
|
||||
*
|
||||
* COUNTED, not just "is there a slot". Only the W box has to be free to begin, so once a load
|
||||
* moves W→A a second can start behind it — two loads walking toward one slot. Every red box in
|
||||
* play today holds exactly one car, which is precisely when that bites.
|
||||
*/
|
||||
const inFlight = f.menAtWork.filter((l) => l?.dir === 'in').length;
|
||||
return f.capacity.inbound - f.inboundBox.length > inFlight ? null : 'INBOUND_BOX_FULL';
|
||||
}
|
||||
|
||||
case 'loadUnload.end':
|
||||
@@ -1360,7 +1433,8 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
|
||||
case 'laborer.startLoad': {
|
||||
const f = facilityAt(s, player, i.at)!;
|
||||
return [{ type: 'loadStarted', player, at: i.at, carType: f.outboundBox[0]!.type }];
|
||||
// The first load with a car to land on, which is not always the first in the box.
|
||||
return [{ type: 'loadStarted', player, at: i.at, carType: f.outboundBox[startableLoad(f)!]!.type }];
|
||||
}
|
||||
|
||||
case 'laborer.advanceLoad': {
|
||||
@@ -1744,9 +1818,10 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
const ci = tray?.consist.findIndex((c) => c.type === 'coach' && c.loaded) ?? -1;
|
||||
if (tray && ci >= 0) {
|
||||
// The empty coach comes OUT OF THE DIVISION YARD, as §9.2 says. It used to be conjured,
|
||||
// which minted a coach on every de-training.
|
||||
// which minted a coach on every de-training. Throws now, for the reason in `unloadBegan`.
|
||||
const yi = s.yards.divisionYard.findIndex((c) => c.type === 'coach' && !c.loaded);
|
||||
const empty = yi >= 0 ? s.yards.divisionYard.splice(yi, 1)[0]! : { type: 'coach' as const, loaded: false };
|
||||
if (yi < 0) throw new Error('passengersDetrained: no empty coach in the Division Yard');
|
||||
const empty = s.yards.divisionYard.splice(yi, 1)[0]!;
|
||||
refillDivisionYardIfEmpty(s);
|
||||
f.inboundBox.push(tray.consist[ci]!);
|
||||
tray.consist[ci] = empty;
|
||||
@@ -1798,10 +1873,17 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
const f = facilityAt(s, e.player, e.at)!;
|
||||
const ci = f.industryTrack.cars.findIndex((c) => c.loaded);
|
||||
if (ci >= 0) {
|
||||
// §9.3 — the replacement empty comes out of the Division Yard. Conjuring it here is what
|
||||
// minted a car on every unload, and it also skipped a requirement the rule states.
|
||||
/**
|
||||
* §9.3 — the replacement empty comes out of the Division Yard. Conjuring it here is what
|
||||
* minted a car on every unload, and it also skipped a requirement the rule states.
|
||||
*
|
||||
* THROWS rather than falling back. `check` guarantees the car is there, so reaching this is
|
||||
* a broken invariant, and the old fallback quietly minted rolling stock instead of saying
|
||||
* so — which is exactly the shape of leak `TODO.md` spent a conservation audit chasing.
|
||||
*/
|
||||
const yi = s.yards.divisionYard.findIndex((c) => c.type === e.carType && !c.loaded);
|
||||
const empty = yi >= 0 ? s.yards.divisionYard.splice(yi, 1)[0]! : { type: e.carType, loaded: false };
|
||||
if (yi < 0) throw new Error(`unloadBegan: no empty ${e.carType} in the Division Yard`);
|
||||
const empty = s.yards.divisionYard.splice(yi, 1)[0]!;
|
||||
refillDivisionYardIfEmpty(s);
|
||||
f.industryTrack.cars[ci] = empty;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,12 @@ export type RejectionCode =
|
||||
* fallback fired 51 times with a coach train in front of the player: 27 with nobody waiting to
|
||||
* travel, 24 with passengers waiting and every coach on the train already full.
|
||||
*/
|
||||
/**
|
||||
* §9.3 — the green box may only be stocked, and a load may only start down the sign, when an empty
|
||||
* car of THAT type is standing on the industry's track and is not already promised to another
|
||||
* load. Fetching cargo you cannot ship is "just dropping it onto the tracks".
|
||||
*/
|
||||
| 'NO_EMPTY_CAR_SPOTTED'
|
||||
| 'NO_PORTERS_HERE'
|
||||
| 'NO_PASSENGERS_WAITING'
|
||||
| 'NO_EMPTY_COACH'
|
||||
|
||||
+23
-4
@@ -17,7 +17,7 @@
|
||||
|
||||
import { adTrackCount, coordKey, seatOf, turnOf } from '../engine/state.ts';
|
||||
import type { GameState, GridCoord, PlayerIndex, RollingStock, TrayId } from '../engine/state.ts';
|
||||
import { areaOf, canAdvanceLoad, canStartLoad, facilityCarType, laborersLeft, movesFor, portersLeft } from '../engine/apply.ts';
|
||||
import { areaOf, canAdvanceLoad, canStartLoad, facilityCarType, facilityCarTypes, laborersLeft, movesFor, portersLeft } from '../engine/apply.ts';
|
||||
import type { GameEvent } from '../engine/events.ts';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -478,17 +478,36 @@ export function impediments(s: GameState, player: PlayerIndex = 0): Impediment[]
|
||||
}
|
||||
|
||||
if (f.outboundBox.length > 0 && !canStartLoad(f) && laborersLeft(f) > 0) {
|
||||
// Two quite different reasons, and telling a player "MEN is occupied" when the real answer is
|
||||
// "there is no car to load onto" sends them to fix the wrong thing.
|
||||
const blockedByBox = f.menAtWork?.[0] != null;
|
||||
out.push({
|
||||
where: `${name} ${key}`,
|
||||
why: 'green box has a load but MEN is occupied',
|
||||
severity: 'waiting',
|
||||
why: blockedByBox
|
||||
? 'green box has a load but MEN is occupied'
|
||||
: `no empty ${want} spotted on this industry's track — a load has nowhere to go until a ` +
|
||||
'crew sets one out here (§9.3)',
|
||||
severity: blockedByBox ? 'waiting' : 'stuck',
|
||||
});
|
||||
}
|
||||
|
||||
if (f.allows.outbound && f.outboundBox.length === 0) {
|
||||
/**
|
||||
* WHAT TO DO NEXT, in the order you have to do it.
|
||||
*
|
||||
* A freight industry cannot be stocked at all until an empty car of its commodity is standing
|
||||
* on its track (§9.3) — so "needs a Freight Agent action" was the second step told to a player
|
||||
* who had not done the first, and the Freight Agent option simply would not be there. Measured
|
||||
* before the rule was enforced, 78% of the stocking moves the menu offered had no car to load
|
||||
* onto.
|
||||
*/
|
||||
const spotted = f.industryTrack.cars.some((c) => !c.loaded && facilityCarTypes(f).includes(c.type));
|
||||
out.push({
|
||||
where: `${name} ${key}`,
|
||||
why: 'green box empty — nothing to load (needs a Freight Agent action)',
|
||||
why: spotted
|
||||
? 'green box empty — nothing to load (needs a Freight Agent action)'
|
||||
: `green box empty, and no empty ${want} is spotted here — bring one in with a crew FIRST, ` +
|
||||
'then the Freight Agent can stage a load onto it (§9.3)',
|
||||
severity: 'waiting',
|
||||
});
|
||||
}
|
||||
|
||||
+4
-1
@@ -449,7 +449,10 @@ describe('Freight Agent operations (§6.3)', () => {
|
||||
inboundBox: [],
|
||||
capacity: { outbound: 3, inbound: 0 },
|
||||
menAtWork: [null, null, null],
|
||||
industryTrack: { length: 4, cars: [] },
|
||||
// An empty hopper is SPOTTED. §9.3 refuses to stage cargo against no car — "otherwise you
|
||||
// are just dropping cargo onto the tracks" — so a fixture with a bare track is a facility
|
||||
// the Freight Agent cannot legally touch.
|
||||
industryTrack: { length: 4, cars: [{ type: 'hopper', loaded: false }] },
|
||||
laborers: 3,
|
||||
porters: 0,
|
||||
usedThisStage: { laborers: 0, porters: 0 },
|
||||
|
||||
Reference in New Issue
Block a user