Exercise combined Homeworld and Nexus games

This commit is contained in:
Quintine committed 2026-09-27 21:32:01 +10:00
1 parent 6124618d42
commit 90c5c7af25
3 files changed
+78 -27

No files matched your search

+22 -12
View File
@@ -100,6 +100,7 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-combined --profile
# Homeworld and Nexus with two through six base factions, Basic and Advanced.
node --import tsx tools/faction-games.ts --out /tmp/dune-new-homeworld --profile homeworld
node --import tsx tools/faction-games.ts --out /tmp/dune-new-nexus --profile nexus
node --import tsx tools/faction-games.ts --out /tmp/dune-new-homeworld-nexus --profile homeworld-nexus
node --import tsx tools/faction-games.ts --out /tmp/dune-new-nexus-three --profile nexus --players 3 --rules advanced
node --import tsx tools/faction-games.ts --out /tmp/dune-new-resume --resume /private/failed-combined-advanced.json
```
@@ -107,15 +108,16 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-resume --resume /pr
Default and `--profile all` retain the original six fixed CHOAM/Richese,
Ecaz/Moritani and combined Basic/Advanced samples. `--profile base` selects ten
additional samples: Basic and Advanced with two through six players.
`--players 2..6` narrows the base, Homeworld or Nexus profile; omission or
`all` retains all five counts. The fixed base roster adds Atreides, Harkonnen,
Fremen, Emperor, Guild and Bene Gesserit in that order. These are sample
rosters, not an assertion that these are the only rules-permitted player-count
configurations. Homeworld and Nexus each run two through six base factions
under Basic and Advanced using the actual optional-module setup seams.
Their output names include the player count. Homeworld's original four-seat
seed offsets remain 36–37; its other rosters use 48–51 and 54–57. Nexus's
six-seat offsets remain 38–39; its smaller rosters use 40–47. These samples
`--players 2..6` narrows the base, Homeworld, Nexus or combined
`homeworld-nexus` profile; omission or `all` retains all five counts. The fixed
base roster adds Atreides, Harkonnen, Fremen, Emperor, Guild and Bene
Gesserit in that order. These are sample rosters, not an assertion that these
are the only rules-permitted player-count configurations. Each optional
module profile runs two through six base factions under Basic and Advanced
using the actual setup seams. Output names include the player count.
Homeworld's original four-seat seed offsets remain 36–37; its other rosters
use 48–51 and 54–57. Nexus's six-seat offsets remain 38–39; its smaller
rosters use 40–47. The combined module uses offsets 58–67. These samples
do not open public module starts or assert complete module interaction
coverage.
@@ -135,15 +137,23 @@ Junction offers/shipments and a Caladan reinforcement decision; these
observations do not certify every Homeworld occupation or expansion
interaction.
At the same seed, all ten combined Homeworld/Nexus samples completed without
rejected actions and with JSON restores. The five-seat Advanced game reached
three Nexus Card choices and eight Homeworld shipments in one physical game,
including 26 restores. This does not certify every interaction between the
two independent modules or either module's unfinished card effects.
`--profile moritani-skills` adds five Basic samples with Moritani, then Emperor,
Guild, Harkonnen, Fremen and Bene Gesserit. `--players` also narrows this profile;
Advanced requests fail before running. Its seed offsets are 16–20, leaving
existing samples unchanged. The full fourteen-card skill census and ordinary
Moritani/base Traitor census are checked throughout. `--resume` preserves this
explicit profile; other optional modules still fail its admission checks.
Homeworld and Nexus snapshots may be resumed only when their saved module and
fixed roster match the corresponding profile; unsupported module combinations
are still rejected.
Homeworld, Nexus and combined snapshots resume only when their saved modules
and fixed rosters match a supported profile; other optional module
combinations remain excluded. An edited snapshot with a removed module can
match a different fixed profile, so verify its original private report
before relying on resumed provenance.
All samples use genuine setup and saved AI profiles, cycling Easy, Medium,
Hard and Brutal by seat. Smaller games necessarily contain fewer profiles;
+33
View File
@@ -220,6 +220,39 @@ void test('Homeworld samples cover each two-to-six-seat roster under both rules
assertFailedEvidence(resumed, 'homeworld-2-advanced', 1049, true);
});
void test('combined Homeworld and Nexus snapshots keep both physical modules across saved continuation', (t) => {
const area = temporary(t);
const out = join(area, 'combined');
assert.equal(run(out, '--profile', 'homeworld-nexus', '--players', '3', '--rules', 'advanced', '--seed', '1000', '--max-actions', '1').status, 1);
assertFailedEvidence(out, 'homeworld-nexus-3-advanced', 1061, false);
const snapshot = join(out, 'failed-homeworld-nexus-3-advanced.json');
const game = json<Record<string, unknown>>(snapshot);
assert.equal((game.players as unknown[]).length, 3);
assert.ok(game.homeworlds);
assert.ok(game.nexusCards);
const resumed = join(area, 'resumed');
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
assertFailedEvidence(resumed, 'homeworld-nexus-3-advanced', 1061, true);
});
void test('combined modules reach saved Nexus draws and actual Homeworld shipment in one complete game', (t) => {
const out = join(temporary(t), 'complete');
const result = run(out, '--profile', 'homeworld-nexus', '--players', '5', '--rules', 'advanced', '--seed', '20260927');
assert.equal(result.status, 0, result.stderr);
const report = json<CliReport>(join(out, 'report.json'));
const evidence = json<{ results: (CliResult & {
used: Record<string, number>;
rejected: Record<string, number>;
restores: number;
})[] }>(join(out, 'results.json')).results[0];
assert.equal(report.status, 'passed');
assert.equal(evidence.outcome, 'complete');
assert.ok(evidence.used.nexusCardChoice > 0);
assert.ok(evidence.used.homeworldShip > 0);
assert.deepEqual(evidence.rejected, {});
assert.ok(evidence.restores > 0);
});
void test('three-seat Nexus samples preserve their module and roster across saved continuation', (t) => {
const area = temporary(t);
for (const [rules, ordinal] of [['basic', 42], ['advanced', 43]] as const) {
+23 -15
View File
@@ -28,7 +28,7 @@ import { privateOutputDirectory, sourceSnapshot } from './verification';
const DEFAULT_SEED = 20_260_926;
const DEFAULT_MAX_ACTIONS = 3_500;
const DIFFICULTIES = ['Easy', 'Medium', 'Hard', 'Brutal'] as const;
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'homeworld' | 'nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'homeworld' | 'nexus' | 'homeworld-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
type Rules = 'basic' | 'advanced';
type Scenario = {
@@ -121,6 +121,15 @@ const MODULE_SCENARIOS: readonly Scenario[] = [
roster: BASE_ROSTER.slice(0, players),
})),
),
...[2, 3, 4, 5, 6].flatMap((players) =>
(['basic', 'advanced'] as const).map((rules, index) => ({
ordinal: 58 + (players - 2) * 2 + index,
profile: 'homeworld-nexus' as const,
rules,
expansions: [],
roster: BASE_ROSTER.slice(0, players),
})),
),
];
@@ -194,10 +203,10 @@ type Result = {
function usage() {
return (
'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' +
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|homeworld|nexus|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' +
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|homeworld|nexus|homeworld-nexus|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' +
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
'[--resume FAILED_GAME.json]\n' +
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base, Homeworld, Nexus and skill profiles default to their 2–6-player samples. --players requires one of these profiles. Output must be a new private directory outside the checkout.'
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base, Homeworld, Nexus, combined Homeworld-Nexus and skill profiles default to their 2–6-player samples. --players requires one of these profiles. Output must be a new private directory outside the checkout.'
);
}
@@ -228,15 +237,15 @@ function positive(value: string | undefined) {
}
function scenarioName(scenario: Scenario) {
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || skillProfile(scenario.profile)
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || skillProfile(scenario.profile)
? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}`
: `${scenario.profile}-${scenario.rules}`;
}
function parseProfile(value: string | undefined) {
const profile = value ?? 'all';
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'homeworld', 'nexus', 'moritani-skills', 'tleilaxu-skills', 'ix-skills', 'choam-skills'].includes(profile))
throw new Error('--profile must be all, base, choam, ecaz, combined, homeworld, nexus, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'homeworld', 'nexus', 'homeworld-nexus', 'moritani-skills', 'tleilaxu-skills', 'ix-skills', 'choam-skills'].includes(profile))
throw new Error('--profile must be all, base, choam, ecaz, combined, homeworld, nexus, homeworld-nexus, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
return profile as Profile | 'all';
}
@@ -280,15 +289,15 @@ function freshGame(scenario: Scenario) {
player.bot = DIFFICULTIES[index % DIFFICULTIES.length];
player.ready = true;
}
if (scenario.profile === 'homeworld')
if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus')
game.homeworlds = { custody: null };
if (scenario.profile === 'nexus')
if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus')
game.nexusCards = { cards: null, phase: null };
return skillProfile(scenario.profile)
? initializeLeaderSkillsGameForAudit(game)
: scenario.profile === 'homeworld'
? initializeHomeworldGameForAudit(game)
: scenario.profile === 'nexus'
: scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus'
? initializeNexusGameForAudit(game)
: scenario.profile === 'base'
? initializeBaseGameForAudit(game)
@@ -327,7 +336,6 @@ function resumedGame(path: string) {
'--resume is not an incomplete faction-games snapshot with saved AI profiles.',
);
if (
(game.homeworlds && game.nexusCards) ||
(game.leaderSkills && !basicExpansionLeaderSkillsProfile(game)) ||
game.discoveryEnabled ||
game.discoveries ||
@@ -345,8 +353,8 @@ function resumedGame(path: string) {
throw new Error('--resume sample scenarios exclude unsupported optional modules.');
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find(
(candidate) =>
(candidate.profile === 'homeworld') === !!game.homeworlds &&
(candidate.profile === 'nexus') === !!game.nexusCards &&
(candidate.profile === 'homeworld' || candidate.profile === 'homeworld-nexus') === !!game.homeworlds &&
(candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus') === !!game.nexusCards &&
skillProfile(candidate.profile) === !!game.leaderSkills &&
candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
JSON.stringify(candidate.expansions) ===
@@ -540,12 +548,12 @@ async function main() {
const profile = parseProfile(values.profile);
const rules = parseRules(values.rules);
const players = parsePlayers(values.players);
if (supplied('players') && !['base', 'homeworld', 'nexus'].includes(profile) && !skillProfile(profile))
throw new Error('--players requires --profile base, homeworld, nexus or a skill profile.');
if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus'].includes(profile) && !skillProfile(profile))
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus or a skill profile.');
if (skillProfile(profile) && rules === 'advanced')
throw new Error('Expansion Leader Skills profiles currently support only Basic samples.');
const resume = values.resume ? resumedGame(values.resume) : null;
const requestedSamples = profile === 'homeworld' || profile === 'nexus'
const requestedSamples = profile === 'homeworld' || profile === 'nexus' || profile === 'homeworld-nexus'
? MODULE_SCENARIOS
: profile === 'base' ? BASE_SCENARIOS
: profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS