Exercise combined Homeworld and Nexus games
This commit is contained in:
1 parent
6124618d42
commit
90c5c7af25
3 files changed
+78
-27
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user