Expand game engine, replay, tests, and documentation
This commit is contained in:
+67
-13
@@ -11,6 +11,7 @@ import { createGame } from '../src/engine/setup.ts';
|
||||
import type { GameConfig, GameState } from '../src/engine/state.ts';
|
||||
import { developerBot, playGame, randomBot } from '../src/sim/bot.ts';
|
||||
import { simulate } from '../src/sim/harness.ts';
|
||||
import { record } from '../src/sim/replay.ts';
|
||||
import { anomalies, strategyBuckets } from '../src/sim/stats.ts';
|
||||
|
||||
const config: GameConfig = {
|
||||
@@ -152,16 +153,22 @@ describe('the revenue chain works end to end (regression)', () => {
|
||||
|
||||
it('instantiates a real Facility when a freight card is placed', () => {
|
||||
// REGRESSION. Placed facility cards were built with `facility: null` — inert, unworkable.
|
||||
const s = createGame({ id: 'g', seed: 11, config: { ...config, length: 'standard' }, playerNames: ['bot'] });
|
||||
playGame(s, developerBot, pump);
|
||||
//
|
||||
// Checked across several seeds: the design's deck holds only 9 industries in 115 cards, so a
|
||||
// single game may legitimately never draw one. That dilution is itself worth knowing — under
|
||||
// the old 52-card placeholder it was 10 in 52.
|
||||
let placed = 0;
|
||||
for (const card of s.officeAreas.get(0)!.grid.values()) {
|
||||
if (card.geometry.kind !== 'facility') continue;
|
||||
placed++;
|
||||
assert.ok(card.facility, 'a placed facility card has no Facility record');
|
||||
assert.ok(card.facility.laborers > 0, 'facility has no Laborers');
|
||||
for (const seed of [11, 23, 47, 91, 137, 211]) {
|
||||
const s = createGame({ id: 'g', seed, config: { ...config, length: 'campaign' }, playerNames: ['bot'] });
|
||||
playGame(s, developerBot, pump);
|
||||
for (const card of s.officeAreas.get(0)!.grid.values()) {
|
||||
if (card.geometry.kind !== 'facility') continue;
|
||||
placed++;
|
||||
assert.ok(card.facility, 'a placed facility card has no Facility record');
|
||||
assert.ok(card.facility.laborers > 0, 'facility has no Laborers');
|
||||
}
|
||||
}
|
||||
assert.ok(placed > 0, 'no facility was placed at all');
|
||||
assert.ok(placed > 0, 'no facility was placed across six campaign games');
|
||||
});
|
||||
|
||||
it('earns freight revenue, not just passenger revenue', () => {
|
||||
@@ -180,16 +187,17 @@ 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 in both directions (Gap 11)', () => {
|
||||
// With orientation fixed, grids only ever grew sideways and downward.
|
||||
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.
|
||||
const rows = new Set<number>();
|
||||
for (const seed of [3, 9, 27, 81]) {
|
||||
const s = createGame({ id: 'g', seed, config: { ...config, length: 'standard' }, playerNames: ['bot'] });
|
||||
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 north of the Running Track');
|
||||
assert.ok([...rows].some((r) => r < 0), 'nothing was ever built south of the Running Track');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,3 +241,49 @@ describe('end-of-game statistics', () => {
|
||||
assert.equal(bucketed, scored.length, 'a scoring game fell outside every bucket');
|
||||
});
|
||||
});
|
||||
|
||||
describe('switching accomplishes something (regression)', () => {
|
||||
it('does not shuttle the crew back and forth to no purpose', () => {
|
||||
// REGRESSION. The bot chose to SWITCH whenever a train stood at the Office, whether or not
|
||||
// there was anything to switch — then, having nothing useful to do, took the first legal move
|
||||
// repeatedly and burned all six Moves oscillating between two cells. A passenger train needs
|
||||
// no switching at all (§9.2 works coaches straight off the A/D track), so an entire Local
|
||||
// Operations action was wasted.
|
||||
const rec = record(1234, 'standard');
|
||||
|
||||
let worstRun = 0;
|
||||
let run = 0;
|
||||
let lastFrom: string | null = null;
|
||||
|
||||
for (const f of rec.frames) {
|
||||
const move = f.lines.find((l) => /^CREW moved/.test(l.text));
|
||||
const didWork = f.lines.some((l) => /Dropped|Coupled/.test(l.text));
|
||||
if (!move || didWork) {
|
||||
run = 0;
|
||||
lastFrom = null;
|
||||
continue;
|
||||
}
|
||||
const m = /\((-?\d+),(-?\d+)\) → \((-?\d+),(-?\d+)\)/.exec(move.text);
|
||||
if (!m) continue;
|
||||
const from = `${m[1]},${m[2]}`;
|
||||
const to = `${m[3]},${m[4]}`;
|
||||
// An oscillation is a move that lands exactly where the previous move started.
|
||||
run = lastFrom === to ? run + 1 : 0;
|
||||
worstRun = Math.max(worstRun, run);
|
||||
lastFrom = from;
|
||||
}
|
||||
|
||||
assert.ok(worstRun < 3, `crew oscillated ${worstRun + 1} times without doing any work`);
|
||||
});
|
||||
|
||||
it('does not spend its whole Local Operations budget on motion', () => {
|
||||
// The absolute ratio is no longer a fair test: with only 9 industries in 115 cards there is
|
||||
// often nothing to switch, so `work` is legitimately near zero. What still must hold is that
|
||||
// the crew is not burning all six Moves every Stage — the shuttling bug.
|
||||
const report = simulate({
|
||||
games: 40, length: 'standard', mode: 'solitaire', players: ['bot'], policy: developerBot,
|
||||
});
|
||||
const moves = report.perGame.reduce((n, g) => n + g.actions.moves, 0) / report.perGame.length;
|
||||
assert.ok(moves < 60, `${moves.toFixed(0)} Moves per game suggests aimless shuttling`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user