make turnout description more clear. fix curves from breaking running track.
This commit is contained in:
@@ -10,6 +10,47 @@ The target is 20 Revenue over 5 Days.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Turnouts say what they do, and a curve may not break the Running Track
|
||||||
|
|
||||||
|
**"Right-hand turnout, stem east, through west, diverges north at 45°"** 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?* Turnouts now read
|
||||||
|
|
||||||
|
> allows traffic from the east to travel west or turn to the south
|
||||||
|
|
||||||
|
and curves
|
||||||
|
|
||||||
|
> carries traffic from the west round to the south
|
||||||
|
|
||||||
|
everywhere they appear — on the card in hand, on the placement it would make, and on the card once it
|
||||||
|
is down. §A.1's rule falls out of the same sentence rather than needing a shouty clause of its own:
|
||||||
|
a train coming the other way, from the through end or the diverging one, may only leave by the stem,
|
||||||
|
so the two roads never join.
|
||||||
|
|
||||||
|
**A curve laid in the Running Track dead-ends the main.** A curve has ONE road — from an east or west
|
||||||
|
edge round to its 45° leg — so a card of it standing in the running row stops the through route at
|
||||||
|
that square and cuts the Office off from its own Limits. Nothing forbade it, and the bot did it: an
|
||||||
|
early trace shows it laying a `ne` curve at (0,−1), turning the west end of its own Running Track
|
||||||
|
into a stub. Every card that may stand in that row now has to carry the road across it —
|
||||||
|
`carriesThroughTrack` — which straights, turnouts, Limits signs, the Office and industries all do.
|
||||||
|
Reported as `BREAKS_RUNNING_TRACK` rather than `NOT_CONNECTED`, because it is a different mistake:
|
||||||
|
the card would join perfectly well and would still leave the main stopping dead at it.
|
||||||
|
|
||||||
|
**The crossover is confirmed, both hands.** A turnout that *"allows traffic from the east to travel
|
||||||
|
west or turn to the south"* joins the one directly beneath it that *"allows traffic from the west to
|
||||||
|
travel east or turn to the north"* — the two 45° legs are one continuous rail across the card edge.
|
||||||
|
It already worked; it now has a test that says so in those words, along with its mirror built from
|
||||||
|
the other hand, the mismatched pair that must NOT join, and a crew actually running down through it
|
||||||
|
onto the parallel track.
|
||||||
|
|
||||||
|
**Measured, and worth flagging:** barring curves from the running row cost the bot a good deal —
|
||||||
|
districts 28.0 → 19.7 cards, facilities 2.23 → 1.85, revenue about 2.0 → 0.8. The rule is right and
|
||||||
|
the bot was partly living off an illegal placement. Two tests needed widening rather than weakening
|
||||||
|
as a result: the unload regression sampled three deals and now samples twelve (unloads still happen,
|
||||||
|
34 across a 40-deal sweep, just not on those three), and the canvas bounds test now holds a straight
|
||||||
|
as well as a curve, since a curve alone is offered only one square once the running row is closed to
|
||||||
|
it.
|
||||||
|
|
||||||
### The Crew Tray is a train, and a train must be made up to leave
|
### The Crew Tray is a train, and a train must be made up to leave
|
||||||
|
|
||||||
**§8.2 was not checked at all.** A train could highball onto the Mainline engine-last with its
|
**§8.2 was not checked at all.** A train could highball onto the Mainline engine-last with its
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ Ordered within each section by how much it is currently costing us.
|
|||||||
holds 243 of which 104 are track, so every draw is diluted by 43% — precisely the pressure
|
holds 243 of which 104 are track, so every draw is diluted by 43% — precisely the pressure
|
||||||
those multipliers exist to relieve. Until then, read no balance conclusion from the revenue
|
those multipliers exist to relieve. Until then, read no balance conclusion from the revenue
|
||||||
numbers; they are a functionality signal only.
|
numbers; they are a functionality signal only.
|
||||||
|
- [ ] **The bot was partly living off an illegal placement.** Barring curves from the Running Track
|
||||||
|
(they have no east-west road and dead-end the main) cost it districts 28.0 → 19.7 cards and
|
||||||
|
revenue ~2.0 → 0.8. It has no plan for where a curve should go once the easy square is gone.
|
||||||
|
Same root cause as the two items below; fix them together, after the rebalance.
|
||||||
- [ ] **The bot cannot get a crew next to an industry, so Flying Switch never fires.** Industries are
|
- [ ] **The bot cannot get a crew next to an industry, so Flying Switch never fires.** Industries are
|
||||||
now stub-only and the bot places 2.23 a game (was 3.84), in districts averaging under two rows
|
now stub-only and the bot places 2.23 a game (was 3.84), in districts averaging under two rows
|
||||||
deep. `flyingSwitch` is exempted by name in the reachability sweep in `sim.test.ts`; deleting
|
deep. `flyingSwitch` is exempted by name in the reachability sweep in `sim.test.ts`; deleting
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import type { Occupancy, Port } from './track.ts';
|
|||||||
import {
|
import {
|
||||||
canDropCarsAt,
|
canDropCarsAt,
|
||||||
canPlaceAt,
|
canPlaceAt,
|
||||||
|
carriesThroughTrack,
|
||||||
facilityVariants,
|
facilityVariants,
|
||||||
opposite,
|
opposite,
|
||||||
reachableDestinations,
|
reachableDestinations,
|
||||||
@@ -590,6 +591,11 @@ function checkPlay(
|
|||||||
{
|
{
|
||||||
const proto = protoCard(card.kind, variant);
|
const proto = protoCard(card.kind, variant);
|
||||||
if (!proto) return 'NO_PLACEMENT';
|
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';
|
return canPlaceAt(area, placement, proto) ? null : 'NOT_CONNECTED';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export type RejectionCode =
|
|||||||
| 'FACILITY_LOCKED'
|
| 'FACILITY_LOCKED'
|
||||||
/** An Industry goes on a straight stub, never on the Running Track (sheet: "Placed" column). */
|
/** An Industry goes on a straight stub, never on the Running Track (sheet: "Placed" column). */
|
||||||
| 'ON_RUNNING_TRACK'
|
| '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'
|
| 'NOT_IMPLEMENTED'
|
||||||
| 'WRONG_INTENT'
|
| 'WRONG_INTENT'
|
||||||
| 'NOT_A_GRADE'
|
| 'NOT_A_GRADE'
|
||||||
|
|||||||
@@ -406,6 +406,20 @@ export function allReachable(
|
|||||||
// Placement
|
// 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
|
* 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.
|
* 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.
|
// with a Limits card stranded mid-track. The district below the Running Track is unbounded.
|
||||||
if (coord.row === area.runningRow) {
|
if (coord.row === area.runningRow) {
|
||||||
if (coord.col < area.limitsWest.col || coord.col > area.limitsEast.col) return false;
|
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'];
|
const ports: Port[] = ['n', 's', 'e', 'w'];
|
||||||
|
|||||||
+23
-18
@@ -27,7 +27,7 @@ import {
|
|||||||
trainProfile,
|
trainProfile,
|
||||||
} from '../engine/content.ts';
|
} from '../engine/content.ts';
|
||||||
import type { Intent } from '../engine/intents.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 { Hand, TrackGeometry } from '../engine/content.ts';
|
||||||
import type { Port } from '../engine/track.ts';
|
import type { Port } from '../engine/track.ts';
|
||||||
import { connectionsFor, slopeOfPair, variantsFor } 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': {
|
case 'sharpCurved': {
|
||||||
const arc = g.arc ?? (g.hand === 'right' ? 'se' : 'sw');
|
const arc = g.arc ?? (g.hand === 'right' ? 'se' : 'sw');
|
||||||
const cost = g.geometry === 'sharpCurved' ? ' · costs TWO Moves to cross' : '';
|
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': {
|
case 'turnout': {
|
||||||
const t = g.turnout;
|
const t = g.turnout;
|
||||||
if (!t) return 'turnout';
|
if (!t) return 'turnout';
|
||||||
// §A.1 is the subtlety worth spelling out: the missing edge, not a one-way street.
|
// §A.1 is the subtlety worth spelling out: the missing edge, not a one-way street.
|
||||||
return (
|
return (
|
||||||
`turnout — stem ${compass(t.stem)}, through ${compass(t.through)}, diverges ${compass(t.diverge)} at 45° · ` +
|
`turnout — ${turnoutPhrase(t)} · a train coming the other way, from the ` +
|
||||||
`a train may run stem↔through or stem↔diverge, but NEVER between ${compass(t.through)} and ` +
|
`${compass(t.through)} or the ${compass(t.diverge)}, may only leave by the ` +
|
||||||
`${compass(t.diverge)}${slopePhrase(t.stem as Port, t.diverge as Port)}`
|
`${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;
|
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
|
* WHAT A TURNOUT DOES, in the words a player would use at the table.
|
||||||
* the MIDDLE of an edge. Naming both halves is what stops it reading as a quarter-circle corner.
|
*
|
||||||
|
* "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 [a, b] = [arc[0] as Port, arc[1] as Port];
|
||||||
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
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 {
|
): string {
|
||||||
const v = variantsFor(geometry, hand)[variant ?? 0];
|
const v = variantsFor(geometry, hand)[variant ?? 0];
|
||||||
if (!v) return '';
|
if (!v) return '';
|
||||||
if (v.turnout) {
|
if (v.turnout) return ` — ${turnoutPhrase(v.turnout)}`;
|
||||||
return ` — stem ${compass(v.turnout.stem)}, through ${compass(v.turnout.through)}, diverges ${compass(v.turnout.diverge)} at 45°`;
|
if (v.arc) return ` — ${curvePhrase(v.arc)}`;
|
||||||
}
|
return ' — straight through, east to west';
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+7
-1
@@ -969,7 +969,13 @@ describe('the freight figures count both halves (regression)', () => {
|
|||||||
let loads = 0;
|
let loads = 0;
|
||||||
let unloads = 0;
|
let unloads = 0;
|
||||||
let begun = 0;
|
let begun = 0;
|
||||||
for (const seed of [1000, 8919, 16838]) {
|
// A WIDER SAMPLE than the three seeds this used to take. The subject is the instrument — does
|
||||||
|
// `freightUnload` count Revenue EARNED rather than unloads STARTED — and `unloads > 0` is only
|
||||||
|
// the precondition that makes the comparison mean anything. Three deals is a thin thread to hang
|
||||||
|
// that on: an unload needs an inbound industry built, reachable, and a loaded car spotted at it,
|
||||||
|
// and whether the bot manages all three on a given deal is luck, not the thing under test.
|
||||||
|
// Measured across these twelve: 34 unloads earned, on 12 of 40 deals sampled more widely.
|
||||||
|
for (const seed of [1000, 8919, 16838, 24757, 32676, 40595, 48514, 56433, 64352, 72271, 80190, 88109]) {
|
||||||
const s = createGame({
|
const s = createGame({
|
||||||
id: `fu-${seed}`, seed,
|
id: `fu-${seed}`, seed,
|
||||||
config: { ...config, length: 'standard' },
|
config: { ...config, length: 'standard' },
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
allReachable,
|
allReachable,
|
||||||
canDropCarsAt,
|
canDropCarsAt,
|
||||||
canPlaceAt,
|
canPlaceAt,
|
||||||
|
carriesThroughTrack,
|
||||||
exitsFrom,
|
exitsFrom,
|
||||||
hasPort,
|
hasPort,
|
||||||
neighbour,
|
neighbour,
|
||||||
@@ -803,3 +804,100 @@ describe('Operational Rail — where a train may stop and leave cars (Appendix A
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('the Running Track must stay a through route', () => {
|
||||||
|
const runningArea = (): OfficeArea =>
|
||||||
|
areaFrom(
|
||||||
|
{
|
||||||
|
[coordKey(at(0, -1))]: straight(),
|
||||||
|
[coordKey(at(0, 0))]: officeCard(),
|
||||||
|
[coordKey(at(0, 1))]: straight(),
|
||||||
|
},
|
||||||
|
at(0, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
it('refuses a curve in the running row, which would dead-end the main', () => {
|
||||||
|
// A curve has ONE road — from an east or west edge round to a 45° leg — so a card of it standing
|
||||||
|
// in the Running Track stops the main dead at that square, cutting the Office off from its own
|
||||||
|
// Limits. Every other card that may stand there carries an east-west road across it.
|
||||||
|
const area = runningArea();
|
||||||
|
for (const arc of ['ne', 'nw', 'se', 'sw'] as TrackArc[]) {
|
||||||
|
assert.ok(!carriesThroughTrack(curve(arc)), `a ${arc} curve has no through road`);
|
||||||
|
assert.ok(!canPlaceAt(area, at(0, 2), curve(arc)), `a ${arc} curve must not go in the Running Track`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still accepts everything that does carry the road through', () => {
|
||||||
|
const area = runningArea();
|
||||||
|
assert.ok(carriesThroughTrack(straight()));
|
||||||
|
assert.ok(carriesThroughTrack(turnout({ stem: 'w', through: 'e', diverge: 's' })));
|
||||||
|
assert.ok(carriesThroughTrack(officeCard()));
|
||||||
|
assert.ok(canPlaceAt(area, at(0, 2), straight()), 'a straight extends the main');
|
||||||
|
assert.ok(
|
||||||
|
canPlaceAt(area, at(0, 2), turnout({ stem: 'w', through: 'e', diverge: 's' })),
|
||||||
|
'a turnout keeps the road AND opens a leg — which is why a district hangs off one',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves a curve free to go anywhere off the main', () => {
|
||||||
|
const area = areaFrom(
|
||||||
|
{
|
||||||
|
[coordKey(at(0, 0))]: officeCard(),
|
||||||
|
[coordKey(at(0, 1))]: turnout({ stem: 'w', through: 'e', diverge: 's' }),
|
||||||
|
},
|
||||||
|
at(0, 0),
|
||||||
|
);
|
||||||
|
assert.ok(canPlaceAt(area, at(-1, 1), curve('ne')), 'the siding is exactly where a curve belongs');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('a turnout may be laid to face another, making a crossover', () => {
|
||||||
|
/** A turnout stated the way the cards read: who may come in, and where they may go. */
|
||||||
|
const facing = (stem: Port, through: Port, diverge: Port, hand: 'left' | 'right'): TrackCard => ({
|
||||||
|
geometry: { kind: 'track', geometry: 'turnout', turnout: { stem, through, diverge }, hand },
|
||||||
|
baseOperationalRail: false, standing: [], facility: null, modifiers: [], enhancements: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
it('joins a south-diverging turnout to the north-diverging one beneath it', () => {
|
||||||
|
// "Traffic from the east to travel west or turn to the south", with directly beneath it "traffic
|
||||||
|
// from the west to travel east or turn to the north". The two 45° legs are one continuous rail
|
||||||
|
// across the card edge — a crossover, and the way a siding gets a track parallel to the main.
|
||||||
|
const above = facing('e', 'w', 's', 'right');
|
||||||
|
const below = facing('w', 'e', 'n', 'right');
|
||||||
|
assert.ok(joins(above, 's', below), 'the crossover does not join');
|
||||||
|
|
||||||
|
// And its mirror, which is the same move built from the other hand.
|
||||||
|
const aboveL = facing('w', 'e', 's', 'left');
|
||||||
|
const belowL = facing('e', 'w', 'n', 'left');
|
||||||
|
assert.ok(joins(aboveL, 's', belowL), 'the mirrored crossover does not join');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses the pair whose legs lie on opposite diagonals', () => {
|
||||||
|
// Both cards have the port and both legs meet the same point on the shared edge. They still form
|
||||||
|
// a V: one descends to the right and the other to the left.
|
||||||
|
assert.ok(!joins(facing('e', 'w', 's', 'right'), 's', facing('e', 'w', 'n', 'left')));
|
||||||
|
assert.ok(!joins(facing('w', 'e', 's', 'left'), 's', facing('w', 'e', 'n', 'right')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets a crew run down the crossover onto the parallel track', () => {
|
||||||
|
// The point of building one: the lower turnout carries its own east-west road, so the siding
|
||||||
|
// runs alongside the main rather than dead-ending under it.
|
||||||
|
const area = areaFrom(
|
||||||
|
{
|
||||||
|
[coordKey(at(0, -1))]: straight(),
|
||||||
|
[coordKey(at(0, 0))]: facing('w', 'e', 's', 'left'),
|
||||||
|
[coordKey(at(0, 1))]: straight(),
|
||||||
|
// Coming down the leg, the lower turnout is entered through its diverging north end, so the
|
||||||
|
// only way on is its stem — east, along the parallel track.
|
||||||
|
[coordKey(at(-1, 0))]: facing('e', 'w', 'n', 'left'),
|
||||||
|
[coordKey(at(-1, 1))]: straight(),
|
||||||
|
},
|
||||||
|
at(0, 5),
|
||||||
|
);
|
||||||
|
const dests = reachableDestinations(ctxFor(area), at(0, -1), 'e');
|
||||||
|
assert.ok(has(dests, -1, 1), 'a crew cannot reach the parallel track through the crossover');
|
||||||
|
assert.ok(!has(dests, -1, 0), 'and may not stop on the turnout itself');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+27
-9
@@ -557,9 +557,14 @@ describe('the page explains itself', () => {
|
|||||||
baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [],
|
baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [],
|
||||||
});
|
});
|
||||||
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
|
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
|
||||||
assert.match(cell.what, /stem east/);
|
// Said the way a player would ask it: if my train comes in from over there, where can it go?
|
||||||
assert.match(cell.what, /diverges south/);
|
// "stem east, through west, diverges south" is three pieces of jargon and a compass reading.
|
||||||
assert.match(cell.what, /NEVER/, 'the missing edge is not spelled out');
|
assert.match(
|
||||||
|
cell.what,
|
||||||
|
/allows traffic from the east to travel west or turn to the south/,
|
||||||
|
`the turnout does not say what it does: ${cell.what}`,
|
||||||
|
);
|
||||||
|
assert.match(cell.what, /the two roads never join/, 'the missing edge is not spelled out');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('describes every card kind in the deck, not just the easy ones', () => {
|
it('describes every card kind in the deck, not just the easy ones', () => {
|
||||||
@@ -649,13 +654,20 @@ describe('the page explains itself', () => {
|
|||||||
variantsFor(geometry, hand).forEach((v, variant) => {
|
variantsFor(geometry, hand).forEach((v, variant) => {
|
||||||
const label = variantLabel(geometry, variant, hand);
|
const label = variantLabel(geometry, variant, hand);
|
||||||
if (v.turnout) {
|
if (v.turnout) {
|
||||||
assert.match(label, new RegExp(`stem ${say[v.turnout.stem]}`), `${geometry}/${hand} v${variant}: ${label}`);
|
assert.equal(
|
||||||
assert.match(label, new RegExp(`diverges ${say[v.turnout.diverge]}`), `${geometry}/${hand} v${variant}: ${label}`);
|
label.trim(),
|
||||||
|
`— allows traffic from the ${say[v.turnout.stem]} to travel ${say[v.turnout.through]} ` +
|
||||||
|
`or turn to the ${say[v.turnout.diverge]}`,
|
||||||
|
`${geometry}/${hand} v${variant}`,
|
||||||
|
);
|
||||||
} else if (v.arc) {
|
} else if (v.arc) {
|
||||||
const [a, b] = [v.arc[0]!, v.arc[1]!];
|
const [a, b] = [v.arc[0]!, v.arc[1]!];
|
||||||
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
||||||
assert.match(label, new RegExp(say[side]!), `${geometry}/${hand} v${variant}: ${label}`);
|
assert.equal(
|
||||||
assert.match(label, new RegExp(`${say[leg]} edge`), `${geometry}/${hand} v${variant}: ${label}`);
|
label.trim(),
|
||||||
|
`— carries traffic from the ${say[side!]} round to the ${say[leg!]}`,
|
||||||
|
`${geometry}/${hand} v${variant}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -682,7 +694,7 @@ describe('the page explains itself', () => {
|
|||||||
.filter((sp) => sp.coord.row === under.row && sp.coord.col === under.col);
|
.filter((sp) => sp.coord.row === under.row && sp.coord.col === under.col);
|
||||||
|
|
||||||
assert.equal(spots.length, 1, 'exactly one turnout rotation should meet the leg coming down');
|
assert.equal(spots.length, 1, 'exactly one turnout rotation should meet the leg coming down');
|
||||||
assert.match(spots[0]!.label, /diverges north/, 'the crossover turnout must point back up');
|
assert.match(spots[0]!.label, /turn to the north/, 'the crossover turnout must point back up');
|
||||||
assert.match(spots[0]!.label, /joins the track above/, 'the spot must say what it connects to');
|
assert.match(spots[0]!.label, /joins the track above/, 'the spot must say what it connects to');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1278,7 +1290,13 @@ describe('every square the menu offers can actually be clicked (regression)', ()
|
|||||||
// is offered a square OFF the Running Track, which is the case that used to draw off-canvas.
|
// is offered a square OFF the Running Track, which is the case that used to draw off-canvas.
|
||||||
const { option: turnout } = dealTrack(game, 'turnout', 'left');
|
const { option: turnout } = dealTrack(game, 'turnout', 'left');
|
||||||
if (turnout) submit(game, turnout);
|
if (turnout) submit(game, turnout);
|
||||||
dealTrack(game, 'curved', 'left');
|
// Hold BOTH the curve its leg calls for and a straight: the curve is offered the square off
|
||||||
|
// the Running Track, the straight the growth points on it. A curve may no longer be laid in
|
||||||
|
// the running row at all — it has no east-west road and would dead-end the main — so on its
|
||||||
|
// own it now offers a single square and the bounds check would barely exercise anything.
|
||||||
|
const curve = dealTrack(game, 'curved', 'left');
|
||||||
|
const straight = dealTrack(game, 'straight', 'none');
|
||||||
|
game.state.decks.hands.set(0, [curve.cardId, straight.cardId]);
|
||||||
|
|
||||||
const menu = actionMenu(game);
|
const menu = actionMenu(game);
|
||||||
const f = view(game);
|
const f = view(game);
|
||||||
|
|||||||
Reference in New Issue
Block a user