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
## 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
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-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-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-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
additional samples: Basic and Advanced with two through six players.
`--players 2..6` narrows the base, Homeworld, Nexus, combined
`homeworld-nexus`, `choam-roster`, `ecaz-roster` or `ix-roster` profile;
omission or `all` retains all five counts. The fixed base roster adds Atreides,
`homeworld-nexus`, `choam-roster`, `ecaz-roster`, `ix-roster` or `choam-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 they are the only rules-permitted player-count
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
Moritani then Ecaz, Ixian/Tleilaxu with Ixians then Tleilaxu; subsequent seats
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
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
+15 -1
View File
@@ -6360,6 +6360,18 @@ export function initializeNexusGameForAudit(state: Game): Game {
'Enable Nexus cards in a fresh audit lobby first.');
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.
* No player action or room API bypasses the normal release gates. */
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))),
),
),
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.'
: choam
? '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) => {
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 initial = join(area, `${profile}-initial`);
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);
});
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) => {
const area = temporary(t);
for (const [rules, ordinal] of [['basic', 42], ['advanced', 43]] as const) {
+18 -11
View File
@@ -10,6 +10,7 @@ import {
applyAction,
createGame,
initializeBaseGameForAudit,
initializeChoamNexusGameForAudit,
initializeFactionExpansionsGameForAudit,
initializeHomeworldGameForAudit,
initializeNexusGameForAudit,
@@ -28,7 +29,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' | '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 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[] = [
@@ -215,10 +219,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|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] ' +
'[--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) {
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.rules}`;
}
function parseProfile(value: string | undefined) {
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))
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.');
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, choam-nexus, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
return profile as Profile | 'all';
}
@@ -304,8 +308,10 @@ function freshGame(scenario: Scenario) {
}
if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus')
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 };
if (scenario.profile === 'choam-nexus')
return initializeChoamNexusGameForAudit(game);
return skillProfile(scenario.profile)
? initializeLeaderSkillsGameForAudit(game)
: scenario.profile === 'homeworld'
@@ -364,10 +370,10 @@ function resumedGame(path: string) {
game.moritaniAssassinateCallEvents
)
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.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 &&
candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
JSON.stringify(candidate.expansions) ===
@@ -561,14 +567,15 @@ async function main() {
const profile = parseProfile(values.profile);
const rules = parseRules(values.rules);
const players = parsePlayers(values.players);
if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster'].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.');
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, choam-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' || profile === 'homeworld-nexus'
? MODULE_SCENARIOS
: profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS
: profile === 'choam-nexus' ? CHOAM_NEXUS_SCENARIOS
: profile === 'base' ? BASE_SCENARIOS
: profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS
: profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS