diff --git a/public/replays/seed-1846127.json b/public/replays/seed-1759018.json similarity index 66% rename from public/replays/seed-1846127.json rename to public/replays/seed-1759018.json index 2a04732..269d78f 100644 --- a/public/replays/seed-1846127.json +++ b/public/replays/seed-1759018.json @@ -1,7 +1,7 @@ { - "seed": 1846127, - "title": "8 Revenue · seed 1846127", - "note": "8 Revenue over 5 Days · 1 train(s) on the timetable · 10 cards down · no collisions · played by developer", + "seed": 1759018, + "title": "8 Revenue · seed 1759018", + "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 18 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -19,11 +19,15 @@ }, { "type": "draw.fromDepartment", - "slot": 0 + "slot": 2 }, { "type": "card.play", - "cardId": "c117", + "cardId": "c24" + }, + { + "type": "card.play", + "cardId": "c78", "placement": { "row": 0, "col": 1 @@ -38,107 +42,15 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c20" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 21, - "start": { - "kind": "divisionPoint", - "side": "west" - } - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c94", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c0" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c40", - "placement": { - "row": 1, + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, "col": 0 }, - "variant": 0 - }, - { - "type": "draw.end" + "carType": "coach" }, { "type": "loadUnload.end" @@ -150,304 +62,6 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.discard", - "cardId": "c46", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c29", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.discard", - "cardId": "c91", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c52", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c70", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c80", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c29", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.discard", - "cardId": "c95", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c120", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c96", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c29", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.discard", - "cardId": "c71", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c29", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.discard", - "cardId": "c58", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c64", - "node": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c25" - }, { "type": "card.play", "cardId": "c17" @@ -490,6 +104,197 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "mainline.modify", + "cardId": "c69", + "node": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c72", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c107", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c89", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c29", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c104", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c73", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c43", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "freightAgent" @@ -502,6 +307,185 @@ }, "carType": "coach" }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c6" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c92", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c112", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c94", + "placement": { + "row": 1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c5" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c50", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c51", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c61", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, { "type": "porter.detrain", "at": { @@ -521,6 +505,34 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "freightAgent" @@ -533,6 +545,14 @@ }, "index": 0 }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray2" + }, { "type": "loadUnload.end" }, @@ -574,52 +594,67 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c12" - }, - { - "type": "card.play", - "cardId": "c107", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 + "type": "card.discard", + "cardId": "c55", + "toSlot": 1 }, { "type": "draw.end" }, { - "type": "newTrain.startExtra", - "trainNumber": 13, - "start": { - "kind": "divisionPoint", - "side": "west" - } + "type": "loadUnload.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true + "type": "localOps.choose", + "option": "draw" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true + "type": "draw.fromHomeOffice" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true + "type": "card.discard", + "cardId": "c31", + "toSlot": 2 }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c102", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c101", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -633,18 +668,31 @@ }, { "type": "card.play", - "cardId": "c93", + "cardId": "c77", "placement": { - "row": -1, + "row": 0, "col": 2 }, - "variant": 1 + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c29", + "cardId": "c82", "placement": { - "row": -1, + "row": 0, "col": 3 }, "variant": 0 @@ -652,73 +700,39 @@ { "type": "draw.end" }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": 2 + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c40", + "placement": { + "row": 1, + "col": -1 }, - "reverse": false + "variant": 0 }, { - "type": "switch.dropCars", + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", "trayId": "tray2", - "count": 1 + "carType": "boxcar", + "loaded": false }, { - "type": "switch.move", + "type": "newTrain.placeCar", "trayId": "tray2", - "to": { - "row": -1, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 + "carType": "coach", + "loaded": true }, { "type": "loadUnload.end" @@ -730,504 +744,26 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { "row": 0, "col": 0 }, - "reverse": true + "carType": "coach" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "porter.detrain", + "at": { "row": 0, "col": 0 }, - "reverse": true + "trayId": "tray2" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "porter.board", + "at": { "row": 0, "col": 0 }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" + "trayId": "tray2" }, { "type": "loadUnload.end" @@ -1256,91 +792,236 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "freightAgent" }, { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "freightAgent.clearInbound", + "at": { "row": 0, "col": 0 }, - "reverse": true + "index": 0 }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "porter.board", + "at": { "row": 0, "col": 0 }, - "reverse": true + "trayId": "tray3" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "porter.board", + "at": { "row": 0, "col": 0 }, - "reverse": true + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c87", + "placement": { + "row": 0, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c117", + "placement": { + "row": 0, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c60", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c84", + "placement": { + "row": 0, + "col": 4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c116", + "placement": { + "row": 0, + "col": 5 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c63", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c7", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -1357,10 +1038,215 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c7", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c7", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c7", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c7", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -1377,23 +1263,27 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" }, { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" + "type": "game.extend", + "player": 0, + "agree": false } ] } \ No newline at end of file diff --git a/public/replays/seed-674115.json b/public/replays/seed-191056.json similarity index 75% rename from public/replays/seed-674115.json rename to public/replays/seed-191056.json index 1bf9171..d429bcc 100644 --- a/public/replays/seed-674115.json +++ b/public/replays/seed-191056.json @@ -1,7 +1,7 @@ { - "seed": 674115, - "title": "8 Revenue · seed 674115", - "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 19 cards down · no collisions · played by developer", + "seed": 191056, + "title": "8 Revenue · seed 191056", + "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 18 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -19,7 +19,7 @@ }, { "type": "draw.fromDepartment", - "slot": 1 + "slot": 2 }, { "type": "card.play", @@ -27,279 +27,20 @@ }, { "type": "card.play", - "cardId": "c27" - }, - { - "type": "card.play", - "cardId": "c17" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 18, - "start": { - "kind": "divisionPoint", - "side": "west" - } - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c21" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 22, - "start": { - "kind": "divisionPoint", - "side": "west" - } - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c86", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c38", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "cardId": "c20" }, { "type": "card.play", "cardId": "c78", "placement": { "row": 0, - "col": -1 + "col": 1 }, "variant": 0 }, { "type": "draw.end" }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c20" - }, - { - "type": "draw.end" - }, { "type": "newTrain.startExtra", "trainNumber": 21, @@ -337,112 +78,30 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c61", - "placement": { + "type": "freightAgent.stockOutbound", + "at": { "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "mainline.modify", - "cardId": "c69", - "node": 3 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c111", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c89", - "placement": { - "row": -1, - "col": -2 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c30", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c44", - "placement": { - "row": -1, "col": 0 }, - "variant": 0 + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c6" }, { "type": "draw.end" @@ -455,15 +114,34 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 2 }, { "type": "card.play", - "cardId": "c8" + "cardId": "c10" }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, { "type": "loadUnload.end" }, @@ -472,16 +150,8 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c88", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 + "type": "draw.fromDepartment", + "slot": 1 }, { "type": "draw.end" @@ -511,7 +181,7 @@ }, { "type": "card.discard", - "cardId": "c71", + "cardId": "c101", "toSlot": 0 }, { @@ -527,31 +197,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c87", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.discard", - "cardId": "c62", + "cardId": "c29", "toSlot": 1 }, { @@ -564,17 +212,63 @@ "type": "localOps.choose", "option": "draw" }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.discard", + "cardId": "c53", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, { "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c118", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 + "type": "card.discard", + "cardId": "c49", + "toSlot": 2 }, { "type": "draw.end" @@ -591,30 +285,101 @@ }, { "type": "card.play", - "cardId": "c109", + "cardId": "c108", "placement": { "row": 0, - "col": 1 + "col": -1 }, "variant": 0 }, { "type": "card.play", - "cardId": "c91", + "cardId": "c93", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c29", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c31", "placement": { "row": -1, "col": 1 }, - "variant": 1 + "variant": 0 }, { "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" }, { "type": "newTrain.placeCar", @@ -645,13 +410,22 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c116", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c69", + "toSlot": 0 }, { "type": "draw.end" @@ -663,29 +437,6 @@ "type": "localOps.choose", "option": "switch" }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, { "type": "switch.end" }, @@ -699,25 +450,12 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.play", - "cardId": "c81", + "cardId": "c39", "placement": { - "row": 0, - "col": -4 + "row": 1, + "col": 0 }, "variant": 0 }, @@ -727,6 +465,21 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" @@ -735,19 +488,18 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c16" + "type": "card.discard", + "cardId": "c58", + "toSlot": 1 }, { "type": "draw.end" }, { - "type": "newTrain.startExtra", - "trainNumber": 17, - "start": { - "kind": "divisionPoint", - "side": "west" - } + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false }, { "type": "newTrain.placeCar", @@ -767,7 +519,30 @@ }, { "type": "card.play", - "cardId": "c9" + "cardId": "c45", + "placement": { + "row": -2, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c25", + "toSlot": 2 }, { "type": "draw.end" @@ -791,6 +566,16 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "freightAgent" @@ -830,32 +615,10 @@ }, { "type": "card.play", - "cardId": "c97", + "cardId": "c33", "placement": { "row": -1, - "col": -3 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c112", - "placement": { - "row": -1, - "col": 3 + "col": 2 }, "variant": 0 }, @@ -874,6 +637,256 @@ "carType": "boxcar", "loaded": false }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c82", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c28", + "toSlot": 0 + }, + { + "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" + }, + { + "type": "card.play", + "cardId": "c87", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c90", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c74", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c67", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c55", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, { "type": "newTrain.placeCar", "trayId": "tray3", @@ -898,9 +911,18 @@ }, { "type": "card.play", - "cardId": "c100", + "cardId": "c91", "placement": { - "row": -1, + "row": -2, + "col": 3 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c98", + "placement": { + "row": -2, "col": 4 }, "variant": 1 @@ -911,72 +933,227 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "switch" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c63", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c76", + "placement": { + "row": 0, "col": -3 }, - "reverse": true + "variant": 0 }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "type": "draw.end" }, { - "type": "switch.move", + "type": "newTrain.placeCar", "trayId": "tray3", - "to": { - "row": -1, - "col": -1 + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c64", + "node": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 }, - "reverse": false + "trayId": "tray3" }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "type": "loadUnload.end" }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "type": "localOps.choose", + "option": "switch" }, { "type": "switch.end" }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c5", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", - "loaded": false + "loaded": true }, { "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "caboose", "loaded": true }, @@ -985,45 +1162,38 @@ }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "draw" }, { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -1 - }, - "carType": "boxcar" + "type": "draw.fromDepartment", + "slot": 0 }, { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -1 - } + "type": "card.discard", + "cardId": "c5", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "draw" }, { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -1 - }, - "carType": "boxcar" + "type": "draw.fromDepartment", + "slot": 0 }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 0 + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -1036,360 +1206,127 @@ "type": "switch.end" }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 1 + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c5", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromDepartment", + "slot": 0 }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 2 + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromDepartment", + "slot": 0 }, { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -1 - } + "type": "card.discard", + "cardId": "c5", + "toSlot": 0 }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -1 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -1 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -1 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" + "type": "draw.end" }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", "loaded": false }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true + "type": "loadUnload.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "reefer", - "loaded": true + "type": "localOps.choose", + "option": "draw" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true + "type": "draw.fromDepartment", + "slot": 0 }, { - "type": "laborer.advanceLoad", + "type": "card.discard", + "cardId": "c3", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c5", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", "at": { - "row": -1, - "col": -1 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { "row": 0, "col": 0 }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true + "trayId": "tray3" }, { "type": "loadUnload.end" }, { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" + "type": "game.extend", + "player": 0, + "agree": false } ] } \ No newline at end of file diff --git a/public/replays/seed-1917398.json b/public/replays/seed-1917398.json index b71e49b..462c801 100644 --- a/public/replays/seed-1917398.json +++ b/public/replays/seed-1917398.json @@ -1389,6 +1389,11 @@ }, { "type": "loadUnload.end" + }, + { + "type": "game.extend", + "player": 0, + "agree": false } ] } \ No newline at end of file diff --git a/src/engine/advance.ts b/src/engine/advance.ts index af342dc..abd8fa3 100644 --- a/src/engine/advance.ts +++ b/src/engine/advance.ts @@ -50,6 +50,31 @@ export type AdvanceResult = { const NIGHT_STAGES = new Set([1, 2, 3, 11, 12]); +/** + * IS EVERY CAR ON THIS TRAIN LOADED? (Gitea#13) + * + * "You only get credit for a circus or campaign train (one point per stop) if you have it fully + * loaded. Not much of a circus if all the cars are empty." + * + * A COACH COUNTS AS LOADED WHEN IT IS OCCUPIED, which is what makes this the right test for the + * Campaign Train: X17 carries one coach and no freight, so "fully loaded" is precisely "the + * candidate is aboard" (Jesse's ruling, 2026-08-29). The engine already models an occupied coach + * as `loaded`, so no second notion is introduced here. + * + * A CABOOSE IS EXEMPT, and it costs nothing to say so: every caboose in `ROLLING_STOCK_SUPPLY` is + * minted `loaded: true` — there is no empty one — so including it would change no outcome today. + * It is excluded anyway because a caboose is crew space rather than payload, and a supply table + * that grew an empty caboose should not silently start voiding circus points. + * + * AN EMPTY TRAIN IS NOT FULLY LOADED. A Circus that departed short and carries nothing at all earns + * nothing: `every` on an empty list is vacuously true, which would pay the emptiest train of the + * lot, so the length is tested first. + */ +function fullyLoaded(tray: CrewTray): boolean { + const payload = tray.consist.filter((c) => c.type !== 'caboose'); + return payload.length > 0 && payload.every((c) => c.loaded); +} + function movesForStage(s: GameState): number { return s.config.optionalRules.reducedVisibility && NIGHT_STAGES.has(s.clock.stage) ? MOVES_PER_LOCAL_OPS_NIGHT @@ -420,33 +445,45 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult { const where = tray.position; const moved = moveTrain(s, id, tray, events); /** - * X18 CIRCUS TRAIN — "one turn stopped on any track (circus set-up) earns 1 point". + * X18 CIRCUS / X17 CAMPAIGN — a Stage spent set up in somebody's Office Area earns a point. * - * The flag was declared on the profile and read NOWHERE, so the one card in the deck that pays + * The flag was declared on the profile and read NOWHERE, so the one card in the deck that paid * for standing still paid nothing: reported from a playtest where TX18 sat on a siding for a - * full Stage and no point arrived. Claimed once — an Extra runs once and is gone. + * full Stage and no point arrived. + * + * ONCE PER OFFICE AREA (Gitea#13, Jesse 2026-08-29): "once per stop in an office area. In a + * multiplayer game, each player could score if the circus stops in their area." So a Circus + * touring three districts is paid three times and one parked in the same district all game is + * paid once, which `stopPointSeats` records per seat. + * + * ONLY IN AN OFFICE AREA. It used to pay for standing on the Mainline or at a Division Point + * too, and misattributed both: `playerAtSeat` needs a seat, and off the grid there is none, so + * the fallback handed the point to PLAYER 0 wherever the train happened to be. Scoping the rule + * to Office Areas is what Jesse's ruling says and it removes that bug rather than patching it. + * + * FULLY LOADED, or nothing. "Not much of a circus if all the cars are empty" — see + * `fullyLoaded` below for what that means for a train whose only car is a coach. * * "Stopped" is measured against the Mainline Phase: the train attempted to move and stayed where * it was. A train that is still in the district when the phase runs has not moved either, which * is the circus setting up on a siding rather than crossing the Division. */ - if (!tray.stopPointClaimed && trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra)?.rules.stopEarnsPoint) { + if (trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra)?.rules.stopEarnsPoint) { const stillThere = tray.position.at === where.at && (tray.position.at !== 'mainline' || where.at !== 'mainline' || tray.position.index === where.index) && (tray.position.at !== 'grid' || where.at !== 'grid' || (tray.position.coord.row === where.coord.row && tray.position.coord.col === where.coord.col)); - if (stillThere) { - tray.stopPointClaimed = true; + // Bound as one value so the grid case narrows: `tray.position` is a union, and testing a + // `seat` extracted from it does not tell the compiler which member it came from. + const at = tray.position.at === 'grid' ? tray.position : null; + const alreadyPaidHere = at !== null && (tray.stopPointSeats ?? []).includes(at.seat); + if (stillThere && at !== null && !alreadyPaidHere && fullyLoaded(tray)) { + tray.stopPointSeats = [...(tray.stopPointSeats ?? []), at.seat]; // The point goes to whoever is SITTING in the district it stopped in. - const owner = tray.position.at === 'grid' ? playerAtSeat(s, tray.position.seat) : 0; - const label = - tray.position.at === 'grid' - ? `(${tray.position.coord.col},${tray.position.coord.row})` - : tray.position.at === 'mainline' - ? `Mainline card ${tray.position.index}` - : `the ${tray.position.side} Division Point`; + const owner = playerAtSeat(s, at.seat); + const label = `(${at.coord.col},${at.coord.row})`; events.push({ type: 'trainStoodStill', trainNumber: tray.trainNumber ?? 0, where: label }); const p = s.players[owner]; if (p) { @@ -456,7 +493,7 @@ function mainlinePhase(s: GameState, events: GameEvent[]): AdvanceResult { player: owner, delta: 1, total: p.revenue, - reason: 'circus set-up — a Stage spent standing still', + reason: 'set up in the district — a Stage spent standing still, fully loaded', }); } } diff --git a/src/engine/apply.ts b/src/engine/apply.ts index 02175c3..94bcea6 100644 --- a/src/engine/apply.ts +++ b/src/engine/apply.ts @@ -1159,7 +1159,7 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo // This was not enforced at all: any car could be added in any quantity, so Train 9 "Heavy // Freight" — a card calling for 3 freight AND a caboose — was made up with four hoppers and // no caboose. Fewer is allowed; more, or of the wrong category, is not. - return acceptsCar(tray, i.carType) ? null : 'NO_SUITABLE_CAR'; + return acceptsCar(tray, i.carType, i.loaded, s.yards.divisionYard) ? null : 'NO_SUITABLE_CAR'; } case 'newTrain.secondSection': { @@ -2996,7 +2996,20 @@ function extendLimitsIfNeeded(area: OfficeArea, placed: GridCoord): void { * this. A second copy stalled the game outright: the phase believed a car could be added while * `check` rejected every option, so the Stage never completed. */ -export function acceptsCar(tray: CrewTray, carType: CarType): boolean { +export function acceptsCar( + tray: CrewTray, + carType: CarType, + /** + * Whether the car being offered is loaded, and what the Division Yard still holds. + * + * Both optional so that a caller asking the SHAPE question — "does this card take a car of this + * category at all?" — need not answer the loading question. `trainNeedingCars` asks the shape + * question of every car in the yard; `check` asks the full one about a specific car a player has + * named. Omitting them skips the loading rules rather than guessing at them. + */ + loaded?: boolean, + yard?: readonly RollingStock[], +): boolean { const profile = trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra); if (!profile) return true; @@ -3019,6 +3032,40 @@ export function acceptsCar(tray: CrewTray, carType: CarType): boolean { const types = profile.consist.freightTypes; if (adding === 'freight' && types && !types.includes(carType)) return false; + if (loaded === undefined) return true; + + /** + * X13 APPLESEED — "may drop MTs but not pick up anything", and its consist prints EMPTIES ONLY. + * + * `emptiesOnly` was declared on the card, RENDERED to the player as "(empties only)" by both + * `web/game.ts` and `sim/view.ts`, and enforced by nothing: the Appleseed could be made up with + * loaded cars while its own card said it could not. Found while building Gitea#13, which is the + * same rule pointing the other way, and fixed with it rather than left as the odd one out. + * + * A caboose is exempt. Every caboose in `ROLLING_STOCK_SUPPLY` is `loaded: true` — there is no + * such thing as an empty one — so applying this to the caboose would bar the Appleseed from the + * caboose its own consist calls for. + */ + if (profile.consist.emptiesOnly && loaded && adding !== 'caboose') return false; + + /** + * MUST RUN LOADED (Gitea#13) — a preference order, not a flat requirement. + * + * "If not loaded, then empty, and if none available, run without." So an EMPTY is refused only + * while the yard can still supply a loaded car this train would accept; once it cannot, the empty + * becomes legal and the train may also simply depart short. Asked of the yard rather than + * remembered on the tray, because the yard is what the rule is about and it changes under the + * train as other consists are built. + * + * The caboose is exempt for the same reason as above. + */ + if (profile.rules.mustRunLoaded && !loaded && adding !== 'caboose' && yard) { + const loadedAvailable = yard.some( + (c) => c.loaded && cat(c.type) === adding && acceptsCar(tray, c.type), + ); + if (loadedAvailable) return false; + } + return true; } @@ -3059,11 +3106,21 @@ export function isBeingMadeUp(tray: CrewTray): boolean { export function trainNeedingCars(s: GameState): TrayId | null { for (const [id, tray] of s.trays) { if (!isBeingMadeUp(tray)) continue; - // Consists are specified by CATEGORY — "Freight (2)" is any two freight cars — so any car in - // the yard is potentially suitable unless the card narrows it. Ask the SAME predicate `check` - // uses: a separate copy of this test stalled the game, because the phase believed a car could - // be added while `check` rejected every option, so the Stage never ended. - if (s.yards.divisionYard.some((c) => acceptsCar(tray, c.type))) return id; + /** + * Consists are specified by CATEGORY — "Freight (2)" is any two freight cars — so any car in + * the yard is potentially suitable unless the card narrows it. Ask the SAME predicate `check` + * uses: a separate copy of this test stalled the game, because the phase believed a car could + * be added while `check` rejected every option, so the Stage never ended. + * + * ASKED PER CAR, WITH ITS LOADED STATE, since Gitea#13. The shape question alone is no longer + * the same question `check` answers: an `emptiesOnly` train looking at a yard of nothing but + * loaded cars, or a `mustRunLoaded` train offered only empties while loaded ones remain, would + * both be told a car was available and then refused every one of them — the very stall this + * comment was written about. + */ + if (s.yards.divisionYard.some((c) => acceptsCar(tray, c.type, c.loaded, s.yards.divisionYard))) { + return id; + } } return null; } diff --git a/src/engine/content.ts b/src/engine/content.ts index 71c38dd..a913025 100644 --- a/src/engine/content.ts +++ b/src/engine/content.ts @@ -434,6 +434,19 @@ export type TrainRules = { /** X17 Campaign, X18 Circus: a scheduled stop that does something. */ stopEarnsPoint?: boolean; stopThenExpedite?: boolean; + /** + * MUST RUN LOADED (Gitea#13) — X17 Campaign, X18 Circus, X19 Military. + * + * "I've redefined some of the extra trains that they have to run full boxcars (not just any crazy + * stuff) — military trains, circus trains, etc. If not loaded, then empty, and if none available, + * run without." So it is a PREFERENCE ORDER enforced at make-up, not a flat requirement: a loaded + * car of an acceptable type must be taken while one is in the Division Yard; only once none is + * left may an empty be taken; and a train may still depart short (§8.2 already allows fewer cars + * than the card lists). + * + * Distinct from `ConsistSpec.emptiesOnly`, which is the opposite rule for X13 Appleseed. + */ + mustRunLoaded?: boolean; /** * `copiesNextScheduled` was here and is DELETED. No train card ever carried it: a Second Section * is a Maneuver card played on a train that is due out, and it has its own intent @@ -492,9 +505,9 @@ export const EXTRA_TRAINS: readonly TrainProfile[] = [ { number: 14, isExtra: true, name: 'Fruit Growers Express', speed: 'fast', direction: 'playerChoice', consist: { freight: 2, coach: 0, caboose: 1, freightTypes: ['reefer'] }, rules: { expedite: true, note: 'Reefers only. May pick up one extra loaded reefer.' } }, { number: 15, isExtra: true, name: 'Yard Xfer', speed: 'slow', direction: 'playerChoice', consist: { freight: 2, coach: 0, caboose: 1 }, rules: {} }, { number: 16, isExtra: true, name: 'Light Engine Move', speed: 'fast', direction: 'playerChoice', consist: { freight: 0, coach: 0, caboose: 0 }, rules: { noSwitching: true, note: 'No cars at all.' } }, - { number: 17, isExtra: true, name: 'Campaign Train', speed: 'fast', direction: 'playerChoice', consist: { freight: 0, coach: 1, caboose: 0 }, rules: { noSwitching: true, stopThenExpedite: true, note: 'One turn at station (speeches) then expedite.' } }, - { number: 18, isExtra: true, name: 'Circus Train', speed: 'slow', direction: 'playerChoice', consist: { freight: 2, coach: 1, caboose: 1 }, rules: { noSwitching: true, stopEarnsPoint: true, note: 'One turn stopped on any track (circus set-up) earns 1 point.' } }, - { number: 19, isExtra: true, name: 'Military Train', speed: 'slow', direction: 'playerChoice', consist: { freight: 1, coach: 2, caboose: 0 }, rules: { noSwitching: true, noPassengerWork: true, expedite: true } }, + { number: 17, isExtra: true, name: 'Campaign Train', speed: 'fast', direction: 'playerChoice', consist: { freight: 0, coach: 1, caboose: 0 }, rules: { noSwitching: true, stopThenExpedite: true, stopEarnsPoint: true, mustRunLoaded: true, note: 'One turn at station (speeches) then expedite. Earns a point per Office Area if the candidate is aboard.' } }, + { number: 18, isExtra: true, name: 'Circus Train', speed: 'slow', direction: 'playerChoice', consist: { freight: 2, coach: 1, caboose: 1 }, rules: { noSwitching: true, stopEarnsPoint: true, mustRunLoaded: true, note: 'One turn stopped in an Office Area (circus set-up) earns 1 point, once per Area, if fully loaded.' } }, + { number: 19, isExtra: true, name: 'Military Train', speed: 'slow', direction: 'playerChoice', consist: { freight: 1, coach: 2, caboose: 0 }, rules: { noSwitching: true, noPassengerWork: true, expedite: true, mustRunLoaded: true, note: 'Troops and materiel: runs loaded where the yard can supply it.' } }, { number: 20, isExtra: true, name: "Director's private car", speed: 'slow', direction: 'playerChoice', consist: { freight: 2, coach: 1, caboose: 0 }, rules: { noPassengerWork: true } }, { number: 21, isExtra: true, name: 'Freight Extra', speed: 'slow', direction: 'playerChoice', consist: { freight: 3, coach: 0, caboose: 1 }, rules: {} }, { number: 22, isExtra: true, name: 'Pee-Dee', speed: 'slow', direction: 'playerChoice', consist: { freight: 0, coach: 0, caboose: 1 }, rules: { pickUpEmptiesOnly: true, note: 'Per-diem train. May only pick up MTs.' } }, diff --git a/src/engine/state.ts b/src/engine/state.ts index c18c79b..ba07c97 100644 --- a/src/engine/state.ts +++ b/src/engine/state.ts @@ -445,20 +445,27 @@ export type CrewTray = { position: NodeRef; movesUsed: number; /** - * X18 Circus Train — "one turn stopped on any track (circus set-up) earns 1 point", claimed once. + * X18 Circus / X17 Campaign — Office Areas this train has already been paid for setting up in + * (Gitea#13). * - * Recorded on the tray rather than the player because it is the TRAIN that sets up, and an Extra - * runs once and is gone; there is no second visit to claim it on. + * "Once per stop in an office area. In a multiplayer game, each player could score if the circus + * stops in their area" (Jesse, 2026-08-29). So the claim is per SEAT, not per train: a Circus + * touring three districts is paid three times, and one that parks in the same district for six + * Stages is paid once. + * + * Recorded on the tray, which also gives the other half of Jesse's ruling for free — "if the + * circus train gets recycled and played a second time as a second extra, then it could again + * score points later too". A train is made up onto a FRESH tray object every time, so a re-played + * Extra starts with an empty list and no reset code is needed. */ - stopPointClaimed?: boolean; + stopPointSeats?: SeatIndex[]; /** * X17 Campaign Train — "one turn at station (speeches) then expedite". * * It makes its speech at the first Office it reaches: that arrival is an ordinary stop, and from * then on the train is expedited — it may be switched normally, but it faults (Q3) if it is left * off the Office square when a Mainline Phase begins. Recorded on the tray for the same reason as - * `stopPointClaimed` — it is the TRAIN that stops, and an Extra runs once, so there is no later - * visit to hang it on. + * `stopPointSeats` — it is the TRAIN that stops, and a re-played Extra gets a fresh tray. */ speechMade?: boolean; }; diff --git a/test/advance.test.ts b/test/advance.test.ts index 45ee103..bc9c052 100644 --- a/test/advance.test.ts +++ b/test/advance.test.ts @@ -749,43 +749,156 @@ describe('MILESTONE: a full solitaire game runs headless', () => { }); }); -describe('X18 Circus Train — a point for standing still', () => { - it('pays once for a Stage spent stopped, and never again', () => { - /** - * REPORTED: "Circus train TX18 was stopped on a siding for a full Stage and I did not get my - * Revenue point." It never could: `stopEarnsPoint` was declared on the profile and read - * NOWHERE, along with eight other special-train rules. The one card in the deck that pays for - * standing still paid nothing. - */ - const s = game(); +describe('X18 Circus / X17 Campaign — a point for setting up (Gitea#13)', () => { + /** + * REPORTED originally: "Circus train TX18 was stopped on a siding for a full Stage and I did not + * get my Revenue point." It never could: `stopEarnsPoint` was declared on the profile and read + * NOWHERE, along with eight other special-train rules. + * + * REDEFINED by Gitea#13 (Jesse, 2026-08-29), and these tests carry the three parts of that + * ruling: the point is paid ONCE PER OFFICE AREA rather than once per game, only when the train + * is FULLY LOADED, and only in an Office Area at all. + */ + const circusAt = (s: GameState, seat: number, coord: { row: number; col: number }, consist: unknown[]) => { s.clock.phase = 'mainline'; s.trays.set('circus', { id: 'circus', trainNumber: 18, trainIsExtra: true, engineAt: 0, - consist: [], direction: 'east', - position: { at: 'grid', seat: 0, coord: { row: -1, col: 0 } }, + consist, direction: 'east', + position: { at: 'grid', seat, coord }, movesUsed: 0, } as never); // A card under it, so the crew is somewhere real rather than off the grid. - areaOf(s, 0).grid.set('-1,0', { + areaOf(s, seat as never).grid.set(`${coord.row},${coord.col}`, { geometry: { kind: 'track', geometry: 'straight' }, baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [], } as never); + }; + const loaded = [ + { type: 'boxcar', loaded: true }, + { type: 'boxcar', loaded: true }, + { type: 'coach', loaded: true }, + { type: 'caboose', loaded: true }, + ]; + + const runPhase = (s: GameState) => { + s.clock.phase = 'mainline'; + s.movedThisPhase = new Set(); + return pump(s); + }; + + it('pays a fully loaded Circus for a Stage spent set up', () => { + const s = game(); + circusAt(s, 0, { row: -1, col: 0 }, loaded); const before = s.players[0]!.revenue; - const first = pump(s); assert.ok( - first.some((e) => e.type === 'trainStoodStill' && e.trainNumber === 18), + pump(s).some((e) => e.type === 'trainStoodStill' && e.trainNumber === 18), 'the Circus Train stood still for a Stage and earned nothing', ); assert.equal(s.players[0]!.revenue, before + 1, 'the point was not paid'); + }); - // "One turn stopped" — once. A train that goes on standing there does not keep earning. - const paidAgain = () => { - s.clock.phase = 'mainline'; - s.movedThisPhase = new Set(); - return pump(s).some((e) => e.type === 'trainStoodStill'); - }; - assert.ok(!paidAgain(), 'the Circus Train collected a second time for the same set-up'); + it('pays once per Office Area, however long it parks there', () => { + // "Once per stop in an office area" — a train that goes on standing in the same district does + // not keep earning. This is the half that was already true, for a different reason. + const s = game(); + circusAt(s, 0, { row: -1, col: 0 }, loaded); + pump(s); + assert.ok(!runPhase(s).some((e) => e.type === 'trainStoodStill'), + 'the Circus collected twice for the same set-up'); + assert.ok(!runPhase(s).some((e) => e.type === 'trainStoodStill'), + 'the Circus collected a third time for the same set-up'); + }); + + it('pays AGAIN in a different district — each player can be visited', () => { + /** + * The half that is new. "In a multiplayer game, each player could score if the circus stops in + * their area" — so the claim is per seat, and a touring Circus is paid by each district it sets + * up in. Before Gitea#13 this paid once per GAME and the second district got nothing. + */ + const s = createGame({ + id: 'g', seed: 5, config: baseConfig({ mode: 'competitive' }), playerNames: ['A', 'B'], + }); + circusAt(s, 0, { row: -1, col: 0 }, loaded); + pump(s); + const paidFirst = s.players.map((p) => p.revenue); + + // The same train, moved into the other player's district. + const tray = s.trays.get('circus')!; + areaOf(s, 1 as never).grid.set('-1,0', { + geometry: { kind: 'track', geometry: 'straight' }, + baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [], + } as never); + tray.position = { at: 'grid', seat: 1, coord: { row: -1, col: 0 } } as never; + + assert.ok(runPhase(s).some((e) => e.type === 'trainStoodStill'), + 'the Circus set up in a second district and earned nothing'); + const owner = s.seating[1]!; + assert.equal( + s.players[owner]!.revenue, + paidFirst[owner]! + 1, + 'the point did not go to whoever sits in the district it stopped in', + ); + }); + + it('pays nothing when the cars are empty — "not much of a circus"', () => { + const s = game(); + circusAt(s, 0, { row: -1, col: 0 }, [ + { type: 'boxcar', loaded: false }, + { type: 'coach', loaded: true }, + { type: 'caboose', loaded: true }, + ]); + const before = s.players[0]!.revenue; + assert.ok(!pump(s).some((e) => e.type === 'trainStoodStill'), + 'an empty car aboard still collected the set-up point'); + assert.equal(s.players[0]!.revenue, before, 'Revenue moved for a train that was not full'); + }); + + it('pays nothing to a train carrying nothing at all', () => { + // `every` on an empty list is vacuously true, so the emptiest train of the lot is exactly the + // one a careless test would pay. + const s = game(); + circusAt(s, 0, { row: -1, col: 0 }, []); + assert.ok(!pump(s).some((e) => e.type === 'trainStoodStill'), + 'a Circus carrying nothing was paid for setting up'); + }); + + it('pays nothing for standing out on the Mainline', () => { + /** + * It used to, and it misattributed the point: `playerAtSeat` needs a seat, there is none off + * the grid, and the fallback handed it to PLAYER 0 wherever the train was standing. Jesse's + * ruling scopes the rule to Office Areas, which removes the bug rather than patching it. + */ + const s = game(); + s.clock.phase = 'mainline'; + const index = s.division.nodes.findIndex((n) => n.kind === 'mainline'); + s.trays.set('circus', { + id: 'circus', trainNumber: 18, trainIsExtra: true, engineAt: 0, + consist: loaded, direction: 'east', + position: { at: 'mainline', index }, + movesUsed: 0, + } as never); + const before = s.players[0]!.revenue; + pump(s); + assert.equal(s.players[0]!.revenue, before, 'a Mainline set-up paid a point'); + }); + + it('pays the Campaign Train only when its candidate is aboard', () => { + // X17 carries one coach and no freight, so "fully loaded" is exactly "the coach is occupied". + // It earned nothing at all before Gitea#13 — it had `stopThenExpedite` and no scoring rule. + const occupied = game(); + circusAt(occupied, 0, { row: -1, col: 0 }, [{ type: 'coach', loaded: true }]); + occupied.trays.get('circus')!.trainNumber = 17; + const beforeOccupied = occupied.players[0]!.revenue; + pump(occupied); + assert.equal(occupied.players[0]!.revenue, beforeOccupied + 1, 'a full Campaign Train earned nothing'); + + const empty = game(); + circusAt(empty, 0, { row: -1, col: 0 }, [{ type: 'coach', loaded: false }]); + empty.trays.get('circus')!.trainNumber = 17; + const beforeEmpty = empty.players[0]!.revenue; + pump(empty); + assert.equal(empty.players[0]!.revenue, beforeEmpty, 'an empty Campaign Train was paid for its speech'); }); }); diff --git a/test/mainline-cards.test.ts b/test/mainline-cards.test.ts index f87c41d..3fdee5e 100644 --- a/test/mainline-cards.test.ts +++ b/test/mainline-cards.test.ts @@ -770,6 +770,100 @@ describe('a turnout may be laid on top of a card already down', () => { }); }); +describe('extras that must run loaded, and one that must not (Gitea#13)', () => { + /** + * "I've redefined some of the extra trains that they have to run full boxcars — military trains, + * circus trains, etc. If not loaded, then empty, and if none available, run without." + * + * A PREFERENCE ORDER, so every test here is about what the DIVISION YARD still holds. The rule + * has nothing to say about a train once it is running; it decides which car may be taken next. + */ + const madeUp = (trainNumber: number) => { + const s = game(); + s.clock.phase = 'newTrain'; + s.trays.set('t', { + id: 't', trainNumber, trainIsExtra: true, engineAt: 0, + consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, + }); + return s; + }; + const place = (carType: string, loaded: boolean) => + ({ type: 'newTrain.placeCar', trayId: 't', carType, loaded }) as never; + /** Leaves the Division Yard holding exactly the cars described. */ + const stockYard = (s: ReturnType, cars: { type: string; loaded: boolean }[]) => { + s.yards.divisionYard.length = 0; + s.yards.divisionYard.push(...(cars as never[])); + }; + + it('refuses an empty while the yard can still supply a loaded one (X18 Circus)', () => { + const s = madeUp(18); + stockYard(s, [{ type: 'boxcar', loaded: true }, { type: 'boxcar', loaded: false }]); + assert.equal(check(s, 0, place('boxcar', false)), 'NO_SUITABLE_CAR', + 'an empty was accepted while a loaded boxcar was still in the yard'); + assert.equal(check(s, 0, place('boxcar', true)), null, 'the loaded boxcar was refused'); + }); + + it('accepts an empty once the yard has no loaded car of that kind left', () => { + // "If not loaded, then empty." The rule releases as soon as the yard cannot supply. + const s = madeUp(18); + stockYard(s, [{ type: 'boxcar', loaded: false }]); + assert.equal(check(s, 0, place('boxcar', false)), null, + 'an empty was refused when the yard held no loaded car at all'); + }); + + it('does not let a loaded car of the WRONG category unlock the rule', () => { + // A loaded coach is no reason to refuse an empty boxcar: the preference is per category, since + // that is the slot the car is competing for. + const s = madeUp(18); + stockYard(s, [{ type: 'coach', loaded: true }, { type: 'boxcar', loaded: false }]); + assert.equal(check(s, 0, place('boxcar', false)), null, + 'a loaded coach blocked an empty boxcar'); + }); + + it('exempts the caboose, which is never empty in the supply', () => { + const s = madeUp(18); + stockYard(s, [{ type: 'caboose', loaded: true }, { type: 'boxcar', loaded: true }]); + assert.equal(check(s, 0, place('caboose', true)), null, 'the caboose its card calls for was refused'); + }); + + it('applies to the Military train too', () => { + const s = madeUp(19); + stockYard(s, [{ type: 'coach', loaded: true }, { type: 'coach', loaded: false }]); + assert.equal(check(s, 0, place('coach', false)), 'NO_SUITABLE_CAR', + 'the Military train took an empty coach over a loaded one'); + }); + + it('leaves trains without the rule alone', () => { + // X21 Freight Extra has no loading rule: an empty is as good as a loaded one. + const s = madeUp(21); + stockYard(s, [{ type: 'boxcar', loaded: true }, { type: 'boxcar', loaded: false }]); + assert.equal(check(s, 0, place('boxcar', false)), null, + 'a train with no loading rule was made to prefer loaded cars'); + }); + + it('REGRESSION: X13 Appleseed is empties-only, and now the rules say so too', () => { + /** + * `ConsistSpec.emptiesOnly` was declared on the card, RENDERED to the player as "(empties only)" + * by `web/game.ts` and `sim/view.ts`, and enforced by NOTHING — `acceptsCar` never read it. So + * the Appleseed could be made up with loaded cars while its own card said it could not. Found + * while building Gitea#13, which is the same rule pointing the other way. + */ + const s = madeUp(13); + stockYard(s, [{ type: 'boxcar', loaded: true }, { type: 'boxcar', loaded: false }]); + assert.equal(check(s, 0, place('boxcar', true)), 'NO_SUITABLE_CAR', + 'the Appleseed took a loaded car despite printing "empties only"'); + assert.equal(check(s, 0, place('boxcar', false)), null, 'the Appleseed refused an empty'); + }); + + it('still lets the Appleseed take the caboose its consist calls for', () => { + // Every caboose in ROLLING_STOCK_SUPPLY is minted loaded, so an unexempted empties-only rule + // would bar the one car the card explicitly lists. + const s = madeUp(13); + stockYard(s, [{ type: 'caboose', loaded: true }]); + assert.equal(check(s, 0, place('caboose', true)), null, 'the empties-only rule ate the caboose'); + }); +}); + describe("a train is made up to its card's consist (§8.2)", () => { it('takes a caboose when the card calls for one, and refuses a fourth freight car', () => { // Train 9 "Heavy Freight" is freight 3 + caboose 1. It was being made up with FOUR hoppers and