v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist
This commit is contained in:
+175
-8
@@ -6,13 +6,13 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { applyIntent, check, areaOf, facilityCarTypes, reduce } from '../src/engine/apply.ts';
|
||||
import { HAND_LIMIT, INDUSTRY_PROFILES, MOVES_PER_LOCAL_OPS, OPENING_OTHER, OPENING_TRACK } from '../src/engine/content.ts';
|
||||
import { applyIntent, check, areaOf, facilityCarTypes, movesFor, reduce } from '../src/engine/apply.ts';
|
||||
import { HAND_LIMIT, INDUSTRY_PROFILES, MOVES_PER_LOCAL_OPS } from '../src/engine/content.ts';
|
||||
import type { Intent } from '../src/engine/intents.ts';
|
||||
import { legalActions } from '../src/engine/legal.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import type { CrewTray, GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts';
|
||||
import { coordKey, turnOf } from '../src/engine/state.ts';
|
||||
import { coordKey, railFacingOf, turnOf } from '../src/engine/state.ts';
|
||||
import { cardDescription, snapshot } from '../src/sim/view.ts';
|
||||
|
||||
const config: GameConfig = {
|
||||
@@ -115,13 +115,15 @@ describe('Local Operations: drawing (§6.2)', () => {
|
||||
it('draws from the Home Office deck into the hand', () => {
|
||||
const s = game();
|
||||
const before = s.decks.homeOffice.length;
|
||||
const dealt = s.decks.hands.get(0)!.length;
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
const r = applyIntent(s, 0, { type: 'draw.fromHomeOffice' });
|
||||
assert.ok(r.ok);
|
||||
assert.equal(s.decks.homeOffice.length, before - 1);
|
||||
// The opening deal is 3 track + 3 other, so a draw takes the hand to seven — and §6.2's limit
|
||||
// then has to be played down to three before the turn can end.
|
||||
assert.equal(s.decks.hands.get(0)!.length, OPENING_TRACK + OPENING_OTHER + 1);
|
||||
// Measured against the deal rather than against a constant: the opening hand is a house rule
|
||||
// now (three random, six random, or three-and-three), and none of the three changes what a draw
|
||||
// does — it adds one card, and §6.2's limit then has to be played down to three.
|
||||
assert.equal(s.decks.hands.get(0)!.length, dealt + 1);
|
||||
});
|
||||
|
||||
it('takes the TOP card from a Department pile, never one buried under it', () => {
|
||||
@@ -239,13 +241,15 @@ describe('Local Operations: drawing (§6.2)', () => {
|
||||
it('will not end the phase over the hand limit', () => {
|
||||
// §6.2 — "must reduce his hand to no more than three cards".
|
||||
const s = game();
|
||||
const dealt = s.decks.hands.get(0)!.length;
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
applyIntent(s, 0, { type: 'draw.fromHomeOffice' });
|
||||
assert.equal(s.decks.hands.get(0)!.length, OPENING_TRACK + OPENING_OTHER + 1);
|
||||
assert.equal(s.decks.hands.get(0)!.length, dealt + 1);
|
||||
assert.ok(dealt + 1 > HAND_LIMIT, 'a draw must put the player over the limit for this to test anything');
|
||||
assert.equal(check(s, 0, { type: 'draw.end' }), 'HAND_LIMIT');
|
||||
|
||||
// And the way down is play or discard — there is no per-turn cap on either, which is what makes
|
||||
// the six-card opening hand playable in one turn rather than a limit that cannot be met.
|
||||
// a six-card opening hand playable in one turn rather than a limit that cannot be met.
|
||||
const hand = () => s.decks.hands.get(0)!;
|
||||
while (hand().length > HAND_LIMIT) {
|
||||
const r = applyIntent(s, 0, { type: 'card.discard', cardId: hand()[0]!, toSlot: 0 });
|
||||
@@ -1252,3 +1256,166 @@ describe('an Office upgrade keeps what Modifiers added (§9)', () => {
|
||||
assert.ok(office().capacity.outbound > base.out, 'and the upgrade still raised the tier');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('the engine is drawn pointing east or west, whatever track it is standing on', () => {
|
||||
/**
|
||||
* Reported from play: a crew that turned onto a north-south spur drew ▲, and the same crew kept
|
||||
* that ▲ after it departed onto the Division — where there is no north or south at all — because
|
||||
* nothing resets `facing` when a train leaves a district. A Division runs east and west, and a
|
||||
* player reads a train the way a railroader does: which END the engine is on.
|
||||
*
|
||||
* So `facing` stays a PORT (movement needs one) and `railFacingOf` is what the board draws.
|
||||
*/
|
||||
const moved = (id: string, facing: 'n' | 's' | 'e' | 'w') =>
|
||||
({ type: 'trayMoved', trayId: id, from: at(0, 0), to: at(0, 0), movesRemaining: 3, facing }) as const;
|
||||
|
||||
it('carries the east-west sense across north-south track', () => {
|
||||
const s = game();
|
||||
const id = placeTray(s, at(0, 0));
|
||||
const tray = s.trays.get(id)!;
|
||||
|
||||
reduce(s, moved(id, 'w'));
|
||||
assert.equal(railFacingOf(tray), 'w');
|
||||
|
||||
// Onto a curve, and away up a spur. The port is north; the engine is still on the west end.
|
||||
reduce(s, moved(id, 'n'));
|
||||
assert.equal(tray.facing, 'n', 'the movement port must still be the real one');
|
||||
assert.equal(railFacingOf(tray), 'w', 'but the drawing must not turn the train on end');
|
||||
});
|
||||
|
||||
it('follows the engine around 180° of curves, because that really is a turn', () => {
|
||||
// Forward through two curves — west port to north, north to east — and the engine genuinely
|
||||
// does come out pointing the other way. Held values are held, not frozen.
|
||||
const s = game();
|
||||
const id = placeTray(s, at(0, 0));
|
||||
const tray = s.trays.get(id)!;
|
||||
for (const f of ['w', 'n', 'e'] as const) reduce(s, moved(id, f));
|
||||
assert.equal(railFacingOf(tray), 'e');
|
||||
});
|
||||
|
||||
it('falls back to the direction of the run for a tray that has not moved yet', () => {
|
||||
// A tray placed straight onto the board has no history to carry, and `direction` is the only
|
||||
// east-west fact about it.
|
||||
const s = game();
|
||||
const tray = s.trays.get(placeTray(s, at(0, 0)))!;
|
||||
assert.equal(tray.railFacing, undefined);
|
||||
assert.equal(railFacingOf(tray), 'e', 'placed running east');
|
||||
tray.direction = 'west';
|
||||
assert.equal(railFacingOf(tray), 'w');
|
||||
});
|
||||
|
||||
it('never reports north or south to the board', () => {
|
||||
// The view type says 'e' | 'w'; this is the runtime half of that promise, and it is the one a
|
||||
// north-south spur used to break.
|
||||
const s = game();
|
||||
const id = placeTray(s, at(0, 0), [{ type: 'boxcar', loaded: false }] as never);
|
||||
addCard(s, at(0, 0), straight());
|
||||
reduce(s, moved(id, 's'));
|
||||
const cell = snapshot(s, [], null).cells.find((c) => c.row === 0 && c.col === 0);
|
||||
assert.ok(cell?.train, 'the train should be on the card');
|
||||
assert.ok(cell.train.facing === 'e' || cell.train.facing === 'w', `got ${cell.train.facing}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('a train that rounds a curve points where the curve took it', () => {
|
||||
/**
|
||||
* REPORTED from play: "a train reversed into a siding and the display of the cars was reversed."
|
||||
*
|
||||
* `facing` is the port the engine would leave by, and a move recorded it as `opposite(entry)` —
|
||||
* which is only the far end of a STRAIGHT. A curve is an arc between two ADJACENT edges: enter a
|
||||
* north-west curve through its west port and the far end is north, not east. So a train that
|
||||
* rounded a curve was left facing a port the card does not have.
|
||||
*
|
||||
* Two things went wrong with that. Movement: the next Move explores from `facing`, and a port the
|
||||
* card lacks yields nothing, so the crew could only ever back out the way it came. Display: the
|
||||
* east-west sense is carried from `facing` (`railFacingOf`), so a curve that turned the engine
|
||||
* west could leave the board still drawing it east — the consist mirrored, which is what was seen.
|
||||
*/
|
||||
const curve = (arc: 'ne' | 'nw' | 'se' | 'sw'): TrackCard => ({
|
||||
geometry: { kind: 'track', geometry: 'curved', arc, hand: 'right' },
|
||||
baseOperationalRail: true,
|
||||
standing: [],
|
||||
facility: null,
|
||||
modifiers: [],
|
||||
enhancements: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* Runs a crew one Move and reports where the engine ended up pointing.
|
||||
*
|
||||
* The card it starts ON matters as much as the one it moves to: a crew facing north or south has
|
||||
* to be standing on something with that port, and only a curve has one.
|
||||
*/
|
||||
const roundIt = (
|
||||
from: GridCoord,
|
||||
to: GridCoord,
|
||||
origin: TrackCard,
|
||||
dest: TrackCard,
|
||||
start: { facing: 'n' | 's' | 'e' | 'w'; railFacing: 'e' | 'w' },
|
||||
reverse = false,
|
||||
): { facing?: string; drawn: string } => {
|
||||
const s = game();
|
||||
addCard(s, from, origin);
|
||||
addCard(s, to, dest);
|
||||
const id = placeTray(s, from, [{ type: 'boxcar', loaded: false }] as never);
|
||||
const tray = s.trays.get(id)!;
|
||||
tray.facing = start.facing;
|
||||
tray.railFacing = start.railFacing;
|
||||
s.clock.phase = 'localOps';
|
||||
s.clock.currentActor = 0;
|
||||
turnOf(s, 0).option = 'switch';
|
||||
|
||||
const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to, reverse });
|
||||
assert.ok(r.ok, `the move was refused: ${r.ok ? '' : r.code}`);
|
||||
return { facing: tray.facing, drawn: railFacingOf(tray) };
|
||||
};
|
||||
|
||||
it('leaves by the curve’s own far end, not by the opposite of the way it came in', () => {
|
||||
// East along the running track into a curve that turns north. The far end is NORTH.
|
||||
const out = roundIt({ row: 0, col: 0 }, { row: 0, col: 1 }, straight(), curve('nw'), { facing: 'e', railFacing: 'e' });
|
||||
assert.equal(out.facing, 'n', 'the engine was left facing a port the curve does not have');
|
||||
});
|
||||
|
||||
it('does not strand the crew on the curve it just rounded', () => {
|
||||
// The symptom the wrong port produces: `movesFor` explores from `facing`, and a card without
|
||||
// that port yields nothing at all — so the only move left was backing out the way it came.
|
||||
const s = game();
|
||||
addCard(s, at(0, 0), straight());
|
||||
addCard(s, at(0, 1), curve('nw')); // joins north and west
|
||||
addCard(s, at(1, 1), curve('se')); // joins south and east — the track continues
|
||||
addCard(s, at(1, 2), straight());
|
||||
const id = placeTray(s, at(0, 0), []);
|
||||
s.trays.get(id)!.facing = 'e';
|
||||
s.clock.phase = 'localOps';
|
||||
s.clock.currentActor = 0;
|
||||
turnOf(s, 0).option = 'switch';
|
||||
|
||||
assert.ok(applyIntent(s, 0, { type: 'switch.move', trayId: id, to: at(0, 1), reverse: false }).ok);
|
||||
const { to } = movesFor(s, 0, id);
|
||||
assert.ok(
|
||||
to.some((c) => c.row === 1 && c.col === 1),
|
||||
'the crew cannot carry on round the curve — it can only back out the way it came',
|
||||
);
|
||||
});
|
||||
|
||||
it('turns the DRAWN direction when the curve really does turn the engine round', () => {
|
||||
// South down a spur into a curve that turns west. The engine genuinely now points west, and the
|
||||
// board has to say so — this is the mirrored consist that was reported.
|
||||
const out = roundIt({ row: 1, col: 1 }, { row: 0, col: 1 }, curve('se'), curve('nw'), { facing: 's', railFacing: 'e' });
|
||||
assert.equal(out.facing, 'w', 'the curve turned the engine west');
|
||||
assert.equal(out.drawn, 'w', 'the board would still have drawn the consist facing east');
|
||||
});
|
||||
|
||||
it('still points back the way it came when it BACKS round a curve', () => {
|
||||
// Backing up does not turn a train around, whatever the track does underneath it: the engine
|
||||
// trails, pointing out through the port the train came in by. That is geometry-independent and
|
||||
// was already right — this is here so the fix above cannot quietly change it.
|
||||
const out = roundIt({ row: 1, col: 1 }, { row: 0, col: 1 }, curve('se'), curve('nw'), { facing: 'n', railFacing: 'e' }, true);
|
||||
assert.equal(out.facing, 'n', 'backing up turned the train around');
|
||||
assert.equal(out.drawn, 'e', 'backing up changed which end the engine is on');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user