diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 5c953cae..a0547c8e 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -120,7 +120,7 @@ bulk actions and backup/operations tools remain unfinished. [Participant AI](ADMIN_PARTICIPANT_AI.md) checkpoint `2cddf03` is pushed. Types/lint, -The latest [complete-game integration samples](IMPLEMENTATION_STATUS.md) +Earlier [complete-game integration samples](IMPLEMENTATION_STATUS.md) finished 62 genuine games: Basic Leader Skills with Tleilaxu, Moritani, Ixians or CHOAM; Basic/Advanced classic Homeworld + Nexus; all three paired faction-expansion + Nexus profiles across two through six seats; and two @@ -128,6 +128,13 @@ six-seat all-expansion games. All 29,333 actions were accepted, with 765 JSON/private-view restorations and unchanged source fingerprints. Rare effects not encountered naturally and all public release gates remain unverified. +Four further [all-expansion Nexus samples](IMPLEMENTATION_STATUS.md) +completed genuine five/six-seat Basic and Advanced games: 4,098 accepted +actions, 109 JSON/private-view restorations, unchanged source fingerprints +and five Nexus Card draw/keep decisions in the five-seat games. No Nexus +effect was played in those samples; card-specific combinations and public +expansion/Nexus starts remain gated. + The bounded [native Richese Nexus Cunning shipment](NEXUS_RICHESE_RULES.md#bounded-native-cunning-prototype-27-september-2026) now uses two distinct physical No-Field tokens in one priced shipment in genuine Richese/CHOAM paired Nexus games. One token immediately reveals a reserve-capped public force group; the other remains concealed. Owner-only controls, four legal AI profiles, Karama/Guild responses, signed saved receipts and concurrent in-memory SQLite settlement are connected in Basic/Advanced. A read-only local browser fixture displayed the owner selector and no rival private panel. Same-shipment nonrepeat is a documented product interpretation; faceup-component artwork, combined modules, Betrayal and full Nexus acceptance remain gated. This code is local and not in the last verified production image. The independent [Reinforcements Battle Plan prototype](REINFORCEMENTS_RUNTIME.md) diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index ba6ac51b..aaf116b8 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,21 @@ # Dune implementation status +## 28 September 2026 — five/six-seat all-expansion Nexus integration samples + +The offline-only `combined-nexus` audit setup selects all three physical +expansions and Nexus Cards for five seats (Ixians, Tleilaxu, CHOAM, Ecaz, +Moritani) or six (plus Richese). Basic and Advanced samples for each roster +completed with 4,098 accepted actions, zero rejected bot actions and 109 +JSON/private-view restores. The five-seat tables reached five real Nexus Card +draw/keep decisions; the six-seat tables formed alliances but did not draw +cards. The report at `/tmp/dune-all-expansion-nexus-matrix-final/report.json` +records `status: passed` and unchanged source. A separate intentionally +capped ten-action five-seat save resumed to a turn-nine win with 1,380 accepted +actions, 37 more restores and zero rejected actions. The resumed random stream +restarts, so its outcome need not match an uninterrupted game. No card effect +happened in these samples, so they do not establish individual card correctness, +complete cross-expansion behavior or permission to open public starts. + ## 28 September 2026 — owner-only operational counts `/admin/operations` now samples the build revision and live database aggregates: diff --git a/docs/VERIFICATION_WORKFLOW.md b/docs/VERIFICATION_WORKFLOW.md index 1fcd48e7..13057dcc 100644 --- a/docs/VERIFICATION_WORKFLOW.md +++ b/docs/VERIFICATION_WORKFLOW.md @@ -114,6 +114,7 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-ix-roster --profile 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-combined-nexus --profile combined-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 ``` @@ -122,8 +123,9 @@ 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`, -`ecaz-nexus` or `ix-nexus` profile; omission or `all` retains all five counts. The fixed +`homeworld-nexus`, expansion-roster or paired-Nexus profiles. The +`combined-nexus` profile supports `--players 5` or `6` only. Omission or `all` +retains every roster in a selected profile. 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 @@ -134,13 +136,17 @@ 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`, `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 -offsets 58–67. These samples do not open public module or expansion starts or -assert complete interaction coverage. +rosters with Nexus Cards enabled at seed offsets 98–107, 108–117 and 118–127. +`combined-nexus` selects five or six actual expansion factions and all three +expansion decks with Nexus Cards in Basic and Advanced. Five seats leave Richese +absent so a player can remain unallied and draw; six seats contain every expansion +faction. Their seed offsets are 130–131 and 128–129 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 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 diff --git a/game/engine.ts b/game/engine.ts index 30e0da36..100fe488 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -6716,6 +6716,22 @@ export function initializePairedNexusGameForAudit(state: Game): Game { return initializeSetupGameForAudit(state, false, true, false, false, false, false, true); } +/** Offline-only all-expansion Nexus composition; not a public start gate. */ +export function initializeCombinedNexusGameForAudit(state: Game): Game { + requireRule(state.expansions.length === 3 && + ['ix', 'choam', 'ecaz'].every(expansion => state.expansions.includes(expansion)) && + [5, 6].includes(state.players.length) && + state.players.every(player => ['ixians', 'tleilaxu', 'choam', 'richese', 'ecaz', 'moritani'].includes(player.faction)) && + ['ixians', 'tleilaxu', 'choam', 'ecaz', 'moritani'] + .every(faction => state.players.some(player => player.faction === faction)), + 'The combined Nexus sample requires five or six expansion factions and their three decks.'); + requireRule(!!state.nexusCards && state.nexusCards.cards === null && state.nexusCards.phase === null && + !state.homeworlds && !state.leaderSkills && !state.discoveryEnabled && + !state.ecazTreachery && !state.techTokens && !state.strongholdCards, + 'Enable only Nexus Cards in a fresh combined 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 { diff --git a/tools/faction-games.ts b/tools/faction-games.ts index 68e236af..76c008d1 100644 --- a/tools/faction-games.ts +++ b/tools/faction-games.ts @@ -14,6 +14,7 @@ import { initializeHomeworldGameForAudit, initializeNexusGameForAudit, initializePairedNexusGameForAudit, + initializeCombinedNexusGameForAudit, initializeLeaderSkillsGameForAudit, joinGame, newPlayer, @@ -29,7 +30,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' | 'ix-nexus' | 'moritani-skills' | 'tleilaxu-skills' | 'ix-skills' | 'choam-skills'; +type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'combined-nexus' | '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 = { @@ -150,7 +151,15 @@ const PAIRED_NEXUS_SCENARIOS: readonly Scenario[] = EXPANSION_ROSTER_SCENARIOS profile: scenario.profile.replace('-roster', '-nexus') as Profile, ordinal: scenario.ordinal + 30, })); - +const COMBINED_NEXUS_SCENARIOS: readonly Scenario[] = [ + ...SCENARIOS.filter(scenario => scenario.profile === 'combined') + .map(scenario => ({ ...scenario, profile: 'combined-nexus' as const, ordinal: scenario.ordinal + 124 })), + ...(['basic', 'advanced'] as const).map((rules, index) => ({ + ordinal: 130 + index, profile: 'combined-nexus' as const, rules, + expansions: ['ix', 'choam', 'ecaz'], + roster: ['ecaz', 'ixians', 'tleilaxu', 'choam', 'moritani'] as FactionId[], + })), +]; const MORITANI_SKILLS_ROSTER: readonly FactionId[] = [ 'moritani', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit', @@ -222,10 +231,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|choam-nexus|ecaz-nexus|ix-nexus|moritani-skills|tleilaxu-skills|ix-skills|choam-skills] ' + + '[--seed UINT32] [--profile all|base|choam|ecaz|combined|combined-nexus|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.' + 'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; combined-nexus adds five/six-seat Basic/Advanced all-expansion Nexus samples. Base, Homeworld, Nexus, Homeworld-Nexus, expansion roster, paired expansion Nexus and skill profiles default to their 2–6-player samples. --players requires a supported profile. Output must be a new private directory outside the checkout.' ); } @@ -256,7 +265,7 @@ function positive(value: string | undefined) { } 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 === 'combined-nexus' || 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}`; @@ -264,8 +273,8 @@ function scenarioName(scenario: Scenario) { 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', '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.'); + if (!['all', 'base', 'choam', 'ecaz', 'combined', 'combined-nexus', '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, combined-nexus, 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'; } @@ -311,8 +320,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' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus') + if (scenario.profile === 'nexus' || scenario.profile === 'homeworld-nexus' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus' || scenario.profile === 'combined-nexus') game.nexusCards = { cards: null, phase: null }; + if (scenario.profile === 'combined-nexus') + return initializeCombinedNexusGameForAudit(game); if (scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus') return initializePairedNexusGameForAudit(game); return skillProfile(scenario.profile) @@ -373,10 +384,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, ...PAIRED_NEXUS_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find( + const scenario = [...SCENARIOS, ...COMBINED_NEXUS_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' || candidate.profile === 'ix-nexus') === !!game.nexusCards && + (candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'choam-nexus' || candidate.profile === 'ecaz-nexus' || candidate.profile === 'ix-nexus' || candidate.profile === 'combined-nexus') === !!game.nexusCards && skillProfile(candidate.profile) === !!game.leaderSkills && candidate.rules === (game.advanced ? 'advanced' : 'basic') && JSON.stringify(candidate.expansions) === @@ -570,8 +581,10 @@ 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', '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 (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'combined-nexus', 'choam-roster', 'ecaz-roster', 'ix-roster', 'choam-nexus', 'ecaz-nexus', 'ix-nexus'].includes(profile) && !skillProfile(profile)) + throw new Error('--players requires a roster or optional-module profile.'); + if (profile === 'combined-nexus' && players !== 'all' && ![5, 6].includes(players)) + throw new Error('--profile combined-nexus supports only five or six players.'); 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; @@ -579,6 +592,7 @@ async function main() { ? MODULE_SCENARIOS : profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS : profile === 'choam-nexus' || profile === 'ecaz-nexus' || profile === 'ix-nexus' ? PAIRED_NEXUS_SCENARIOS + : profile === 'combined-nexus' ? COMBINED_NEXUS_SCENARIOS : profile === 'base' ? BASE_SCENARIOS : profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS : profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS