Exercise Advanced Moritani assassination preview games

This commit is contained in:
Quintine committed 2026-09-29 21:39:31 +10:00
1 parent c961a93f62
commit ac8742c829
8 files changed
+152 -20

No files matched your search

+9
View File
@@ -6,6 +6,15 @@ Expansion starts and full rules/publication acceptance remain gated.**
## Current checkpoint and work ## 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 The independent Ecaz three-card variant now has a repeatable
[genuine-game sample profile](VERIFICATION_WORKFLOW.md#reusable-base-and-faction-sample-games) [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 for paired Ecaz/Moritani rosters from two to six seats. Ten Basic/Advanced
+29
View File
@@ -1,5 +1,34 @@
# Dune implementation status # 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 ## 29 September 2026 — Ecaz card variant full-game integration samples
The reusable offline runner now has `--profile ecaz-treachery`: two through The reusable offline runner now has `--profile ecaz-treachery`: two through
+13
View File
@@ -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 and Git delivery are recorded with the checkpoint. Focused and staged evidence
does not certify a complete faction, all combinations or public does not certify a complete faction, all combinations or public
Advanced readiness. 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.
+23 -5
View File
@@ -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-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-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-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-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-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-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. 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 The runner counts all three physical cards and resume requires the variant
marker and exact roster. This is still a gated development profile. 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 `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. 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 `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). [the implementation log](IMPLEMENTATION_STATUS.md#29-september-2026--ecaz-card-variant-full-game-integration-samples).
This is not expansion release evidence. 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 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;
@@ -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 existing samples unchanged. The full fourteen-card skill census and ordinary
Moritani/base Traitor census are checked throughout. `--resume` preserves this Moritani/base Traitor census are checked throughout. `--resume` preserves this
explicit profile; other optional modules still fail its admission checks. explicit profile; other optional modules still fail its admission checks.
Homeworld, Nexus, independent Ecaz-card and combined snapshots resume only Homeworld, Nexus, independent Ecaz-card, Advanced Moritani assassination and
when their saved modules and fixed rosters match a supported profile; other combined snapshots resume only when their saved modules and fixed rosters
optional-module combinations remain excluded. An edited snapshot with a match a supported profile; other optional-module combinations remain
removed module can match a different fixed profile, so verify its original excluded. An edited snapshot with a removed module can match a different
private report before relying on resumed provenance. 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, All samples use genuine setup and saved AI profiles, cycling Easy, Medium,
Hard and Brutal by seat. Smaller games necessarily contain fewer profiles; Hard and Brutal by seat. Smaller games necessarily contain fewer profiles;
+21
View File
@@ -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); 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) => { void test('Moritani Skills samples preserve their full module on saved continuation and reject Advanced before running', (t) => {
const area = temporary(t); const area = temporary(t);
const out = join(area, 'skills'); const out = join(area, 'skills');
+24
View File
@@ -12,6 +12,13 @@ import {
} from '../game/engine'; } from '../game/engine';
import { sampleInventory, verifySampleCustody } from '../tools/sample-custody'; import { sampleInventory, verifySampleCustody } from '../tools/sample-custody';
import { completedMoritaniSkillsGame } from './moritani-skills-fixture'; 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', () => { void test('Moritani Skills sample custody checks the physical skill and Traitor inventories after genuine setup', () => {
const game = completedMoritaniSkillsGame(); 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/); 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) { function fixture(advanced = false) {
let game = createGame( let game = createGame(
'SAMPLEQA', 'SAMPLEQA',
+27 -13
View File
@@ -15,6 +15,7 @@ import {
initializeNexusGameForAudit, initializeNexusGameForAudit,
initializePairedNexusGameForAudit, initializePairedNexusGameForAudit,
initializeEcazTreacheryGameForAudit, initializeEcazTreacheryGameForAudit,
initializeMoritaniAssassinateGameForAudit,
initializeCombinedNexusGameForAudit, initializeCombinedNexusGameForAudit,
initializeLeaderSkillsGameForAudit, initializeLeaderSkillsGameForAudit,
joinGame, joinGame,
@@ -31,7 +32,7 @@ import { privateOutputDirectory, sourceSnapshot } from './verification';
const DEFAULT_SEED = 20_260_926; const DEFAULT_SEED = 20_260_926;
const DEFAULT_MAX_ACTIONS = 3_500; const DEFAULT_MAX_ACTIONS = 3_500;
const DIFFICULTIES = ['Easy', 'Medium', 'Hard', 'Brutal'] as const; 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 Rules = 'basic' | 'advanced';
type Scenario = { type Scenario = {
@@ -183,6 +184,16 @@ const MORITANI_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(playe
expansions: ['ecaz'], expansions: ['ecaz'],
roster: MORITANI_SKILLS_ROSTER.slice(0, players), 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 => ({ const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({
ordinal: 21 + players - 2, ordinal: 21 + players - 2,
@@ -243,10 +254,10 @@ type Result = {
function usage() { function usage() {
return ( return (
'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' + '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] ' + '[--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; 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) { 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' || 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.roster.length}-${scenario.rules}`
: `${scenario.profile}-${scenario.rules}`; : `${scenario.profile}-${scenario.rules}`;
} }
function parseProfile(value: string | undefined) { function parseProfile(value: string | undefined) {
const profile = value ?? 'all'; 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)) 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, 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.'); 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'; return profile as Profile | 'all';
} }
@@ -340,6 +351,8 @@ function freshGame(scenario: Scenario) {
return initializePairedNexusGameForAudit(game); return initializePairedNexusGameForAudit(game);
if (scenario.profile === 'ecaz-treachery') if (scenario.profile === 'ecaz-treachery')
return initializeEcazTreacheryGameForAudit(game); return initializeEcazTreacheryGameForAudit(game);
if (scenario.profile === 'moritani-assassinate')
return initializeMoritaniAssassinateGameForAudit(game);
return skillProfile(scenario.profile) return skillProfile(scenario.profile)
? initializeLeaderSkillsGameForAudit(game) ? initializeLeaderSkillsGameForAudit(game)
: scenario.profile === 'homeworld' : scenario.profile === 'homeworld'
@@ -390,19 +403,17 @@ function resumedGame(path: string) {
game.greatMaker || game.greatMaker ||
game.techTokens || game.techTokens ||
game.strongholdCards || game.strongholdCards ||
game.mentatQuestionPreview || game.mentatQuestionPreview
game.moritaniAssassinatePreview ||
game.moritaniAssassinate ||
game.moritaniAssassinateResume ||
game.moritaniAssassinateCallEvents
) )
throw new Error('--resume sample scenarios exclude unsupported optional modules.'); 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) =>
(candidate.profile === 'homeworld' || candidate.profile === 'homeworld-nexus' || candidate.profile === 'combined-homeworld-nexus') === !!game.homeworlds && (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 && (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 && skillProfile(candidate.profile) === !!game.leaderSkills &&
(candidate.profile === 'ecaz-treachery') === !!game.ecazTreachery && (candidate.profile === 'ecaz-treachery') === !!game.ecazTreachery &&
(candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinatePreview &&
(candidate.profile === 'moritani-assassinate') === !!game.moritaniAssassinate &&
candidate.rules === (game.advanced ? 'advanced' : 'basic') && candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
JSON.stringify(candidate.expansions) === JSON.stringify(candidate.expansions) ===
JSON.stringify(game.expansions) && JSON.stringify(game.expansions) &&
@@ -595,17 +606,20 @@ 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') && !['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.'); 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)) 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.`); throw new Error(`--profile ${profile} supports only five or six players.`);
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.');
if (profile === 'moritani-assassinate' && rules === 'basic')
throw new Error('Moritani assassination samples require Advanced rules.');
const resume = values.resume ? resumedGame(values.resume) : null; const resume = values.resume ? resumedGame(values.resume) : null;
const requestedSamples = profile === 'homeworld' || profile === 'nexus' || profile === 'homeworld-nexus' const requestedSamples = profile === 'homeworld' || profile === 'nexus' || profile === 'homeworld-nexus'
? MODULE_SCENARIOS ? MODULE_SCENARIOS
: profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS : profile === 'choam-roster' || profile === 'ecaz-roster' || profile === 'ix-roster' ? EXPANSION_ROSTER_SCENARIOS
: profile === 'ecaz-treachery' ? ECAZ_TREACHERY_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 === 'choam-nexus' || profile === 'ecaz-nexus' || profile === 'ix-nexus' ? PAIRED_NEXUS_SCENARIOS
: profile === 'combined-nexus' ? COMBINED_NEXUS_SCENARIOS : profile === 'combined-nexus' ? COMBINED_NEXUS_SCENARIOS
: profile === 'combined-homeworld-nexus' ? COMBINED_HOMEWORLD_NEXUS_SCENARIOS : profile === 'combined-homeworld-nexus' ? COMBINED_HOMEWORLD_NEXUS_SCENARIOS
+6 -2
View File
@@ -20,8 +20,9 @@ export function sampleInventory(game: Game) {
] ]
.map((card) => card.id) .map((card) => card.id)
.sort(), .sort(),
// Other expansion assassination zones require a separate adapter. // The bounded Moritani preview has a separate public retired-card zone.
traitors: game.expansions.length && !basicExpansionLeaderSkillsProfile(game) traitors: game.expansions.length && !basicExpansionLeaderSkillsProfile(game) &&
!game.moritaniAssassinatePreview
? null ? null
: traitorDeck( : traitorDeck(
game.players.map((player) => ({ leaders: leaders(player.faction) })), game.players.map((player) => ({ leaders: leaders(player.faction) })),
@@ -130,6 +131,9 @@ export function verifySampleCustody(
...game.players.flatMap((player) => [ ...game.players.flatMap((player) => [
...player.traitors, ...(player.faceDancers ?? []).map(card => card.leader), ...player.traitors, ...(player.faceDancers ?? []).map(card => card.leader),
]), ]),
...(game.moritaniAssassinate?.opportunities ?? [])
.filter(receipt => receipt.stage === 'replaced')
.map(receipt => receipt.card!),
].sort(), ].sort(),
inventory.traitors, inventory.traitors,
'physical traitor custody', 'physical traitor custody',