Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button

This commit is contained in:
Jesse
2026-08-05 08:28:22 -04:00
parent 2d348e1ecf
commit 9c76b1a5e2
28 changed files with 1927 additions and 394 deletions
+10 -19
View File
@@ -90,6 +90,9 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
}
for (let n = 1; n <= tray.consist.length; n++) {
out.push({ type: 'switch.dropCars', trayId, count: n });
// Off the nose as well as the tail — the only way to get cars back off the front of a train
// that shoved a cut, and therefore the only way an engine buried mid-train reaches an end.
out.push({ type: 'switch.dropCars', trayId, count: n, fromNose: true });
}
// Small Yard: enumerating every permutation would explode, so offer the useful ones —
// bringing each car to the droppable end, plus a full reversal. `check` validates any order,
@@ -136,31 +139,19 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
});
for (const cardId of s.decks.hands.get(player) ?? []) {
out.push({ type: 'card.play', cardId });
// Gap 11 — orientation is chosen on placement, so every rotation is a distinct candidate.
// Six is the widest set (turnouts); `check` discards the ones whose ports do not meet.
const targets = s.cards.get(cardId)?.kind.kind === 'enhancement' ? attachments : placements;
const kind = s.cards.get(cardId)?.kind;
// Enhancements ATTACH to a card already down, so their candidates are the occupied cells.
const targets = kind?.kind === 'enhancement' ? attachments : placements;
// Orientation is chosen on placement, and a printed card turns but never flips, so the widest
// variant set is TWO. Anything that is not track has a single orientation and needs one entry.
const rotations = kind?.kind === 'track' ? variantsFor(kind.geometry, kind.hand).length : 1;
for (const placement of targets) {
// A printed card turns but does not flip, so the widest variant set is TWO. This offered six
// per placement per card back when a curve could be laid at all four rotations; four of every
// six were dead weight in an option list the bot scores in full.
for (let variant = 0; variant < 2; variant++) {
for (let variant = 0; variant < rotations; variant++) {
out.push({ type: 'card.play', cardId, placement, variant });
}
}
for (let slot = 0; slot < 3; slot++) out.push({ type: 'card.discard', cardId, toSlot: slot });
}
// Lay track from the personal supply — the only way a district grows now that track is not in
// the deck.
for (const [key, count] of s.turn.laidThisTurn ? [] : area.trackSupply) {
if (count < 1) continue;
const [geometry, hand] = key.split(':') as [TrackGeometry, Hand];
for (const placement of placements) {
// 0° and 180°. A straight is the same card either way; `check` discards the duplicate.
for (let variant = 0; variant < variantsFor(geometry, hand).length; variant++) {
out.push({ type: 'track.lay', geometry, hand, placement, variant });
}
}
}
// Mainline modifiers go on a Mainline card, not a grid cell, so `node` indexes division.nodes.
for (const cardId of s.decks.hands.get(player) ?? []) {
if (s.cards.get(cardId)?.kind.kind !== 'mainlineModifier') continue;