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:
Jesse
2026-08-19 13:38:06 -04:00
co-authored by Claude Opus 5
parent 9f3b92d08e
commit 461c4d9bac
25 changed files with 3264 additions and 4524 deletions
+31 -8
View File
@@ -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) {