v0.4.8 — the Limits bound the whole district, the nine spots really are nine, and an action points at its square
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQAJ4dND7enLj54eLyiF2C
This commit is contained in:
@@ -77,6 +77,7 @@ import {
|
||||
opposite,
|
||||
reachableDestinations,
|
||||
variantsFor,
|
||||
withinLimits,
|
||||
} from './track.ts';
|
||||
|
||||
export type ApplyResult =
|
||||
@@ -1105,6 +1106,9 @@ function checkPlay(
|
||||
if (placement.row === area.runningRow && !carriesThroughTrack(proto)) {
|
||||
return 'BREAKS_RUNNING_TRACK';
|
||||
}
|
||||
// Same reasoning: a siding reaching past your own sign JOINS perfectly well, and is refused
|
||||
// because it leaves your territory (§2.1). `canPlaceAt` enforces it too — this only names it.
|
||||
if (!withinLimits(area, placement)) return 'OUTSIDE_LIMITS';
|
||||
return canPlaceAt(area, placement, proto) ? null : 'NOT_CONNECTED';
|
||||
}
|
||||
|
||||
@@ -1123,6 +1127,10 @@ function checkPlay(
|
||||
* main line.
|
||||
*/
|
||||
if (placement.row === area.runningRow) return 'ON_RUNNING_TRACK';
|
||||
// A Facility CARRIES TRACK — "placing a Facility places track" (§11.2) — so it is bounded by
|
||||
// the Limits exactly as a siding is. An industry outside them is how a district used to leave
|
||||
// its own territory.
|
||||
if (!withinLimits(area, placement)) return 'OUTSIDE_LIMITS';
|
||||
// Q4 — a lockout prevents BUILDING both in one district: no duplicate, and never a producer
|
||||
// alongside the consumer of the same commodity.
|
||||
if (isLockedOut(area, card.kind.facility)) return 'FACILITY_LOCKED';
|
||||
@@ -1133,6 +1141,18 @@ function checkPlay(
|
||||
case 'modifier': {
|
||||
if (!placement) return 'NO_PLACEMENT';
|
||||
if (area.grid.has(coordKey(placement))) return 'NOT_CONNECTED';
|
||||
/**
|
||||
* NOT ON THE RUNNING TRACK ROW — AND NOT BOUNDED BY THE LIMITS EITHER. Jesse's call, both
|
||||
* halves.
|
||||
*
|
||||
* A Modifier is not track (§9), so unlike a siding it may hang outside the Limits: a Facility
|
||||
* standing at the limit has three of its nine spots out there, and refusing them would make
|
||||
* the card unplayable exactly where the district ends. What it may NOT do is stand in the row
|
||||
* the Running Track grows along. Inside the Limits that row is always full, so this bites only
|
||||
* beyond the sign — which is the ground the main extends onto, and a Modifier parked there
|
||||
* would block your own sign from moving outward (§2.1) with nothing on screen to warn you.
|
||||
*/
|
||||
if (placement.row === area.runningRow) return 'ON_RUNNING_TRACK';
|
||||
// One of a kind per Office Area, as with industries (Q4) — no district gets two Ice Houses.
|
||||
if (hasModifierInArea(area, card.kind.modifier)) return 'FACILITY_LOCKED';
|
||||
// §9 — a Modifier is not track. It must sit adjacent to a Facility THAT CAN HOST IT (one of
|
||||
|
||||
@@ -143,6 +143,11 @@ export type RejectionCode =
|
||||
| 'ON_RUNNING_TRACK'
|
||||
/** A card with no east-west road would dead-end the Running Track it was laid in. */
|
||||
| 'BREAKS_RUNNING_TRACK'
|
||||
/**
|
||||
* §2.1 — track, and the Facilities that carry it, stay inside your own Limits at every row. A
|
||||
* Modifier is not track and is exempt (§9's nine spots), which is why this is not shared with it.
|
||||
*/
|
||||
| 'OUTSIDE_LIMITS'
|
||||
/**
|
||||
* A turnout may be laid on top of a straight, or of a curve on the same arc as its diverging leg
|
||||
* — but only those, and only while the square is idle. See `checkTurnoutUpgrade`.
|
||||
|
||||
@@ -313,5 +313,35 @@ function placementCandidates(s: GameState, player: PlayerIndex): GridCoord[] {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* THE NINE-SPOT MODIFIER NEIGHBOURHOOD (§9), which the four-square walk above cannot reach.
|
||||
*
|
||||
* A Modifier goes "adjacent to a Facility, on any of the nine nearby spots" — diagonals included —
|
||||
* and `check` has always accepted all eight. It was the CANDIDATES that were orthogonal-only, so a
|
||||
* diagonal square with no orthogonal neighbour was legal and never offered: reported by Jesse as
|
||||
* being unable to place a Modifier to the south-east of his industry.
|
||||
*
|
||||
* Only around a Facility, and only in a second pass. Track has to JOIN, and a diagonal shares no
|
||||
* edge, so a Modifier is the only card these squares can ever take — generating diagonals around
|
||||
* every card instead costs 54% more simulation time for candidates `check` then rejects. Appending
|
||||
* rather than interleaving leaves the existing order untouched, which matters because the bot
|
||||
* breaks ties by first-best: measured over 200 seeded games, 0 played differently.
|
||||
*/
|
||||
for (const [key, card] of area.grid) {
|
||||
if (!card.facility) continue;
|
||||
const [row, col] = key.split(',').map(Number);
|
||||
for (const c of [
|
||||
{ row: row! + 1, col: col! + 1 },
|
||||
{ row: row! + 1, col: col! - 1 },
|
||||
{ row: row! - 1, col: col! + 1 },
|
||||
{ row: row! - 1, col: col! - 1 },
|
||||
]) {
|
||||
const k = `${c.row},${c.col}`;
|
||||
if (seen.has(k) || area.grid.has(k)) continue;
|
||||
seen.add(k);
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
+31
-8
@@ -559,6 +559,31 @@ export function carriesThroughTrack(card: TrackCard): boolean {
|
||||
* a TURNOUT laid on the Running Track — the Office no longer carries a stub of its own, because no
|
||||
* office or industry card on the printed sheet does.
|
||||
*/
|
||||
/**
|
||||
* §2.1 — IS THIS SQUARE INSIDE THE DISTRICT AT ALL?
|
||||
*
|
||||
* The Limits sign denotes "the limit of your control area", and §3 defines Secondary Track as "all
|
||||
* tracks IN YOUR LIMITS that are not the Running Track" — so the bound belongs to the district, not
|
||||
* to the one row the sign happens to stand in. This used to guard the Running Track alone, on the
|
||||
* reasoning that the rows above and below were unbounded; the consequence was a siding snaking east
|
||||
* past your own sign, taking industries with it, and a player switching cars on track outside the
|
||||
* territory §8.1 and §10 reason about. Reported by Jesse: sidings must not be built outside the
|
||||
* Limits.
|
||||
*
|
||||
* INCLUSIVE OF THE SIGN'S OWN COLUMN. The sign stands ON the boundary rather than beyond it, and the
|
||||
* opening district is signs at ±1 around the Office — so the strict reading would leave exactly one
|
||||
* buildable column and break §11.3's promise that both Secondary rows, and the nine-spot Modifier
|
||||
* neighbourhood, are usable from the first Stage.
|
||||
*
|
||||
* MODIFIERS ARE NOT SUBJECT TO THIS, and are not track: §9 places one on any of the nine spots
|
||||
* around a Facility, and a Facility standing at the limit has three of its nine outside them.
|
||||
* Jesse's call. `check` bars them from the Running Track ROW instead, which is the ground the main
|
||||
* grows onto.
|
||||
*/
|
||||
export function withinLimits(area: OfficeArea, coord: GridCoord): boolean {
|
||||
return coord.col >= area.limitsWest.col && coord.col <= area.limitsEast.col;
|
||||
}
|
||||
|
||||
export function canPlaceAt(area: OfficeArea, coord: GridCoord, card: TrackCard): boolean {
|
||||
const existing = cardAt(area, coord);
|
||||
// A Limits sign on the Running Track is the GROWTH POINT, not an obstacle. Extending means laying
|
||||
@@ -575,14 +600,12 @@ export function canPlaceAt(area: OfficeArea, coord: GridCoord, card: TrackCard):
|
||||
// the sign itself is the only growth point. Allowing a placement beyond it built track on the far
|
||||
// side of the sign and then planted a SECOND sign further out, leaving the board reading
|
||||
// limits · Whistle Post · limits · straight · limits
|
||||
// with a Limits card stranded mid-track. The district below the Running Track is unbounded.
|
||||
if (coord.row === area.runningRow) {
|
||||
if (coord.col < area.limitsWest.col || coord.col > area.limitsEast.col) return false;
|
||||
// And it must not BREAK the Running Track. A curve laid here has no east-west road, so the main
|
||||
// stops dead at it — the bot did exactly this, turning the west end of its own Running Track
|
||||
// into a stub and cutting the Office off from the Limits.
|
||||
if (!carriesThroughTrack(card)) return false;
|
||||
}
|
||||
// with a Limits card stranded mid-track.
|
||||
if (!withinLimits(area, coord)) return false;
|
||||
// And it must not BREAK the Running Track. A curve laid here has no east-west road, so the main
|
||||
// stops dead at it — the bot did exactly this, turning the west end of its own Running Track
|
||||
// into a stub and cutting the Office off from the Limits.
|
||||
if (coord.row === area.runningRow && !carriesThroughTrack(card)) return false;
|
||||
|
||||
const ports: Port[] = ['n', 's', 'e', 'w'];
|
||||
for (const p of ports) {
|
||||
|
||||
Reference in New Issue
Block a user