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
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
+29
View File
@@ -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
+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
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.
+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-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;
+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);
});
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');
+24
View File
@@ -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
View File
@@ -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
+6 -2
View File
@@ -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',