Exercise Nexus games across two to six seats
This commit is contained in:
1 parent
8045c9e3eb
commit
628c536218
3 files changed
+77
-25
No files matched your search
@@ -97,29 +97,36 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-moritani-skills --p
|
|||||||
node --import tsx tools/faction-games.ts --out /tmp/dune-new-tleilaxu-skills --profile tleilaxu-skills
|
node --import tsx tools/faction-games.ts --out /tmp/dune-new-tleilaxu-skills --profile tleilaxu-skills
|
||||||
node --import tsx tools/faction-games.ts --out /tmp/dune-new-base-three --profile base --players 3 --rules advanced
|
node --import tsx tools/faction-games.ts --out /tmp/dune-new-base-three --profile base --players 3 --rules advanced
|
||||||
node --import tsx tools/faction-games.ts --out /tmp/dune-new-combined --profile combined --rules advanced --seed 20260926
|
node --import tsx tools/faction-games.ts --out /tmp/dune-new-combined --profile combined --rules advanced --seed 20260926
|
||||||
# Homeworld (four base factions) and Nexus (six), Basic and Advanced.
|
# Homeworld (four base factions) and Nexus (two through six), 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-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-nexus --profile 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
|
node --import tsx tools/faction-games.ts --out /tmp/dune-new-resume --resume /private/failed-combined-advanced.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Default and `--profile all` retain the original six fixed CHOAM/Richese,
|
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 only that base profile; omission or `all` retains all
|
`--players 2..6` narrows the base or Nexus profile; omission or `all` retains
|
||||||
five counts. The fixed base roster adds Atreides, Harkonnen, Fremen, Emperor,
|
all five counts. The fixed base roster adds Atreides, Harkonnen, Fremen,
|
||||||
Guild and Bene Gesserit in that order. These are sample rosters, not an assertion
|
Emperor, Guild and Bene Gesserit in that order. These are sample rosters, not
|
||||||
that these are the only rules-permitted player-count configurations. The
|
an assertion that these are the only rules-permitted player-count
|
||||||
`homeworld` profile runs four base factions; `nexus` runs all six, each under Basic
|
configurations. The `homeworld` profile runs four base factions; `nexus` runs
|
||||||
and Advanced using the actual optional-module setup seams. They do not open
|
two through six, each under Basic and Advanced using the actual
|
||||||
public module starts or assert complete module interaction coverage.
|
optional-module setup seams. Nexus output names include the player count;
|
||||||
|
the six-seat random seed offsets remain 38–39, and smaller rosters use 40–47.
|
||||||
|
They do not open public module starts or assert complete module interaction
|
||||||
|
coverage.
|
||||||
|
|
||||||
All-AI Nexus samples now make public reciprocal offers as well as accepting
|
All-AI Nexus samples now make public reciprocal offers as well as accepting
|
||||||
them, so an actual alliance can form without a human proposer. At the
|
them, so an actual alliance can form without a human proposer. At the
|
||||||
20260927 seed, each six-seat sample made six alliance actions and completed;
|
20260927 seed, each six-seat sample made six alliance actions and completed;
|
||||||
all six seats paired, leaving no unallied Nexus Card recipient. The focused
|
no Nexus Card choice occurred at those tables. The two-to-six-seat matrix
|
||||||
Nexus alliance regression exercises the subsequent draw when a seat remains
|
completed without rejected actions and with JSON restores throughout; the
|
||||||
unallied. These samples do not establish complete card-effect coverage.
|
three-seat Advanced sample and five-seat Basic sample reached actual
|
||||||
|
`nexusCardChoice` actions. The focused Nexus alliance regression exercises
|
||||||
|
the subsequent draw when a seat remains unallied. These samples do not
|
||||||
|
establish complete card-effect coverage.
|
||||||
|
|
||||||
`--profile moritani-skills` adds five Basic samples with Moritani, then Emperor,
|
`--profile moritani-skills` adds five Basic samples with Moritani, then Emperor,
|
||||||
Guild, Harkonnen, Fremen and Bene Gesserit. `--players` also narrows this profile;
|
Guild, Harkonnen, Fremen and Bene Gesserit. `--players` also narrows this profile;
|
||||||
|
|||||||
@@ -180,15 +180,17 @@ void test('Homeworld and Nexus sample snapshots resume only their matching modul
|
|||||||
['nexus', 39, 'nexusCards', 'homeworlds'],
|
['nexus', 39, 'nexusCards', 'homeworlds'],
|
||||||
] as const) {
|
] as const) {
|
||||||
const out = join(area, profile);
|
const out = join(area, profile);
|
||||||
assert.equal(run(out, '--profile', profile, '--rules', 'advanced', '--seed', '1000', '--max-actions', '1').status, 1);
|
const name = profile === 'nexus' ? 'nexus-6-advanced' : 'homeworld-advanced';
|
||||||
assertFailedEvidence(out, `${profile}-advanced`, 1000 + ordinal, false);
|
const playerFilter = profile === 'nexus' ? ['--players', '6'] : [];
|
||||||
const snapshot = join(out, `failed-${profile}-advanced.json`);
|
assert.equal(run(out, '--profile', profile, '--rules', 'advanced', ...playerFilter, '--seed', '1000', '--max-actions', '1').status, 1);
|
||||||
|
assertFailedEvidence(out, name, 1000 + ordinal, false);
|
||||||
|
const snapshot = join(out, `failed-${name}.json`);
|
||||||
const game = json<Record<string, unknown>>(snapshot);
|
const game = json<Record<string, unknown>>(snapshot);
|
||||||
assert.ok(game[module], `${profile} module remains in the snapshot`);
|
assert.ok(game[module], `${profile} module remains in the snapshot`);
|
||||||
assert.equal(Boolean(game[other]), false);
|
assert.equal(Boolean(game[other]), false);
|
||||||
const resumed = join(area, `${profile}-resumed`);
|
const resumed = join(area, `${profile}-resumed`);
|
||||||
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
|
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
|
||||||
assertFailedEvidence(resumed, `${profile}-advanced`, 1000 + ordinal, true);
|
assertFailedEvidence(resumed, name, 1000 + ordinal, true);
|
||||||
const unsupported = join(area, `${profile}-combined.json`);
|
const unsupported = join(area, `${profile}-combined.json`);
|
||||||
writeFileSync(unsupported, JSON.stringify({ ...game, [other]: game[module] }), { mode: 0o600 });
|
writeFileSync(unsupported, JSON.stringify({ ...game, [other]: game[module] }), { mode: 0o600 });
|
||||||
const refused = join(area, `${profile}-refused`);
|
const refused = join(area, `${profile}-refused`);
|
||||||
@@ -198,6 +200,40 @@ void test('Homeworld and Nexus sample snapshots resume only their matching modul
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const out = join(area, rules);
|
||||||
|
const initial = run(out, '--profile', 'nexus', '--players', '3', '--rules', rules, '--seed', '1000', '--max-actions', '1');
|
||||||
|
assert.equal(initial.status, 1);
|
||||||
|
assertFailedEvidence(out, `nexus-3-${rules}`, 1000 + ordinal, false);
|
||||||
|
const snapshot = join(out, `failed-nexus-3-${rules}.json`);
|
||||||
|
const game = json<{ players: unknown[]; nexusCards: unknown }>(snapshot);
|
||||||
|
assert.equal(game.players.length, 3);
|
||||||
|
assert.ok(game.nexusCards);
|
||||||
|
const resumed = join(area, `${rules}-resumed`);
|
||||||
|
assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1);
|
||||||
|
assertFailedEvidence(resumed, `nexus-3-${rules}`, 1000 + ordinal, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('a genuine three-seat Advanced Nexus game reaches unallied card choice and completes without illegal bot actions', (t) => {
|
||||||
|
const out = join(temporary(t), 'card-draw');
|
||||||
|
const result = run(out, '--profile', 'nexus', '--players', '3', '--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.deepEqual(evidence.rejected, {});
|
||||||
|
assert.ok(evidence.restores > 0);
|
||||||
|
});
|
||||||
|
|
||||||
void test('selected genuine three-player Advanced base sample fails honestly, resumes its fixed snapshot and keeps source-bound private evidence', (t) => {
|
void test('selected genuine three-player Advanced base sample fails honestly, resumes its fixed snapshot and keeps source-bound private evidence', (t) => {
|
||||||
const area = temporary(t);
|
const area = temporary(t);
|
||||||
const first = join(area, 'first');
|
const first = join(area, 'first');
|
||||||
|
|||||||
+19
-10
@@ -102,15 +102,24 @@ const BASE_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].flatMap((players) =>
|
|||||||
roster: BASE_ROSTER.slice(0, players),
|
roster: BASE_ROSTER.slice(0, players),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const MODULE_SCENARIOS: readonly Scenario[] = (['homeworld', 'nexus'] as const).flatMap((profile, index) =>
|
const MODULE_SCENARIOS: readonly Scenario[] = [
|
||||||
(['basic', 'advanced'] as const).map((rules, ruleIndex) => ({
|
...(['basic', 'advanced'] as const).map((rules, index) => ({
|
||||||
ordinal: 36 + 2 * index + ruleIndex,
|
ordinal: 36 + index,
|
||||||
profile,
|
profile: 'homeworld' as const,
|
||||||
rules,
|
rules,
|
||||||
expansions: [],
|
expansions: [],
|
||||||
roster: BASE_ROSTER.slice(0, profile === 'nexus' ? 6 : 4),
|
roster: BASE_ROSTER.slice(0, 4),
|
||||||
})),
|
})),
|
||||||
);
|
...[2, 3, 4, 5, 6].flatMap((players) =>
|
||||||
|
(['basic', 'advanced'] as const).map((rules, index) => ({
|
||||||
|
ordinal: players === 6 ? 38 + index : 40 + (players - 2) * 2 + index,
|
||||||
|
profile: 'nexus' as const,
|
||||||
|
rules,
|
||||||
|
expansions: [],
|
||||||
|
roster: BASE_ROSTER.slice(0, players),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [
|
const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [
|
||||||
@@ -186,7 +195,7 @@ function usage() {
|
|||||||
'[--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|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 and skill profiles default to their 2–6-player samples. Homeworld uses four base factions; Nexus uses all six. --players requires --profile base or a skill profile. Output must be a new private directory outside the checkout.'
|
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base, Nexus and skill profiles default to their 2–6-player samples. Homeworld uses four base factions. --players requires --profile base, nexus or a skill profile. Output must be a new private directory outside the checkout.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +226,7 @@ function positive(value: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scenarioName(scenario: Scenario) {
|
function scenarioName(scenario: Scenario) {
|
||||||
return scenario.profile === 'base' || skillProfile(scenario.profile)
|
return scenario.profile === 'base' || scenario.profile === '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}`;
|
||||||
}
|
}
|
||||||
@@ -529,8 +538,8 @@ 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') && profile !== 'base' && !skillProfile(profile))
|
if (supplied('players') && profile !== 'base' && profile !== 'nexus' && !skillProfile(profile))
|
||||||
throw new Error('--players requires --profile base, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
|
throw new Error('--players requires --profile base, 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;
|
||||||
|
|||||||
Reference in new issue
Block a user