Exercise Ixian Nexus expansion games
This commit is contained in:
1 parent
d77aa5472b
commit
2e39ea00bb
5 files changed
+51
-20
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 27 September 2026 — Ixian/Tleilaxu with Nexus Cards
|
||||
|
||||
The paired-faction Nexus sample path now includes genuine Ixian/Tleilaxu games without opening the public expansion/module starts. At seed 20260927 all ten two-through-six-seat Basic/Advanced [samples](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) completed with 3,853 accepted actions, 100 JSON restores, five unallied card choices and no rejected actions. The three-seat Basic and Advanced traces reach Ixian auction decisions and Nexus draws; Basic reaches a Face Dancer decision, Advanced an Ixian Technology decision. A five-seat Basic trace also plays one Atreides Nexus effect, while an interrupted four-seat Advanced game resumes its own roster. The source-bound report is `/tmp/dune-ix-nexus-checkpoint-20260927/report.json`. This does not certify all card effects, combined module configurations or complete expansion rules.
|
||||
|
||||
## 27 September 2026 — Ecaz/Moritani with Nexus Cards
|
||||
|
||||
The paired-faction Nexus audit setup now includes genuine Ecaz/Moritani games, retaining the CHOAM/Richese profile and both public start gates. At seed 20260927 all ten two-through-six-seat Basic/Advanced [samples](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) completed: 3,827 accepted actions, 98 JSON restores, five unallied card choices and no rejected actions. A three-seat Basic trace reaches Ecaz Ambassador decisions, battles and Nexus drawing; an interrupted four-seat Advanced game resumes as its own Ecaz/Nexus roster. The source-bound report is `/tmp/dune-ecaz-nexus-checkpoint-20260927/report.json`. None of those draws activated a Nexus card effect; card-family and combined-mode completion remain gated.
|
||||
|
||||
@@ -106,6 +106,7 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-ecaz-roster --profi
|
||||
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-ecaz-nexus --profile ecaz-nexus
|
||||
node --import tsx tools/faction-games.ts --out /tmp/dune-new-ix-nexus --profile ix-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
|
||||
```
|
||||
@@ -114,8 +115,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`, `ix-roster`, `choam-nexus`
|
||||
or `ecaz-nexus` profile; omission or `all` retains all five counts. The fixed
|
||||
`homeworld-nexus`, `choam-roster`, `ecaz-roster`, `ix-roster`, `choam-nexus`,
|
||||
`ecaz-nexus` or `ix-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 the only rules-permitted
|
||||
player-count configurations. Each optional module profile runs two through
|
||||
@@ -125,9 +126,9 @@ 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.
|
||||
`choam-nexus` and `ecaz-nexus` reuse the respective expansion rosters with Nexus
|
||||
Cards enabled at seed offsets 98–107 and 108–117, respectively. The original
|
||||
six default samples are unchanged.
|
||||
`choam-nexus`, `ecaz-nexus` and `ix-nexus` reuse the respective expansion
|
||||
rosters with Nexus Cards enabled at seed offsets 98–107, 108–117 and 118–127,
|
||||
respectively. The original six default samples are unchanged.
|
||||
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
|
||||
|
||||
+4
-1
@@ -6369,7 +6369,10 @@ export function initializePairedNexusGameForAudit(state: Game): Game {
|
||||
state.players.some(player => player.faction === 'richese')) ||
|
||||
(expansion === 'ecaz' &&
|
||||
state.players.some(player => player.faction === 'ecaz') &&
|
||||
state.players.some(player => player.faction === 'moritani'))),
|
||||
state.players.some(player => player.faction === 'moritani')) ||
|
||||
(expansion === 'ix' &&
|
||||
state.players.some(player => player.faction === 'ixians') &&
|
||||
state.players.some(player => player.faction === 'tleilaxu'))),
|
||||
'The paired Nexus sample requires both selected expansion factions and their deck.');
|
||||
requireRule(!!state.nexusCards && state.nexusCards.cards === null && state.nexusCards.phase === null &&
|
||||
!state.homeworlds && !state.leaderSkills && !state.discoveryEnabled && !state.ecazTreachery,
|
||||
|
||||
@@ -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'], ['choam-nexus', '20260928'], ['ecaz-nexus', '20260927']] as const) {
|
||||
for (const [profile, seed] of [['choam-roster', '20260928'], ['ecaz-roster', '20260927'], ['ix-roster', '20260927'], ['choam-nexus', '20260928'], ['ecaz-nexus', '20260927'], ['ix-nexus', '20260927']] 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);
|
||||
@@ -351,6 +351,30 @@ void test('Ecaz/Moritani Nexus game draws cards and resolves Ambassador battles
|
||||
assert.ok(game.restores > 0);
|
||||
});
|
||||
|
||||
void test('Ixian/Tleilaxu Nexus games reach unallied card choices, Technology and Face Dancers across saves', (t) => {
|
||||
const area = temporary(t);
|
||||
for (const rules of ['basic', 'advanced'] as const) {
|
||||
const out = join(area, rules);
|
||||
const result = run(out, '--profile', 'ix-nexus', '--players', '3', '--rules', rules, '--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, `ix-nexus-3-${rules}`);
|
||||
assert.equal(game.outcome, 'complete');
|
||||
assert.ok(game.used.nexusCardChoice > 0);
|
||||
assert.ok(game.used['decision:ixAuction'] > 0);
|
||||
assert.ok(game.used[rules === 'advanced' ? 'decision:ixTechnology' : 'decision:faceDance'] > 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) {
|
||||
|
||||
+12
-13
@@ -29,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' | 'choam-nexus' | 'ecaz-nexus' | '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' | 'ecaz-nexus' | 'ix-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
|
||||
type Rules = 'basic' | 'advanced';
|
||||
|
||||
type Scenario = {
|
||||
@@ -145,10 +145,9 @@ const EXPANSION_ROSTER_SCENARIOS: readonly Scenario[] = [
|
||||
),
|
||||
);
|
||||
const PAIRED_NEXUS_SCENARIOS: readonly Scenario[] = EXPANSION_ROSTER_SCENARIOS
|
||||
.filter(scenario => scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster')
|
||||
.map(scenario => ({
|
||||
...scenario,
|
||||
profile: scenario.profile === 'choam-roster' ? 'choam-nexus' : 'ecaz-nexus',
|
||||
profile: scenario.profile.replace('-roster', '-nexus') as Profile,
|
||||
ordinal: scenario.ordinal + 30,
|
||||
}));
|
||||
|
||||
@@ -223,7 +222,7 @@ 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|choam-nexus|ecaz-nexus|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|ecaz-nexus|ix-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, paired expansion 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.'
|
||||
@@ -258,15 +257,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' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || skillProfile(scenario.profile)
|
||||
scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ix-roster' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-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', 'choam-nexus', 'ecaz-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, ecaz-nexus, 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', 'ecaz-nexus', 'ix-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, ecaz-nexus, ix-nexus, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
|
||||
return profile as Profile | 'all';
|
||||
}
|
||||
|
||||
@@ -312,9 +311,9 @@ function freshGame(scenario: Scenario) {
|
||||
}
|
||||
if (scenario.profile === 'homeworld' || scenario.profile === 'homeworld-nexus')
|
||||
game.homeworlds = { custody: null };
|
||||
if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus')
|
||||
if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus')
|
||||
game.nexusCards = { cards: null, phase: null };
|
||||
if (scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus')
|
||||
if (scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus')
|
||||
return initializePairedNexusGameForAudit(game);
|
||||
return skillProfile(scenario.profile)
|
||||
? initializeLeaderSkillsGameForAudit(game)
|
||||
@@ -377,7 +376,7 @@ function resumedGame(path: string) {
|
||||
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...PAIRED_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' || candidate.profile === 'choam-nexus' || candidate.profile === 'ecaz-nexus') === !!game.nexusCards &&
|
||||
(candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'choam-nexus' || candidate.profile === 'ecaz-nexus' || candidate.profile === 'ix-nexus') === !!game.nexusCards &&
|
||||
skillProfile(candidate.profile) === !!game.leaderSkills &&
|
||||
candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
|
||||
JSON.stringify(candidate.expansions) ===
|
||||
@@ -571,15 +570,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', 'choam-nexus', 'ecaz-nexus'].includes(profile) && !skillProfile(profile))
|
||||
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster, ix-roster, choam-nexus, ecaz-nexus or a skill profile.');
|
||||
if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster', 'choam-nexus', 'ecaz-nexus', 'ix-nexus'].includes(profile) && !skillProfile(profile))
|
||||
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster, ix-roster, choam-nexus, ecaz-nexus, ix-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' || profile === 'ecaz-nexus' ? PAIRED_NEXUS_SCENARIOS
|
||||
: profile === 'choam-nexus' || profile === 'ecaz-nexus' || profile === 'ix-nexus' ? PAIRED_NEXUS_SCENARIOS
|
||||
: profile === 'base' ? BASE_SCENARIOS
|
||||
: profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS
|
||||
: profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS
|
||||
|
||||
Reference in new issue
Block a user