Implement Enhancements, Mainline modifiers and Maneuvers; fix unplayable track
This commit is contained in:
+68
-2
@@ -12,7 +12,7 @@
|
||||
* If you find yourself writing a rule here, it belongs in apply.ts.
|
||||
*/
|
||||
|
||||
import type { CarType } from './content.ts';
|
||||
import type { CarType, Hand, TrackGeometry } from './content.ts';
|
||||
import { check, areaOf, destinationsFor } from './apply.ts';
|
||||
import type { Intent } from './intents.ts';
|
||||
import type { GameState, GridCoord, PlayerIndex } from './state.ts';
|
||||
@@ -56,6 +56,14 @@ function candidates(s: GameState, player: PlayerIndex): Intent[] {
|
||||
break;
|
||||
}
|
||||
|
||||
// Red Flags — "any time", so they are candidates in every phase, on any train standing out on
|
||||
// the Mainline (the player's own or another's: protecting a train is not an attack).
|
||||
for (const cardId of s.decks.hands.get(player) ?? []) {
|
||||
const k = s.cards.get(cardId)?.kind;
|
||||
if (k?.kind !== 'maneuver' || k.key !== 'redFlags') continue;
|
||||
for (const [trayId] of s.trays) out.push({ type: 'maneuver.redFlags', cardId, trayId });
|
||||
}
|
||||
|
||||
out.push({ type: 'redFlag.play' });
|
||||
return out;
|
||||
}
|
||||
@@ -82,6 +90,38 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
|
||||
for (let n = 1; n <= tray.consist.length; n++) {
|
||||
out.push({ type: 'switch.dropCars', trayId, count: n });
|
||||
}
|
||||
// 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,
|
||||
// so a UI may submit an arbitrary permutation.
|
||||
const n = tray.consist.length;
|
||||
if (n > 1) {
|
||||
for (let k = 0; k < n; k++) {
|
||||
const order = [...Array(n).keys()].filter((x) => x !== k);
|
||||
order.push(k);
|
||||
out.push({ type: 'switch.sortConsist', trayId, order });
|
||||
}
|
||||
out.push({ type: 'switch.sortConsist', trayId, order: [...Array(n).keys()].reverse() });
|
||||
}
|
||||
}
|
||||
// Flying Switch — roll a cut into an ADJACENT industry without the engine entering it.
|
||||
for (const cardId of s.decks.hands.get(player) ?? []) {
|
||||
const k = s.cards.get(cardId)?.kind;
|
||||
if (k?.kind !== 'maneuver' || k.key !== 'flyingSwitch') continue;
|
||||
for (const [trayId, tray] of s.trays) {
|
||||
if (tray.position.at !== 'grid' || tray.position.owner !== player) continue;
|
||||
const { row, col } = tray.position.coord;
|
||||
const around: GridCoord[] = [
|
||||
{ row: row + 1, col },
|
||||
{ row: row - 1, col },
|
||||
{ row, col: col + 1 },
|
||||
{ row, col: col - 1 },
|
||||
];
|
||||
for (const to of around) {
|
||||
for (let count = 1; count <= tray.consist.length; count++) {
|
||||
out.push({ type: 'maneuver.flyingSwitch', cardId, trayId, count, to });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push({ type: 'switch.end' });
|
||||
|
||||
@@ -90,17 +130,43 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
|
||||
for (let slot = 0; slot < 3; slot++) out.push({ type: 'draw.fromDepartment', slot });
|
||||
|
||||
const placements = placementCandidates(s, player);
|
||||
// Enhancements ATTACH to a card already in the grid, so their candidates are the occupied cells,
|
||||
// not the empty ones every other placeable card wants. Offering them `placements` meant an
|
||||
// Enhancement was never once legal on a real card — 18 of 93 solitaire cards, permanently dead.
|
||||
const attachments = [...area.grid.keys()].map((k) => {
|
||||
const [row, col] = k.split(',').map(Number);
|
||||
return { row: row!, col: col! };
|
||||
});
|
||||
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.
|
||||
for (const placement of placements) {
|
||||
const targets = s.cards.get(cardId)?.kind.kind === 'enhancement' ? attachments : placements;
|
||||
for (const placement of targets) {
|
||||
for (let variant = 0; variant < 6; 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) {
|
||||
for (let variant = 0; variant < 2; 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;
|
||||
for (let node = 0; node < s.division.nodes.length; node++) {
|
||||
out.push({ type: 'mainline.modify', cardId, node });
|
||||
}
|
||||
}
|
||||
out.push({ type: 'draw.end' });
|
||||
|
||||
// -- freight agent (§6.3)
|
||||
|
||||
Reference in New Issue
Block a user