Fix parking trains, freight deadlock, and Whistle Post lock-in
This commit is contained in:
+25
-6
@@ -49,6 +49,7 @@ import {
|
||||
canDropCarsAt,
|
||||
canPlaceAt,
|
||||
facilityVariants,
|
||||
opposite,
|
||||
reachableDestinations,
|
||||
variantsFor,
|
||||
} from './track.ts';
|
||||
@@ -103,6 +104,7 @@ function occupancyFor(s: GameState, player: PlayerIndex, self: TrayId): Occupanc
|
||||
/** A tray's facing, expressed as the port it would leave by going forward. */
|
||||
function facingPort(s: GameState, trayId: TrayId): Port {
|
||||
const tray = s.trays.get(trayId);
|
||||
if (tray?.facing) return tray.facing;
|
||||
return tray?.direction === 'west' ? 'w' : 'e';
|
||||
}
|
||||
|
||||
@@ -392,11 +394,19 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo
|
||||
const here = trayCoord(s, i.trayId);
|
||||
if (!here) return 'CANNOT_DROP_HERE';
|
||||
if (i.count < 1 || i.count > tray.consist.length) return 'CONSIST_EMPTY';
|
||||
// The cut rolls into an ADJACENT industry — the engine never enters, which is the whole point
|
||||
// of the move and why it beats a normal spot.
|
||||
const adjacent =
|
||||
Math.abs(i.to.row - here.row) + Math.abs(i.to.col - here.col) === 1;
|
||||
if (!adjacent) return 'NOT_CONNECTED';
|
||||
// The cut is uncoupled and ROLLS to the industry under its own momentum, so the target must
|
||||
// be somewhere the train could itself have run to — track-connected, not merely a neighbouring
|
||||
// square. An earlier version tested orthogonal adjacency, which was wrong in both directions:
|
||||
// it would have allowed a cut to cross to a cell with no rail between, while refusing a siding
|
||||
// two cards along the same track. It also made the card effectively unplayable — held for
|
||||
// 1,400 turns across 60 games and legal on 2.
|
||||
const reachable = [
|
||||
...destinationsFor(s, player, i.trayId, here, false),
|
||||
...destinationsFor(s, player, i.trayId, here, true),
|
||||
];
|
||||
if (!reachable.some((d) => d.coord.row === i.to.row && d.coord.col === i.to.col)) {
|
||||
return 'NOT_CONNECTED';
|
||||
}
|
||||
const fsArea = areaOf(s, player);
|
||||
const target = fsArea.grid.get(coordKey(i.to));
|
||||
if (!target?.facility || target.facility.kind !== 'freight') return 'CANNOT_DROP_HERE';
|
||||
@@ -618,7 +628,11 @@ function destinationsFor(
|
||||
) {
|
||||
const tray = s.trays.get(trayId)!;
|
||||
const facing = facingPort(s, trayId);
|
||||
const exit: Port = reverse ? (facing === 'e' ? 'w' : 'e') : facing;
|
||||
// Reversing is the OPPOSITE port, whichever it is. This was hardcoded to flip between east and
|
||||
// west, so a crew facing north or south reversed to 'e' — a port a north-south card does not
|
||||
// have — and could never back out of a district spur. Combined with a facing that was itself
|
||||
// derived from an east/west direction, it stranded 29 of 62 leftover crews on north-south track.
|
||||
const exit: Port = reverse ? opposite(facing) : facing;
|
||||
return reachableDestinations(
|
||||
{
|
||||
area: areaOf(s, player),
|
||||
@@ -644,6 +658,9 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
const from = trayCoord(s, i.trayId)!;
|
||||
const dests = destinationsFor(s, player, i.trayId, from, i.reverse);
|
||||
const dest = dests.find((d) => d.coord.row === i.to.row && d.coord.col === i.to.col)!;
|
||||
// The crew leaves by the port opposite the one it entered through, which is what it will be
|
||||
// facing when it stops. Without this the facing stays 'e'/'w' forever and a crew that turns
|
||||
// onto a north-south spur can never move again.
|
||||
const events: GameEvent[] = [
|
||||
{
|
||||
type: 'trayMoved',
|
||||
@@ -651,6 +668,7 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
from,
|
||||
to: i.to,
|
||||
movesRemaining: s.turn.movesRemaining - 1,
|
||||
facing: opposite(dest.entry),
|
||||
},
|
||||
];
|
||||
if (dest.couples.length > 0) {
|
||||
@@ -941,6 +959,7 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
const tray = s.trays.get(e.trayId)!;
|
||||
const owner = tray.position.at === 'grid' ? tray.position.owner : 0;
|
||||
tray.position = { at: 'grid', owner, coord: e.to };
|
||||
if (e.facing) tray.facing = e.facing;
|
||||
s.turn.movesRemaining = e.movesRemaining;
|
||||
|
||||
// An A/D track is held only while the train is actually standing at the Office (§2.1).
|
||||
|
||||
Reference in New Issue
Block a user