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
|
## 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in new issue
Block a user