Sample expansion faction rosters from two to six seats
This commit is contained in:
1 parent
0bf61c8067
commit
2fbc754484
4 files changed
+65
-21
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 27 September 2026 — CHOAM/Richese and Ecaz/Moritani roster samples
|
||||
|
||||
The [sample runner](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) now covers each expansion pair in Basic and Advanced at two through six seats, using genuine faction setup, legal AI decisions and periodic JSON restoration. The ten CHOAM/Richese games at seed 20260928 and ten Ecaz/Moritani games at seed 20260927 completed: 6,523 accepted actions, 167 restores, no rejected actions. Source-bound checkpoint reports are `/tmp/dune-choam-roster-checkpoint-20260928/report.json` and `/tmp/dune-ecaz-roster-checkpoint-20260927/report.json`. A separate CHOAM/Richese seed 20260927 completed nine games but stopped in six-seat Advanced Bidding after 1,728 accepted actions: an exhausted Richese cache reached the existing explicit unresolved normal-auction-count guard. That source question remains open, not papered over by the passing matrix. These samples do not certify full expansion rules, all rosters or public starts.
|
||||
|
||||
## 27 September 2026 — Bounded CHOAM Nexus Betrayal
|
||||
|
||||
The [CHOAM Betrayal panel](NEXUS_CARD_RULES.md#bounded-choam-betrayal-runtime) now has a playable Basic/Advanced path when CHOAM is seated. An unallied rival spends the physical CHOAM Nexus card at a clean boundary; the server samples and discards one actual held CHOAM Treachery Card without spice compensation. The holder-only control never offers a rival card choice or identities; its hand count appears only during Bidding. Bots act only when that count is public. A saved discard receipt completes once after JSON restore, including later same-turn Nexus recycling. Focused quote, controls, bot, physical inventory, rejection and interruption cases pass; the isolated browser table rendered the enabled holder control without room writes. Complete Nexus, combined-module and deployed revision acceptance remain open.
|
||||
|
||||
@@ -101,6 +101,8 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-combined --profile
|
||||
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-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-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
|
||||
```
|
||||
@@ -108,18 +110,22 @@ 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, 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.
|
||||
`--players 2..6` narrows the base, Homeworld, Nexus, combined
|
||||
`homeworld-nexus`, `choam-roster` or `ecaz-roster` 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
|
||||
under Basic and Advanced using the actual setup seams. The two expansion-roster
|
||||
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; subsequent seats extend each to six without changing
|
||||
existing fixed four-seat samples. Their seed offsets are 68–77 and 78–87.
|
||||
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 or expansion starts or
|
||||
assert complete interaction coverage.
|
||||
|
||||
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
|
||||
|
||||
@@ -253,6 +253,27 @@ void test('combined modules reach saved Nexus draws and actual Homeworld shipmen
|
||||
assert.ok(evidence.restores > 0);
|
||||
});
|
||||
|
||||
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']] 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);
|
||||
const snapshot = join(initial, `failed-${name}.json`);
|
||||
const continued = join(area, `${profile}-continued`);
|
||||
const resumed = run(continued, '--resume', snapshot, '--seed', seed);
|
||||
assert.equal(resumed.status, 0, resumed.stderr);
|
||||
const report = json<CliReport>(join(continued, 'report.json'));
|
||||
const result = json<{ results: (CliResult & { restores: number; rejected: Record<string, number> })[] }>(join(continued, 'results.json')).results[0];
|
||||
assert.equal(report.status, 'passed');
|
||||
assert.equal(result.name, name);
|
||||
assert.equal(result.outcome, 'complete');
|
||||
assert.equal(result.resumed, true);
|
||||
assert.deepEqual(result.rejected, {});
|
||||
assert.ok(result.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) {
|
||||
|
||||
+22
-9
@@ -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' | 'homeworld-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
|
||||
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'homeworld' | 'nexus' | 'homeworld-nexus' | 'choam-roster' | 'ecaz-roster' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills';
|
||||
type Rules = 'basic' | 'advanced';
|
||||
|
||||
type Scenario = {
|
||||
@@ -131,6 +131,17 @@ const MODULE_SCENARIOS: readonly Scenario[] = [
|
||||
})),
|
||||
),
|
||||
];
|
||||
const EXPANSION_ROSTER_SCENARIOS: readonly Scenario[] = [
|
||||
{ profile: 'choam-roster' as const, expansions: ['choam'], roster: ['choam', 'richese', 'emperor', 'guild', 'harkonnen', 'fremen'] as FactionId[] },
|
||||
{ profile: 'ecaz-roster' as const, expansions: ['ecaz'], roster: ['moritani', 'ecaz', 'atreides', 'beneGesserit', 'harkonnen', 'guild'] as FactionId[] },
|
||||
].flatMap(({ profile, expansions, roster }, family) =>
|
||||
[2, 3, 4, 5, 6].flatMap(players =>
|
||||
(['basic', 'advanced'] as const).map((rules, index) => ({
|
||||
ordinal: 68 + family * 10 + (players - 2) * 2 + index,
|
||||
profile, rules, expansions, roster: roster.slice(0, players),
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [
|
||||
@@ -203,10 +214,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|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' +
|
||||
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|homeworld|nexus|homeworld-nexus|choam-roster|ecaz-roster|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 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 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.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,15 +248,16 @@ function positive(value: string | undefined) {
|
||||
}
|
||||
|
||||
function scenarioName(scenario: Scenario) {
|
||||
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || skillProfile(scenario.profile)
|
||||
return scenario.profile === 'base' || scenario.profile === 'homeworld' || scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' ||
|
||||
scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || 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', '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.');
|
||||
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-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, moritani-skills, tleilaxu-skills, ix-skills or choam-skills.');
|
||||
return profile as Profile | 'all';
|
||||
}
|
||||
|
||||
@@ -351,7 +363,7 @@ function resumedGame(path: string) {
|
||||
game.moritaniAssassinateCallEvents
|
||||
)
|
||||
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(
|
||||
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_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 &&
|
||||
@@ -548,13 +560,14 @@ 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'].includes(profile) && !skillProfile(profile))
|
||||
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus or a skill profile.');
|
||||
if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'choam-roster', 'ecaz-roster'].includes(profile) && !skillProfile(profile))
|
||||
throw new Error('--players requires --profile base, homeworld, nexus, homeworld-nexus, choam-roster, ecaz-roster 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' ? EXPANSION_ROSTER_SCENARIOS
|
||||
: profile === 'base' ? BASE_SCENARIOS
|
||||
: profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS
|
||||
: profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS
|
||||
|
||||
Reference in new issue
Block a user