Exercise Advanced Moritani assassination preview games
This commit is contained in:
1 parent
c961a93f62
commit
ac8742c829
8 files changed
+152
-20
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
+27
-13
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user