fix tracks to all be EW. no more NS tracks
This commit is contained in:
+59
-15
@@ -8,11 +8,11 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import { pump } from '../src/engine/advance.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import { refillDivisionYardIfEmpty } from '../src/engine/apply.ts';
|
||||
import { applyIntent, check, refillDivisionYardIfEmpty } from '../src/engine/apply.ts';
|
||||
import { TOTAL_ROLLING_STOCK } from '../src/engine/content.ts';
|
||||
import type { GameConfig, GameState, OfficeArea } from '../src/engine/state.ts';
|
||||
import type { Intent } from '../src/engine/intents.ts';
|
||||
import { connectionsFor, exitsFrom, hasPort, neighbour, opposite, variantsFor } from '../src/engine/track.ts';
|
||||
import { connectionsFor, exitsFrom, hasPort, joins, neighbour, opposite, variantsFor } from '../src/engine/track.ts';
|
||||
import type { Port } from '../src/engine/track.ts';
|
||||
import { developerBot, playGame, randomBot } from '../src/sim/bot.ts';
|
||||
import { simulate } from '../src/sim/harness.ts';
|
||||
@@ -192,17 +192,57 @@ describe('the revenue chain works end to end (regression)', () => {
|
||||
assert.ok(freight > 0, 'no freight load completed across 40 games');
|
||||
});
|
||||
|
||||
it('grows the Office Area downward from the Running Track (Q7)', () => {
|
||||
// The card art puts the through-track at the top edge, so Secondary Track hangs beneath it.
|
||||
// Nothing should ever be built above row 0.
|
||||
it('grows the Office Area off the Running Track, on either side', () => {
|
||||
// The through track runs across the middle of every card and a turnout's 45° leg reaches north
|
||||
// as readily as south, so a district hangs off the Running Track rather than beneath it. Q7's
|
||||
// "downward only" is gone, and with it the rule that rejected every square above row 0.
|
||||
const rows = new Set<number>();
|
||||
for (const seed of [3, 9, 27, 81]) {
|
||||
const s = createGame({ id: 'g', seed, config: { ...config, length: 'campaign' }, playerNames: ['bot'] });
|
||||
playGame(s, developerBot, pump);
|
||||
for (const key of s.officeAreas.get(0)!.grid.keys()) rows.add(Number(key.split(',')[0]));
|
||||
}
|
||||
assert.ok([...rows].some((r) => r < 0), 'nothing was ever built below the Running Track');
|
||||
assert.ok(![...rows].some((r) => r > 0), 'something was built ABOVE the Running Track');
|
||||
assert.ok([...rows].some((r) => r !== 0), 'nothing was ever built off the Running Track');
|
||||
});
|
||||
|
||||
it('offers squares above the Running Track, not only below', () => {
|
||||
// Asserted against LEGALITY rather than against what the bot happened to build: whether its
|
||||
// heuristics favour one side is a tuning question, but the rules must allow both.
|
||||
const s = createGame({ id: 'g', seed: 5, config, playerNames: ['bot'] });
|
||||
const area = s.officeAreas.get(0)!;
|
||||
s.clock.phase = 'localOps';
|
||||
s.clock.currentActor = 0;
|
||||
s.turn.option = 'draw';
|
||||
// A north-diverging turnout on the Running Track, and the arc that climbs to meet it. The
|
||||
// column is captured BEFORE the lay: laying on the sign moves the sign outward, so reading
|
||||
// `limitsEast` again afterwards names the next square along, not the turnout.
|
||||
const col = area.limitsEast.col;
|
||||
assert.ok(
|
||||
applyIntent(s, 0, {
|
||||
type: 'track.lay', geometry: 'turnout', hand: 'left',
|
||||
placement: { row: area.runningRow, col }, variant: 1,
|
||||
}).ok,
|
||||
'a turnout must be layable on the Limits sign',
|
||||
);
|
||||
s.turn.laidThisTurn = false;
|
||||
// Left-hand variant 0 is the `sw` arc — the one on the same diagonal as that turnout's leg.
|
||||
assert.equal(
|
||||
check(s, 0, {
|
||||
type: 'track.lay', geometry: 'curved', hand: 'left',
|
||||
placement: { row: area.runningRow + 1, col }, variant: 0,
|
||||
}),
|
||||
null,
|
||||
'a curve ABOVE the Running Track must be legal',
|
||||
);
|
||||
// And the opposite diagonal must not be, at the same square, for the same turnout.
|
||||
assert.equal(
|
||||
check(s, 0, {
|
||||
type: 'track.lay', geometry: 'curved', hand: 'right',
|
||||
placement: { row: area.runningRow + 1, col }, variant: 0,
|
||||
}),
|
||||
'NOT_CONNECTED',
|
||||
'the wrong diagonal must not join, however well the ports line up',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,16 +354,18 @@ describe('switching accomplishes something (regression)', () => {
|
||||
describe('the bot builds sidings that are actually sidings (regression)', () => {
|
||||
/**
|
||||
* Walk south off one Running Track column and report which OTHER main-line columns the walk can
|
||||
* climb back up to. Uses the engine's own `exitsFrom`, so the test cannot credit a connection the
|
||||
* rules do not have — a turnout's two legs never join each other (§A.1), and a re-implementation
|
||||
* of the traversal would quietly invent that join.
|
||||
* climb back up to. Uses the engine's own `exitsFrom` and `joins`, so the test cannot credit a
|
||||
* connection the rules do not have — a turnout's two legs never join each other (§A.1), and two
|
||||
* 45° legs on opposite diagonals meet at a point on the card edge without being a rail. A
|
||||
* re-implementation of the traversal would quietly invent both.
|
||||
*/
|
||||
const rejoinsFrom = (area: OfficeArea, col: number): { rejoins: Set<number>; seen: Set<string> } => {
|
||||
const main = area.runningRow;
|
||||
const rejoins = new Set<number>();
|
||||
const seen = new Set<string>();
|
||||
const top = area.grid.get(`${main},${col}`);
|
||||
const start = area.grid.get(`${main - 1},${col}`);
|
||||
if (!start || !hasPort(start, 'n')) return { rejoins, seen };
|
||||
if (!top || !start || !joins(top, 's', start)) return { rejoins, seen };
|
||||
|
||||
const stack: { row: number; col: number; entry: Port }[] = [{ row: main - 1, col, entry: 'n' }];
|
||||
const visited = new Set<string>();
|
||||
@@ -338,7 +380,7 @@ describe('the bot builds sidings that are actually sidings (regression)', () =>
|
||||
for (const exit of exitsFrom(card, at.entry)) {
|
||||
const n = neighbour({ row: at.row, col: at.col }, exit);
|
||||
const next = area.grid.get(`${n.row},${n.col}`);
|
||||
if (!next || !hasPort(next, opposite(exit))) continue;
|
||||
if (!next || !joins(card, exit, next)) continue;
|
||||
if (n.row >= main) {
|
||||
if (n.col !== col) rejoins.add(n.col);
|
||||
continue;
|
||||
@@ -745,20 +787,22 @@ describe('the bot does not lay track that cannot work (regression)', () => {
|
||||
if (pick.type === 'track.lay') {
|
||||
laid++;
|
||||
const area = st.officeAreas.get(p)!;
|
||||
const v = variantsFor(pick.geometry)[pick.variant ?? 0];
|
||||
const v = variantsFor(pick.geometry, pick.hand)[pick.variant ?? 0];
|
||||
const probe = {
|
||||
geometry: {
|
||||
kind: 'track', geometry: pick.geometry,
|
||||
...(v?.axis ? { axis: v.axis } : {}),
|
||||
...(v?.arc ? { arc: v.arc } : {}),
|
||||
...(v?.turnout ? { turnout: v.turnout } : {}),
|
||||
...(pick.hand !== 'none' ? { hand: pick.hand } : {}),
|
||||
},
|
||||
baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [],
|
||||
} as never;
|
||||
for (const port of new Set(connectionsFor(probe).flat())) {
|
||||
const n = neighbour(pick.placement, port);
|
||||
const nb = area.grid.get(`${n.row},${n.col}`);
|
||||
if (nb && !hasPort(nb, opposite(port))) dead++;
|
||||
// `joins`, not `hasPort`: on a north or south edge the two 45° legs must also lie on
|
||||
// the same diagonal, or the rails meet at a point and still form a V.
|
||||
if (nb && !joins(probe, port, nb)) dead++;
|
||||
}
|
||||
}
|
||||
return pick;
|
||||
|
||||
Reference in New Issue
Block a user