make turnout description more clear. fix curves from breaking running track.
This commit is contained in:
+7
-1
@@ -969,7 +969,13 @@ describe('the freight figures count both halves (regression)', () => {
|
||||
let loads = 0;
|
||||
let unloads = 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({
|
||||
id: `fu-${seed}`, seed,
|
||||
config: { ...config, length: 'standard' },
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
allReachable,
|
||||
canDropCarsAt,
|
||||
canPlaceAt,
|
||||
carriesThroughTrack,
|
||||
exitsFrom,
|
||||
hasPort,
|
||||
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: [],
|
||||
});
|
||||
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
|
||||
assert.match(cell.what, /stem east/);
|
||||
assert.match(cell.what, /diverges south/);
|
||||
assert.match(cell.what, /NEVER/, 'the missing edge is not spelled out');
|
||||
// Said the way a player would ask it: if my train comes in from over there, where can it go?
|
||||
// "stem east, through west, diverges south" is three pieces of jargon and a compass reading.
|
||||
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', () => {
|
||||
@@ -649,13 +654,20 @@ describe('the page explains itself', () => {
|
||||
variantsFor(geometry, hand).forEach((v, variant) => {
|
||||
const label = variantLabel(geometry, variant, hand);
|
||||
if (v.turnout) {
|
||||
assert.match(label, new RegExp(`stem ${say[v.turnout.stem]}`), `${geometry}/${hand} v${variant}: ${label}`);
|
||||
assert.match(label, new RegExp(`diverges ${say[v.turnout.diverge]}`), `${geometry}/${hand} v${variant}: ${label}`);
|
||||
assert.equal(
|
||||
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) {
|
||||
const [a, b] = [v.arc[0]!, v.arc[1]!];
|
||||
const [side, leg] = a === 'n' || a === 's' ? [b, a] : [a, b];
|
||||
assert.match(label, new RegExp(say[side]!), `${geometry}/${hand} v${variant}: ${label}`);
|
||||
assert.match(label, new RegExp(`${say[leg]} edge`), `${geometry}/${hand} v${variant}: ${label}`);
|
||||
assert.equal(
|
||||
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);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
const { option: turnout } = dealTrack(game, 'turnout', 'left');
|
||||
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 f = view(game);
|
||||
|
||||
Reference in New Issue
Block a user