make turnout description more clear. fix curves from breaking running track.
This commit is contained in:
@@ -50,6 +50,7 @@ import type { Occupancy, Port } from './track.ts';
|
||||
import {
|
||||
canDropCarsAt,
|
||||
canPlaceAt,
|
||||
carriesThroughTrack,
|
||||
facilityVariants,
|
||||
opposite,
|
||||
reachableDestinations,
|
||||
@@ -590,6 +591,11 @@ function checkPlay(
|
||||
{
|
||||
const proto = protoCard(card.kind, variant);
|
||||
if (!proto) return 'NO_PLACEMENT';
|
||||
// Said separately from NOT_CONNECTED because it is a different mistake: the card would join
|
||||
// perfectly well, and would still leave the Running Track stopping dead at it.
|
||||
if (placement.row === area.runningRow && !carriesThroughTrack(proto)) {
|
||||
return 'BREAKS_RUNNING_TRACK';
|
||||
}
|
||||
return canPlaceAt(area, placement, proto) ? null : 'NOT_CONNECTED';
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,8 @@ export type RejectionCode =
|
||||
| 'FACILITY_LOCKED'
|
||||
/** An Industry goes on a straight stub, never on the Running Track (sheet: "Placed" column). */
|
||||
| 'ON_RUNNING_TRACK'
|
||||
/** A card with no east-west road would dead-end the Running Track it was laid in. */
|
||||
| 'BREAKS_RUNNING_TRACK'
|
||||
| 'NOT_IMPLEMENTED'
|
||||
| 'WRONG_INTENT'
|
||||
| 'NOT_A_GRADE'
|
||||
|
||||
@@ -406,6 +406,20 @@ export function allReachable(
|
||||
// Placement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Does this card carry the through route — an east-west road straight across it?
|
||||
*
|
||||
* The Running Track is the road trains run on, Limits to Limits, and every card standing in it has
|
||||
* to pass traffic along the row. A curve does not: it has one road, from an east or west edge round
|
||||
* to a 45° leg, and dropping one into the running row DEAD-ENDS the main there. Straights, turnouts,
|
||||
* Limits signs, the Office and industries all carry it.
|
||||
*/
|
||||
export function carriesThroughTrack(card: TrackCard): boolean {
|
||||
return connectionsFor(card).some(
|
||||
([a, b]) => (a === 'e' && b === 'w') || (a === 'w' && b === 'e'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gap 4a — a placed card must connect to existing track, and "connect" means `joins`: ports meeting
|
||||
* AND, on a north or south edge, 45° legs on the same diagonal.
|
||||
@@ -433,6 +447,10 @@ export function canPlaceAt(area: OfficeArea, coord: GridCoord, card: TrackCard):
|
||||
// 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;
|
||||
}
|
||||
|
||||
const ports: Port[] = ['n', 's', 'e', 'w'];
|
||||
|
||||
+23
-18
@@ -27,7 +27,7 @@ import {
|
||||
trainProfile,
|
||||
} from '../engine/content.ts';
|
||||
import type { Intent } from '../engine/intents.ts';
|
||||
import type { Facility, GameState, TrackCard } from '../engine/state.ts';
|
||||
import type { Facility, GameState, TrackCard, TurnoutOrientation } from '../engine/state.ts';
|
||||
import type { Hand, TrackGeometry } from '../engine/content.ts';
|
||||
import type { Port } from '../engine/track.ts';
|
||||
import { connectionsFor, slopeOfPair, variantsFor } from '../engine/track.ts';
|
||||
@@ -859,16 +859,17 @@ function cellDescription(card: TrackCard, officeName: string, onRunning: boolean
|
||||
case 'sharpCurved': {
|
||||
const arc = g.arc ?? (g.hand === 'right' ? 'se' : 'sw');
|
||||
const cost = g.geometry === 'sharpCurved' ? ' · costs TWO Moves to cross' : '';
|
||||
return `${arcPhrase(arc)}${cost}${slopePhrase(arc[0] as Port, arc[1] as Port)}`;
|
||||
return `curve — ${curvePhrase(arc)}${cost}${slopePhrase(arc[0] as Port, arc[1] as Port)}`;
|
||||
}
|
||||
case 'turnout': {
|
||||
const t = g.turnout;
|
||||
if (!t) return 'turnout';
|
||||
// §A.1 is the subtlety worth spelling out: the missing edge, not a one-way street.
|
||||
return (
|
||||
`turnout — stem ${compass(t.stem)}, through ${compass(t.through)}, diverges ${compass(t.diverge)} at 45° · ` +
|
||||
`a train may run stem↔through or stem↔diverge, but NEVER between ${compass(t.through)} and ` +
|
||||
`${compass(t.diverge)}${slopePhrase(t.stem as Port, t.diverge as Port)}`
|
||||
`turnout — ${turnoutPhrase(t)} · a train coming the other way, from the ` +
|
||||
`${compass(t.through)} or the ${compass(t.diverge)}, may only leave by the ` +
|
||||
`${compass(t.stem)} — the two roads never join` +
|
||||
`${slopePhrase(t.stem as Port, t.diverge as Port)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -878,13 +879,23 @@ function cellDescription(card: TrackCard, officeName: string, onRunning: boolean
|
||||
const compass = (p: string): string => ({ n: 'north', s: 'south', e: 'east', w: 'west' })[p] ?? p;
|
||||
|
||||
/**
|
||||
* A curve, as the printed card draws it: along the centre line to a frog, then out at 45° through
|
||||
* the MIDDLE of an edge. Naming both halves is what stops it reading as a quarter-circle corner.
|
||||
* WHAT A TURNOUT DOES, in the words a player would use at the table.
|
||||
*
|
||||
* "Right-hand turnout, stem east, through west, diverges north" is three pieces of jargon and a
|
||||
* compass reading, and none of it answers the only question being asked: if my train comes in from
|
||||
* over there, where can it go? §A.1's rule falls straight out of the same sentence — traffic from
|
||||
* the stem may take either road, and traffic arriving on either road may only leave by the stem, so
|
||||
* the two roads never join.
|
||||
*/
|
||||
function arcPhrase(arc: string): string {
|
||||
function turnoutPhrase(t: TurnoutOrientation): string {
|
||||
return `allows traffic from the ${compass(t.stem)} to travel ${compass(t.through)} or turn to the ${compass(t.diverge)}`;
|
||||
}
|
||||
|
||||
/** The same, for a curve: it has one road and no choice to make. */
|
||||
function curvePhrase(arc: string): string {
|
||||
const [a, b] = [arc[0] as Port, arc[1] as Port];
|
||||
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
||||
return `curve — runs ${compass(side!)} along the centre line, then leaves at 45° through the middle of the ${compass(leg!)} edge`;
|
||||
return `carries traffic from the ${compass(side!)} round to the ${compass(leg!)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -944,15 +955,9 @@ export function variantLabel(
|
||||
): string {
|
||||
const v = variantsFor(geometry, hand)[variant ?? 0];
|
||||
if (!v) return '';
|
||||
if (v.turnout) {
|
||||
return ` — stem ${compass(v.turnout.stem)}, through ${compass(v.turnout.through)}, diverges ${compass(v.turnout.diverge)} at 45°`;
|
||||
}
|
||||
if (v.arc) {
|
||||
const [a, b] = [v.arc[0] as Port, v.arc[1] as Port];
|
||||
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
||||
return ` — ${compass(side!)} to the middle of the ${compass(leg!)} edge`;
|
||||
}
|
||||
return ' — east–west';
|
||||
if (v.turnout) return ` — ${turnoutPhrase(v.turnout)}`;
|
||||
if (v.arc) return ` — ${curvePhrase(v.arc)}`;
|
||||
return ' — straight through, east to west';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user