user card Hand becomes the action surface, the board shows the train, and a reverse stops turning it around, other bug fixes too.

This commit is contained in:
Jesse
2026-08-05 19:18:26 -04:00
parent 1c321d4cea
commit 543584a2ca
20 changed files with 1851 additions and 320 deletions
+14 -1
View File
@@ -719,7 +719,20 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
from,
to: i.to,
movesRemaining: s.turn.movesRemaining - 1,
facing: opposite(dest.entry),
/**
* A TRAIN THAT BACKS UP HAS NOT TURNED AROUND.
*
* `facing` is which way the ENGINE points, and this set it to the direction of travel on
* every move — so one reverse move silently spun the train about. Everything then read
* "forward" again, and a run-around became pointless: you could change ends for free by
* backing up twice.
*
* Running forward the engine leads, so it points the way the train went: `opposite(entry)`.
* Backing up it trails, still pointing the way it came, which is the port it arrived
* through. Both hold around a curve, where the compass heading changes but the engine's
* relationship to its train does not.
*/
facing: i.reverse ? dest.entry : opposite(dest.entry),
},
];
if (dest.couples.length > 0) {
+14 -2
View File
@@ -13,6 +13,7 @@
*/
import type { CarType, Hand, TrackGeometry } from './content.ts';
import { enhancementRule } from './content.ts';
import { check, areaOf, destinationsFor } from './apply.ts';
import type { Intent } from './intents.ts';
import type { GameState, GridCoord, PlayerIndex } from './state.ts';
@@ -140,8 +141,19 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
for (const cardId of s.decks.hands.get(player) ?? []) {
out.push({ type: 'card.play', cardId });
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;
/**
* Enhancements ATTACH to a card already down, so their candidates are the occupied cells — with
* one exception. ABS Signals goes on a MAINLINE card, and `checkEnhancementPlacement` reads
* `placement.col` as a Division node index for it. Offering it grid squares meant the engine
* accepted "(0, 1)" because node 1 happened to be a Mainline card: the label said one thing and
* the placement meant another, so the card appeared to be playable all over the Office Area.
*/
const onMainline = kind?.kind === 'enhancement' && enhancementRule(kind.key)?.placement === 'mainlineCard';
const targets = onMainline
? s.division.nodes.map((_, node) => ({ row: -1, col: node }))
: 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;