v0.3.0 — playtest fixes, two new rules, and §7 enforced

This commit is contained in:
Jesse
2026-08-12 21:08:28 -04:00
parent 6ead39c530
commit 1bf1e95058
38 changed files with 8514 additions and 5989 deletions
+28 -2
View File
@@ -156,7 +156,22 @@ function localOpsCandidates(s: GameState, player: PlayerIndex): Intent[] {
out.push({ type: 'card.play', cardId, node });
}
}
const targets = onMainline ? [] : kind?.kind === 'enhancement' ? attachments : placements;
/**
* A TURNOUT MAY ALSO UPGRADE A CARD ALREADY DOWN, so it is offered the occupied cells as well as
* the empty ones — `check` decides which of them it can actually be laid over. Without this the
* upgrade rule exists in the engine and is never once presented, which is precisely how the 18
* Enhancement cards came to be permanently dead.
*
* Deduplicated because the two lists overlap: `placements` already contains the Limits signs.
*/
const isTurnout = kind?.kind === 'track' && kind.geometry === 'turnout';
const targets = onMainline
? []
: kind?.kind === 'enhancement'
? attachments
: isTurnout
? dedupe([...placements, ...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;
@@ -226,7 +241,8 @@ function loadUnloadCandidates(s: GameState, player: PlayerIndex): Intent[] {
const f = area.grid.get(`${coord.row},${coord.col}`)?.facility;
if (f) {
out.push({ type: 'laborer.startLoad', at: coord });
for (let box = 0; box < f.menAtWork.length; box++) {
// A Passenger Facility has no MEN | AT | WORK boxes to advance a load along.
for (let box = 0; box < (f.menAtWork?.length ?? 0); box++) {
out.push({ type: 'laborer.advanceLoad', at: coord, box });
}
for (let ci = 0; ci < f.industryTrack.cars.length; ci++) {
@@ -248,6 +264,16 @@ function facilityCoords(s: GameState, player: PlayerIndex): GridCoord[] {
return out;
}
function dedupe(coords: GridCoord[]): GridCoord[] {
const seen = new Set<string>();
return coords.filter((c) => {
const k = `${c.row},${c.col}`;
if (seen.has(k)) return false;
seen.add(k);
return true;
});
}
/** Empty cells adjacent to occupied ones — `check` decides which actually connect. */
function placementCandidates(s: GameState, player: PlayerIndex): GridCoord[] {
const area = areaOf(s, player);