diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 381c6e49..c5a6da26 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -6,6 +6,15 @@ Expansion starts and full rules/publication acceptance remain gated.** ## Current checkpoint and work +The bounded [Advanced Moritani Assassinate Leaders preview](MORITANI_ASSASSINATE_LEADERS.md) +now has repeatable genuine games across two through six supported seats. +Five samples completed 2,511 accepted actions, no rejected candidates +and 65 JSON continuations. The six-seat game reached two private +assassination offers, both declined; real reveal and replacement still +depend on the focused physical-custody evidence, not a natural full-game +sample. The unresolved duration/UX questions and public expansion gate +remain unchanged. This source is not in the last verified live image. + The independent Ecaz three-card variant now has a repeatable [genuine-game sample profile](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) for paired Ecaz/Moritani rosters from two to six seats. Ten Basic/Advanced diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 0f1c36a6..8547d8d6 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,34 @@ # Dune implementation status +## 29 September 2026 — Advanced Moritani assassination sample matrix + +The reusable offline runner now supports the explicitly gated +`moritani-assassinate` profile with native Moritani and supported classic +opponents excluding Harkonnen, across two through six Advanced seats. +Initialization uses the actual preview path; resume requires both its saved +marker and ability state, and the external physical Traitor census includes +publicly retired assassination cards only after Mentat replacement. The +existing six default samples and their ordinals are unchanged. + +At base seed `20260929`, all five games completed in 2,511 accepted actions, +zero rejected candidates and 65 JSON continuations, with an unchanged +source fingerprint. The six-seat game reached two actual private +assassination offers; both were declined. Genuine battle/Mentat focused +tests independently exercise reveal, retirement, replacement and duplicate +custody rejection; the natural game samples do **not** establish that use. +Source-bound private report: +`/tmp/dune-moritani-assassinate-matrix-20260929/report.json`, based on +`c961a93` plus this checkpoint's uncommitted source (fingerprint +`8889d9bf25b964948913588aa3320a3f47fd2b3f1fd79edf9df12397ce687c57`). +Independent read-only review found no concrete profile or custody defect. +The pending rule-duration and uniform-private-step UX questions remain. +No complete Moritani faction, public Moritani start or deployed acceptance +follows from these samples. + +The focused assassination/CLI/custody union passes 56/56 cases. +`npm run check` passes typecheck, lint and 5,767/5,767 offline cases. +No app route, database schema or deployed game changed in this checkpoint. + ## 29 September 2026 — Ecaz card variant full-game integration samples The reusable offline runner now has `--profile ecaz-treachery`: two through diff --git a/docs/MORITANI_ASSASSINATE_LEADERS.md b/docs/MORITANI_ASSASSINATE_LEADERS.md index 53a0daf4..96576da3 100644 --- a/docs/MORITANI_ASSASSINATE_LEADERS.md +++ b/docs/MORITANI_ASSASSINATE_LEADERS.md @@ -120,3 +120,16 @@ server is reused. Broad checks, source fingerprint, final saved-game preservatio and Git delivery are recorded with the checkpoint. Focused and staged evidence does not certify a complete faction, all combinations or public Advanced readiness. + +## 29 September 2026 — five-roster game continuation + +The reusable [faction-game runner](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) +now exercises this exact gated Advanced preview from two through six seats +with Moritani and only the supported classic opponents except Harkonnen. +Five games at seed `20260929` completed in 2,511 accepted actions, with +zero rejected candidates and 65 JSON continuations. The six-seat game +opened two genuine private assassination decisions; both were declined. +The existing staged battle/replacement cases and the external physical +custody check cover the actual reveal, retirement and new-card draw, but +the natural full-game samples do **not** demonstrate a reveal. The source +boundaries and pending user timing/UX questions above remain unchanged. diff --git a/docs/VERIFICATION_WORKFLOW.md b/docs/VERIFICATION_WORKFLOW.md index 44118440..8cb752e6 100644 --- a/docs/VERIFICATION_WORKFLOW.md +++ b/docs/VERIFICATION_WORKFLOW.md @@ -112,6 +112,7 @@ node --import tsx tools/faction-games.ts --out /tmp/dune-new-choam-roster --prof 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-ecaz-cards --profile ecaz-treachery +node --import tsx tools/faction-games.ts --out /tmp/dune-new-moritani-assassinate --profile moritani-assassinate 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 @@ -143,6 +144,13 @@ independent three-card Treachery variant instead of Nexus Cards. Basic and Advanced seed offsets are 136–145; default/all keeps its original six games. The runner counts all three physical cards and resume requires the variant marker and exact roster. This is still a gated development profile. +`moritani-assassinate` runs only Advanced with Moritani followed by Atreides, +Emperor, Fremen, Guild and Bene Gesserit as seats are added. Harkonnen and +other optional modules remain outside this explicit development preview. +Five scenario offsets 146–150 leave earlier profiles unchanged. A resumed +snapshot must retain both the preview and its authoritative ability state; +the external physical census includes each public set-aside Traitor Card after +its actual Mentat replacement. `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. `combined-nexus` selects five or six actual expansion factions and all three @@ -171,6 +179,15 @@ writer paths are documented in [the implementation log](IMPLEMENTATION_STATUS.md#29-september-2026--ecaz-card-variant-full-game-integration-samples). This is not expansion release evidence. +At the same base seed, five Advanced Moritani assassination preview games +completed with 2,511 accepted actions, zero rejected candidates and 65 JSON +continuations. The six-seat game reached two actual private +assassination opportunities, both declined; it did **not** naturally test +reveal and replacement. Focused engine/custody regressions cover that +physical path. Private report: +`/tmp/dune-moritani-assassinate-matrix-20260929/report.json`. This is +sampled continuation, not a duration ruling or public expansion readiness. + 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 20260927 seed, each six-seat sample made six alliance actions and completed; @@ -199,11 +216,12 @@ Advanced requests fail before running. Its seed offsets are 16–20, leaving existing samples unchanged. The full fourteen-card skill census and ordinary Moritani/base Traitor census are checked throughout. `--resume` preserves this explicit profile; other optional modules still fail its admission checks. -Homeworld, Nexus, independent Ecaz-card and combined snapshots resume only -when their saved modules and fixed rosters match a supported profile; other -optional-module combinations remain excluded. An edited snapshot with a -removed module can match a different fixed profile, so verify its original -private report before relying on resumed provenance. +Homeworld, Nexus, independent Ecaz-card, Advanced Moritani assassination and +combined snapshots resume only when their saved modules and fixed rosters +match a supported profile; other optional-module combinations remain +excluded. An edited snapshot with a removed module can match a different +fixed profile, so verify its original private report before relying on +resumed provenance. All samples use genuine setup and saved AI profiles, cycling Easy, Medium, Hard and Brutal by seat. Smaller games necessarily contain fewer profiles; diff --git a/tests/faction-games-cli.test.ts b/tests/faction-games-cli.test.ts index 34c85eac..385a44d5 100644 --- a/tests/faction-games-cli.test.ts +++ b/tests/faction-games-cli.test.ts @@ -119,6 +119,27 @@ void test('Ecaz card samples keep all three physical identities through saved re assertFailedEvidence(resumed, 'ecaz-treachery-2-advanced', 1137, true); }); +void test('Advanced Moritani assassination samples retain their preview and physical Traitor pool on resume', (t) => { + const area = temporary(t); + const out = join(area, 'assassinate'); + assert.equal(run(out, '--profile', 'moritani-assassinate', '--players', '3', + '--seed', '1000', '--max-actions', '1').status, 1); + assertFailedEvidence(out, 'moritani-assassinate-3-advanced', 1147, false); + const snapshot = join(out, 'failed-moritani-assassinate-3-advanced.json'); + const game = json<{ advanced: boolean; moritaniAssassinatePreview: boolean; + moritaniAssassinate: { owner: string; opportunities: unknown[] }; + players: { id: string; faction: string }[] }>(snapshot); + assert.equal(game.advanced, true); + assert.equal(game.moritaniAssassinatePreview, true); + assert.equal(game.moritaniAssassinate.owner, game.players.find(p => p.faction === 'moritani')?.id); + const resumed = join(area, 'resumed'); + assert.equal(run(resumed, '--resume', snapshot, '--seed', '1000', '--max-actions', '1').status, 1); + assertFailedEvidence(resumed, 'moritani-assassinate-3-advanced', 1147, true); + const unsupported = join(area, 'basic'); + assert.equal(run(unsupported, '--profile', 'moritani-assassinate', '--rules', 'basic').status, 1); + assert.equal(existsSync(unsupported), false); +}); + void test('Moritani Skills samples preserve their full module on saved continuation and reject Advanced before running', (t) => { const area = temporary(t); const out = join(area, 'skills'); diff --git a/tests/sample-custody.test.ts b/tests/sample-custody.test.ts index 714e364b..640ff3dc 100644 --- a/tests/sample-custody.test.ts +++ b/tests/sample-custody.test.ts @@ -12,6 +12,13 @@ import { } from '../game/engine'; import { sampleInventory, verifySampleCustody } from '../tools/sample-custody'; import { completedMoritaniSkillsGame } from './moritani-skills-fixture'; +import { + assassinationChoice, + assassinationGame, + assassinationToMentat, + resolveAssassinationBattle, + stageAssassinationBattle, +} from './moritani-assassinate-fixture'; void test('Moritani Skills sample custody checks the physical skill and Traitor inventories after genuine setup', () => { const game = completedMoritaniSkillsGame(); @@ -29,6 +36,23 @@ void test('Moritani Skills sample custody checks the physical skill and Traitor assert.throws(() => verifySampleCustody(traitors, inventory), /traitor custody/); }); +void test('Moritani assassination samples conserve retired Traitors after real battle and Mentat replacement', () => { + const start = assassinationGame(); + const inventory = sampleInventory(start); + assert.ok(inventory.traitors); + verifySampleCustody(start, inventory); + const staged = stageAssassinationBattle(start); + verifySampleCustody(staged, inventory); + const revealed = assassinationChoice(resolveAssassinationBattle(staged), 'guild-1'); + verifySampleCustody(revealed, inventory); + const settled = assassinationToMentat(revealed); + assert.equal(settled.moritaniAssassinate?.opportunities[0].stage, 'replaced'); + verifySampleCustody(settled, inventory); + const duplicated = structuredClone(settled); + duplicated.traitorReserve!.push('guild-1'); + assert.throws(() => verifySampleCustody(duplicated, inventory), /physical traitor custody/); +}); + function fixture(advanced = false) { let game = createGame( 'SAMPLEQA', diff --git a/tools/faction-games.ts b/tools/faction-games.ts index c3bdf845..6498df62 100644 --- a/tools/faction-games.ts +++ b/tools/faction-games.ts @@ -15,6 +15,7 @@ import { initializeNexusGameForAudit, initializePairedNexusGameForAudit, initializeEcazTreacheryGameForAudit, + initializeMoritaniAssassinateGameForAudit, initializeCombinedNexusGameForAudit, initializeLeaderSkillsGameForAudit, joinGame, @@ -31,7 +32,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' | 'ecaz-treachery' | 'combined' | 'combined-nexus' | 'combined-homeworld-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 Profile = 'base' | 'choam' | 'ecaz' | 'ecaz-treachery' | 'moritani-assassinate' | 'combined' | 'combined-nexus' | 'combined-homeworld-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 = { @@ -183,6 +184,16 @@ const MORITANI_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(playe expansions: ['ecaz'], roster: MORITANI_SKILLS_ROSTER.slice(0, players), })); +const MORITANI_ASSASSINATE_ROSTER: readonly FactionId[] = [ + 'moritani', 'atreides', 'emperor', 'fremen', 'guild', 'beneGesserit', +]; +const MORITANI_ASSASSINATE_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({ + ordinal: 146 + players - 2, + profile: 'moritani-assassinate', + rules: 'advanced', + expansions: ['ecaz'], + roster: MORITANI_ASSASSINATE_ROSTER.slice(0, players), +})); const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({ ordinal: 21 + players - 2, @@ -243,10 +254,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|ecaz-treachery|combined|combined-nexus|combined-homeworld-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] ' + + '[--seed UINT32] [--profile all|base|choam|ecaz|ecaz-treachery|moritani-assassinate|combined|combined-nexus|combined-homeworld-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; combined-nexus and combined-homeworld-nexus add five/six-seat Basic/Advanced all-expansion samples. Base, Homeworld, Nexus, Homeworld-Nexus, expansion roster, paired expansion Nexus, Ecaz card variant 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.' + 'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; combined-nexus and combined-homeworld-nexus add five/six-seat Basic/Advanced all-expansion samples. Base, Homeworld, Nexus, Homeworld-Nexus, expansion roster, paired expansion Nexus, Ecaz card variant, Advanced Moritani assassination and skill profiles select their documented rosters. --players requires a supported profile. Output must be a new private directory outside the checkout.' ); } @@ -278,15 +289,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 === 'combined-nexus' || scenario.profile === 'combined-homeworld-nexus' || - scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ecaz-treachery' || scenario.profile === 'ix-roster' || scenario.profile === 'choam-nexus' || scenario.profile === 'ecaz-nexus' || scenario.profile === 'ix-nexus' || skillProfile(scenario.profile) + scenario.profile === 'choam-roster' || scenario.profile === 'ecaz-roster' || scenario.profile === 'ecaz-treachery' || scenario.profile === 'moritani-assassinate' || 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', 'ecaz-treachery', 'combined', 'combined-nexus', 'combined-homeworld-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, ecaz-treachery, combined, combined-nexus, combined-homeworld-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.'); + if (!['all', 'base', 'choam', 'ecaz', 'ecaz-treachery', 'moritani-assassinate', 'combined', 'combined-nexus', 'combined-homeworld-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, ecaz-treachery, moritani-assassinate, combined, combined-nexus, combined-homeworld-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'; } @@ -340,6 +351,8 @@ function freshGame(scenario: Scenario) { return initializePairedNexusGameForAudit(game); if (scenario.profile === 'ecaz-treachery') return initializeEcazTreacheryGameForAudit(game); + if (scenario.profile === 'moritani-assassinate') + return initializeMoritaniAssassinateGameForAudit(game); return skillProfile(scenario.profile) ? initializeLeaderSkillsGameForAudit(game) : scenario.profile === 'homeworld' @@ -390,19 +403,17 @@ function resumedGame(path: string) { game.greatMaker || game.techTokens || game.strongholdCards || - game.mentatQuestionPreview || - game.moritaniAssassinatePreview || - game.moritaniAssassinate || - game.moritaniAssassinateResume || - game.moritaniAssassinateCallEvents + game.mentatQuestionPreview ) throw new Error('--resume sample scenarios exclude unsupported optional modules.'); - const scenario = [...SCENARIOS, ...COMBINED_NEXUS_SCENARIOS, ...COMBINED_HOMEWORLD_NEXUS_SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...ECAZ_TREACHERY_SCENARIOS, ...PAIRED_NEXUS_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find( + const scenario = [...SCENARIOS, ...COMBINED_NEXUS_SCENARIOS, ...COMBINED_HOMEWORLD_NEXUS_SCENARIOS, ...BASE_SCENARIOS, ...MODULE_SCENARIOS, ...EXPANSION_ROSTER_SCENARIOS, ...ECAZ_TREACHERY_SCENARIOS, ...PAIRED_NEXUS_SCENARIOS, ...MORITANI_ASSASSINATE_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS, ...IX_SKILLS_SCENARIOS, ...CHOAM_SKILLS_SCENARIOS].find( (candidate) => (candidate.profile === 'homeworld' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'combined-homeworld-nexus') === !!game.homeworlds && (candidate.profile === 'nexus' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'choam-nexus' || candidate.profile === 'ecaz-nexus' || candidate.profile === 'ix-nexus' || candidate.profile === 'combined-nexus' || candidate.profile === 'combined-homeworld-nexus') === !!game.nexusCards && skillProfile(candidate.profile) === !!game.leaderSkills && (candidate.profile === 'ecaz-treachery') === !!game.ecazTreachery && + (candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinatePreview && + (candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinate && candidate.rules === (game.advanced ? 'advanced' : 'basic') && JSON.stringify(candidate.expansions) === JSON.stringify(game.expansions) && @@ -595,17 +606,20 @@ 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', 'combined-nexus', 'combined-homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ecaz-treachery', 'ix-roster', 'choam-nexus', 'ecaz-nexus', 'ix-nexus'].includes(profile) && !skillProfile(profile)) + if (supplied('players') && !['base', 'homeworld', 'nexus', 'homeworld-nexus', 'combined-nexus', 'combined-homeworld-nexus', 'choam-roster', 'ecaz-roster', 'ecaz-treachery', 'moritani-assassinate', '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' || profile === 'combined-homeworld-nexus') && players !== 'all' && ![5, 6].includes(players)) throw new Error(`--profile ${profile} supports only five or six players.`); if (skillProfile(profile) && rules === 'advanced') throw new Error('Expansion Leader Skills profiles currently support only Basic samples.'); + if (profile === 'moritani-assassinate' && rules === 'basic') + throw new Error('Moritani assassination samples require Advanced rules.'); 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 === 'ecaz-treachery' ? ECAZ_TREACHERY_SCENARIOS + : profile === 'moritani-assassinate' ? MORITANI_ASSASSINATE_SCENARIOS : profile === 'choam-nexus' || profile === 'ecaz-nexus' || profile === 'ix-nexus' ? PAIRED_NEXUS_SCENARIOS : profile === 'combined-nexus' ? COMBINED_NEXUS_SCENARIOS : profile === 'combined-homeworld-nexus' ? COMBINED_HOMEWORLD_NEXUS_SCENARIOS diff --git a/tools/sample-custody.ts b/tools/sample-custody.ts index 0f05ce96..7e3e7255 100644 --- a/tools/sample-custody.ts +++ b/tools/sample-custody.ts @@ -20,8 +20,9 @@ export function sampleInventory(game: Game) { ] .map((card) => card.id) .sort(), - // Other expansion assassination zones require a separate adapter. - traitors: game.expansions.length && !basicExpansionLeaderSkillsProfile(game) + // The bounded Moritani preview has a separate public retired-card zone. + traitors: game.expansions.length && !basicExpansionLeaderSkillsProfile(game) && + !game.moritaniAssassinatePreview ? null : traitorDeck( game.players.map((player) => ({ leaders: leaders(player.faction) })), @@ -130,6 +131,9 @@ export function verifySampleCustody( ...game.players.flatMap((player) => [ ...player.traitors, ...(player.faceDancers ?? []).map(card => card.leader), ]), + ...(game.moritaniAssassinate?.opportunities ?? []) + .filter(receipt => receipt.stage === 'replaced') + .map(receipt => receipt.card!), ].sort(), inventory.traitors, 'physical traitor custody',