Exercise CHOAM Nexus expansion games

This commit is contained in:
Quintine committed 2026-09-27 22:45:53 +10:00
1 parent b7da1389da
commit 99c6ab5863
5 files changed
+63 -16

No files matched your search

+4
View File
@@ -1,5 +1,9 @@
# Dune implementation status # Dune implementation status
## 27 September 2026 — CHOAM/Richese with Nexus Cards
The offline [sample runner](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) now admits genuine CHOAM/Richese setup with the physical Nexus module at two through six seats in Basic and Advanced. At seed 20260928 all ten games completed with 3,481 accepted actions, 90 periodic JSON restores, no rejected actions and one unallied Nexus Card choice. A separate seed 20260927 completed nine games with three choices, then stopped in six-seat Advanced Bidding after 1,863 accepted actions at the existing unresolved exhausted-Richese-cache normal-auction-count guard; the failed report is `/tmp/dune-choam-nexus-trial-20260927/report.json`. The passing source-bound checkpoint report is `/tmp/dune-choam-nexus-checkpoint-20260928/report.json`. No drawn card effect happened in these samples, so they do not certify the CHOAM Betrayal panel, all Nexus effects or public module starts.
## 27 September 2026 — Combined expansion game continuation ## 27 September 2026 — Combined expansion game continuation
The original six [faction samples](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) now complete at seed 20260927, including the six-expansion-faction Basic and Advanced games. Those two combined games reached turn ten and turn nine respectively, with 4,396 accepted actions, 118 periodic JSON restores and no rejected actions. The Advanced trace includes nine explicit Ixian declines on Richese lots, ten Ecaz Ambassador decisions and fifteen Face Dancer decisions; it no longer stops at the historical movement-arrival guard for this seed. The source-bound report is `/tmp/dune-combined-checkpoint-20260927-2183376/report.json`. One seeded roster is not every valid combination, and the Richese Technology exchange remains unresolved; expansion/publication gates stay closed. The original six [faction samples](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) now complete at seed 20260927, including the six-expansion-faction Basic and Advanced games. Those two combined games reached turn ten and turn nine respectively, with 4,396 accepted actions, 118 periodic JSON restores and no rejected actions. The Advanced trace includes nine explicit Ixian declines on Richese lots, ten Ecaz Ambassador decisions and fifteen Face Dancer decisions; it no longer stops at the historical movement-arrival guard for this seed. The source-bound report is `/tmp/dune-combined-checkpoint-20260927-2183376/report.json`. One seeded roster is not every valid combination, and the Richese Technology exchange remains unresolved; expansion/publication gates stay closed.
+6 -3
View File
@@ -104,6 +104,7 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-homeworld-nexus --p
node --import tsx tools/faction-games.ts --out /tmp/dune-new-choam-roster --profile choam-roster node --import tsx tools/faction-games.ts --out /tmp/dune-new-choam-roster --profile choam-roster
node --import tsx tools/faction-games.ts --out /tmp/dune-new-ecaz-roster --profile ecaz-roster node --import tsx tools/faction-games.ts --out /tmp/dune-new-ecaz-roster --profile ecaz-roster
node --import tsx tools/faction-games.ts --out /tmp/dune-new-ix-roster --profile ix-roster node --import tsx tools/faction-games.ts --out /tmp/dune-new-ix-roster --profile ix-roster
node --import tsx tools/faction-games.ts --out /tmp/dune-new-choam-nexus --profile choam-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-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 node --import tsx tools/faction-games.ts --out /tmp/dune-new-resume --resume /private/failed-combined-advanced.json
``` ```
@@ -112,8 +113,8 @@ Default and `--profile all` retain the original six fixed CHOAM/Richese,
Ecaz/Moritani and combined Basic/Advanced samples. `--profile base` selects ten Ecaz/Moritani and combined Basic/Advanced samples. `--profile base` selects ten
additional samples: Basic and Advanced with two through six players. additional samples: Basic and Advanced with two through six players.
`--players 2..6` narrows the base, Homeworld, Nexus, combined `--players 2..6` narrows the base, Homeworld, Nexus, combined
`homeworld-nexus`, `choam-roster`, `ecaz-roster` or `ix-roster` profile; `homeworld-nexus`, `choam-roster`, `ecaz-roster`, `ix-roster` or `choam-nexus`
omission or `all` retains all five counts. The fixed base roster adds Atreides, 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 Harkonnen, Fremen, Emperor, Guild and Bene Gesserit in that order. These are
sample rosters, not an assertion that they are the only rules-permitted player-count sample rosters, not an assertion that they are the only rules-permitted player-count
configurations. Each optional module profile runs two through six base factions configurations. Each optional module profile runs two through six base factions
@@ -122,7 +123,9 @@ profiles also run Basic and Advanced with their respective physical expansion
enabled: CHOAM/Richese start with CHOAM then Richese, Ecaz/Moritani with enabled: CHOAM/Richese start with CHOAM then Richese, Ecaz/Moritani with
Moritani then Ecaz, Ixian/Tleilaxu with Ixians then Tleilaxu; subsequent seats Moritani then Ecaz, Ixian/Tleilaxu with Ixians then Tleilaxu; subsequent seats
extend each to six without changing existing fixed four-seat samples. Their extend each to six without changing existing fixed four-seat samples. Their
seed offsets are 68–77, 78–87 and 88–97. seed offsets are 68–77, 78–87 and 88–97. `choam-nexus` reuses the CHOAM/Richese
roster with both the physical expansion and Nexus Cards enabled at offsets
98–107; it does not alter the fixed six default samples.
Output names include the player count. Homeworld's original four-seat seed 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 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 remain 38–39; its smaller rosters use 40–47. The combined module uses
+15 -1
View File
@@ -6360,6 +6360,18 @@ export function initializeNexusGameForAudit(state: Game): Game {
'Enable Nexus cards in a fresh audit lobby first.'); 'Enable Nexus cards in a fresh audit lobby first.');
return initializeSetupGameForAudit(state, !!state.homeworlds, true); return initializeSetupGameForAudit(state, !!state.homeworlds, true);
} }
/** Offline CHOAM/Richese with Nexus Cards; public expansion/module starts remain gated. */
export function initializeChoamNexusGameForAudit(state: Game): Game {
requireRule(state.expansions.length === 1 && state.expansions[0] === 'choam' &&
state.players.some(player => player.faction === 'choam') &&
state.players.some(player => player.faction === 'richese'),
'The CHOAM Nexus sample requires both expansion factions and their deck.');
requireRule(!!state.nexusCards && state.nexusCards.cards === null && state.nexusCards.phase === null &&
!state.homeworlds && !state.leaderSkills && !state.discoveryEnabled && !state.ecazTreachery,
'Enable only Nexus Cards in a fresh CHOAM expansion lobby.');
return initializeSetupGameForAudit(state, false, true, false, false, false, false, true);
}
/** Offline prototype entry through real Ix setup, including both expansion factions. /** Offline prototype entry through real Ix setup, including both expansion factions.
* No player action or room API bypasses the normal release gates. */ * No player action or room API bypasses the normal release gates. */
export function initializeIxGameForAudit(state: Game): Game { export function initializeIxGameForAudit(state: Game): Game {
@@ -6451,7 +6463,9 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
(f) => f.id === p.faction && (homeworlds || nexus || f.expansion === 'base' || (ix && f.expansion === 'ix') || (choam && f.expansion === 'choam') || (factions && g.expansions.includes(f.expansion))), (f) => f.id === p.faction && (homeworlds || nexus || f.expansion === 'base' || (ix && f.expansion === 'ix') || (choam && f.expansion === 'choam') || (factions && g.expansions.includes(f.expansion))),
), ),
), ),
factions factions && nexus
? 'The CHOAM Nexus audit needs a fresh expansion lobby without other optional modules.'
: factions
? 'The faction prototype supports base factions and the selected expansion factions without optional modules.' ? 'The faction prototype supports base factions and the selected expansion factions without optional modules.'
: choam : choam
? 'The Leader Skills prototype supports base, CHOAM and Richese factions without other expansions or optional modules.' ? 'The Leader Skills prototype supports base, CHOAM and Richese factions without other expansions or optional modules.'
+20 -1
View File
@@ -255,7 +255,7 @@ void test('combined modules reach saved Nexus draws and actual Homeworld shipmen
void test('expansion-roster snapshots resume their own Advanced games rather than the fixed four-seat sample', (t) => { void test('expansion-roster snapshots resume their own Advanced games rather than the fixed four-seat sample', (t) => {
const area = temporary(t); const area = temporary(t);
for (const [profile, seed] of [['choam-roster', '20260928'], ['ecaz-roster', '20260927'], ['ix-roster', '20260927']] as const) { for (const [profile, seed] of [['choam-roster', '20260928'], ['ecaz-roster', '20260927'], ['ix-roster', '20260927'], ['choam-nexus', '20260928']] as const) {
const name = `${profile}-4-advanced`; const name = `${profile}-4-advanced`;
const initial = join(area, `${profile}-initial`); const initial = join(area, `${profile}-initial`);
assert.equal(run(initial, '--profile', profile, '--players', '4', '--rules', 'advanced', '--seed', seed, '--max-actions', '1').status, 1); assert.equal(run(initial, '--profile', profile, '--players', '4', '--rules', 'advanced', '--seed', seed, '--max-actions', '1').status, 1);
@@ -312,6 +312,25 @@ void test('combined-expansion Advanced sample completes with Richese Technology
assert.ok(game.restores > 0); assert.ok(game.restores > 0);
}); });
void test('CHOAM/Richese Nexus game reaches an unallied card choice and restores across phases', (t) => {
const out = join(temporary(t), 'choam-nexus-complete');
const result = run(out, '--profile', 'choam-nexus', '--players', '3', '--rules', 'advanced', '--seed', '20260927');
assert.equal(result.status, 0, result.stderr);
const report = json<CliReport>(join(out, 'report.json'));
const game = 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(report.sourceUnchanged, true);
assert.equal(game.name, 'choam-nexus-3-advanced');
assert.equal(game.outcome, 'complete');
assert.ok(game.used.nexusCardChoice > 0);
assert.deepEqual(game.rejected, {});
assert.ok(game.restores > 0);
});
void test('three-seat Nexus samples preserve their module and roster across saved continuation', (t) => { void test('three-seat Nexus samples preserve their module and roster across saved continuation', (t) => {
const area = temporary(t); const area = temporary(t);
for (const [rules, ordinal] of [['basic', 42], ['advanced', 43]] as const) { for (const [rules, ordinal] of [['basic', 42], ['advanced', 43]] as const) {
+18 -11
View File
@@ -10,6 +10,7 @@ import {
applyAction, applyAction,
createGame, createGame,
initializeBaseGameForAudit, initializeBaseGameForAudit,
initializeChoamNexusGameForAudit,
initializeFactionExpansionsGameForAudit, initializeFactionExpansionsGameForAudit,
initializeHomeworldGameForAudit, initializeHomeworldGameForAudit,
initializeNexusGameForAudit, initializeNexusGameForAudit,
@@ -28,7 +29,7 @@ import { privateOutputDirectory, sourceSnapshot } from './verification';
const DEFAULT_SEED = 20_260_926; const DEFAULT_SEED = 20_260_926;
const DEFAULT_MAX_ACTIONS = 3_500; const DEFAULT_MAX_ACTIONS = 3_500;
const DIFFICULTIES = ['Easy', 'Medium', 'Hard', 'Brutal'] as const; const DIFFICULTIES = ['Easy', 'Medium', 'Hard', 'Brutal'] as const;
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'homeworld' | 'nexus' | 'homeworld-nexus' | 'choam-roster' | 'ecaz-roster' | 'ix-roster' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills'; type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'homeworld' | 'nexus' | 'homeworld-nexus' | 'choam-roster' | 'ecaz-roster' | 'ix-roster' | 'choam-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
type Rules = 'basic' | 'advanced'; type Rules = 'basic' | 'advanced';
type Scenario = { type Scenario = {
@@ -143,6 +144,9 @@ const EXPANSION_ROSTER_SCENARIOS: readonly Scenario[] = [
})), })),
), ),
); );
const CHOAM_NEXUS_SCENARIOS: readonly Scenario[] = EXPANSION_ROSTER_SCENARIOS
.filter(scenario => scenario.profile === 'choam-roster')
.map(scenario => ({ ...scenario, profile: 'choam-nexus', ordinal: scenario.ordinal + 30 }));
const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [ const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [
@@ -215,10 +219,10 @@ type Result = {
function usage() { function usage() {
return ( return (
'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' + 'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' +
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|homeworld|nexus|homeworld-nexus|choam-roster|ecaz-roster|ix-roster|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' + '[--seed UINT32] [--profile all|base|choam|ecaz|combined|homeworld|nexus|homeworld-nexus|choam-roster|ecaz-roster|ix-roster|choam-nexus|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' +
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' + '[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
'[--resume FAILED_GAME.json]\n' + '[--resume FAILED_GAME.json]\n' +
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base, Homeworld, Nexus, combined Homeworld-Nexus, expansion roster 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, expansion roster, CHOAM 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.'
); );
} }
@@ -250,15 +254,15 @@ function positive(value: string | undefined) {
function scenarioName(scenario: Scenario) { function scenarioName(scenario: Scenario) {
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' ||
scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ix-roster' || skillProfile(scenario.profile) scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ix-roster' || scenario.profile === 'choam-nexus' || skillProfile(scenario.profile)
? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}` ? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}`
: `${scenario.profile}-${scenario.rules}`; : `${scenario.profile}-${scenario.rules}`;
} }
function parseProfile(value: string | undefined) { function parseProfile(value: string | undefined) {
const profile = value ?? 'all'; const profile = value ?? 'all';
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster', 'moritani-skills', 'tleilaxu-skills', 'ix-skills', 'choam-skills'].includes(profile)) if (!['all', 'base', 'choam', 'ecaz', 'combined', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster', 'choam-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, choam-roster, ecaz-roster, ix-roster, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.'); throw new Error('--profile must be all, base, choam, ecaz, combined, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster, ix-roster, choam-nexus, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
return profile as Profile | 'all'; return profile as Profile | 'all';
} }
@@ -304,8 +308,10 @@ function freshGame(scenario: Scenario) {
} }
if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus') if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus')
game.homeworlds = { custody: null }; game.homeworlds = { custody: null };
if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus') if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'choam-nexus')
game.nexusCards = { cards: null, phase: null }; game.nexusCards = { cards: null, phase: null };
if (scenario.profile === 'choam-nexus')
return initializeChoamNexusGameForAudit(game);
return skillProfile(scenario.profile) return skillProfile(scenario.profile)
? initializeLeaderSkillsGameForAudit(game) ? initializeLeaderSkillsGameForAudit(game)
: scenario.profile === 'homeworld' : scenario.profile === 'homeworld'
@@ -364,10 +370,10 @@ function resumedGame(path: string) {
game.moritaniAssassinateCallEvents game.moritaniAssassinateCallEvents
) )
throw new Error('--resume sample scenarios exclude unsupported optional modules.'); throw new Error('--resume sample scenarios exclude unsupported optional modules.');
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find( const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...CHOAM_NEXUS_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find(
(candidate) => (candidate) =>
(candidate.profile === 'homeworld' || candidate.profile === 'homeworld-nexus') === !!game.homeworlds && (candidate.profile === 'homeworld' || candidate.profile === 'homeworld-nexus') === !!game.homeworlds &&
(candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus') === !!game.nexusCards && (candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'choam-nexus') === !!game.nexusCards &&
skillProfile(candidate.profile) === !!game.leaderSkills && skillProfile(candidate.profile) === !!game.leaderSkills &&
candidate.rules === (game.advanced ? 'advanced' : 'basic') && candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
JSON.stringify(candidate.expansions) === JSON.stringify(candidate.expansions) ===
@@ -561,14 +567,15 @@ async function main() {
const profile = parseProfile(values.profile); const profile = parseProfile(values.profile);
const rules = parseRules(values.rules); const rules = parseRules(values.rules);
const players = parsePlayers(values.players); const players = parsePlayers(values.players);
if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster'].includes(profile) && !skillProfile(profile)) if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster', 'choam-nexus'].includes(profile) && !skillProfile(profile))
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster, ix-roster or a skill profile.'); throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster, ix-roster, choam-nexus or a skill profile.');
if (skillProfile(profile) && rules === 'advanced') if (skillProfile(profile) && rules === 'advanced')
throw new Error('Expansion Leader Skills profiles currently support only Basic samples.'); throw new Error('Expansion Leader Skills profiles currently support only Basic samples.');
const resume = values.resume ? resumedGame(values.resume) : null; const resume = values.resume ? resumedGame(values.resume) : null;
const requestedSamples = profile === 'homeworld' || profile === 'nexus' || profile === 'homeworld-nexus' const requestedSamples = profile === 'homeworld' || profile === 'nexus' || profile === 'homeworld-nexus'
? MODULE_SCENARIOS ? MODULE_SCENARIOS
: profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS : profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS
: profile === 'choam-nexus' ? CHOAM_NEXUS_SCENARIOS
: profile === 'base' ? BASE_SCENARIOS : profile === 'base' ? BASE_SCENARIOS
: profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS : profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS
: profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS : profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS