Connect original Duke capture and Ecaz Nexus liberation

This commit is contained in:
Quintine committed 2026-10-15 13:24:47 +11:00
1 parent 172363d9d3
commit a24600ae1d
24 files changed
+2082 -235

No files matched your search

+12
View File
@@ -9,6 +9,7 @@ import {
import type { MoritaniRetention } from './moritani-retention';
import { TECH_TOKENS, type TechId, type TechState } from './tech-tokens';
import { CHEAP_HERO_TRAITOR } from './traitors';
import { DUKE_VIDAL_ID, type DukeReleasedCaptureWitness } from './duke-vidal';
export class BattleAftermathQuoteError extends Error {
constructor(message: string) {
@@ -67,6 +68,7 @@ export type AftermathCapture = {
player: string;
loser: string;
territory: string;
duke?: DukeReleasedCaptureWitness;
};
export type AftermathAuditor = {
owner: string;
@@ -374,10 +376,20 @@ function calculate(input: BattleAftermathInput): BattleAftermathQuote {
'Only the winning Harkonnen combatant may capture from this loser.',
);
winner(captor.id);
let duke: DukeReleasedCaptureWitness | undefined;
if (value.duke !== undefined) {
requireAftermath(record(value.duke) &&
Object.keys(value.duke).sort().join(',') === 'deaths,usedAt' &&
whole(value.duke.deaths) && value.duke.usedAt === value.territory &&
context.kind === 'modern' && input.leaderIds.includes(DUKE_VIDAL_ID),
'The capture opportunity lost its original released shared Duke.');
duke = { deaths: value.duke.deaths, usedAt: value.duke.usedAt as string };
}
return {
player: captor.id,
loser: loser.id,
territory: territory(value.territory),
...(duke ? { duke } : {}),
};
}
function auditor(
+182 -13
View File
@@ -7,8 +7,22 @@ export type DukeState = {
controller: string | null;
acquiredTurn: number | null;
source: DukeSource | null;
/** Server-only original underlay retained across private capture and execution. */
captureVisibility?: {
controller: string | null;
acquiredTurn: number | null;
source: DukeSource | null;
};
};
/** A losing battle's living disc, recorded before its one-battle release. */
export type DukeReleasedCaptureWitness = { deaths: number; usedAt: string };
/** Server-only effective custody. Projected clients must use projected leader fields. */
export function dukeController(state: DukeState): string | null {
return state.leader.capturedBy ?? state.leader.gholaBy ?? state.controller;
}
/** One shared disc, deliberately outside every native roster and traitor inventory. */
export function createDukeVidal(): DukeState {
return {
@@ -32,6 +46,7 @@ function cloneDuke(state: DukeState): DukeState {
controller: state.controller,
acquiredTurn: state.acquiredTurn,
source: state.source,
...(state.captureVisibility ? { captureVisibility: { ...state.captureVisibility } } : {}),
};
}
function validTurn(turn: number) {
@@ -39,11 +54,150 @@ function validTurn(turn: number) {
throw new Error('Choose a valid turn for Duke Vidal custody.');
}
function validDisc(state: DukeState) {
const leader = state.leader;
if (leader.id !== DUKE_VIDAL_ID || leader.faction !== 'ecaz' ||
leader.strength !== 6 || typeof leader.dead !== 'boolean' ||
!Number.isSafeInteger(leader.deaths) || leader.deaths < 0 ||
(leader.usedAt !== undefined && (typeof leader.usedAt !== 'string' || !leader.usedAt)))
throw new Error('Duke Vidal lost his shared physical identity or death history.');
}
function validUnderlay(state: Pick<DukeState, 'controller' | 'acquiredTurn' | 'source'>, turn: number) {
if (state.controller === null
? state.acquiredTurn !== null || state.source !== null
: typeof state.controller !== 'string' || !state.controller.trim() ||
!Number.isSafeInteger(state.acquiredTurn) || state.acquiredTurn! < 1 ||
state.acquiredTurn! > turn || !['moritani', 'ecaz', 'ecazNexus', 'ally'].includes(state.source!))
throw new Error('Duke Vidal lost his underlying custody.');
}
function validCaptive(state: DukeState, turn: number) {
validDisc(state);
validUnderlay(state, turn);
const { leader } = state;
const hidden = leader.concealed;
const visibility = state.captureVisibility;
if (!visibility || visibility.controller !== state.controller ||
visibility.acquiredTurn !== state.acquiredTurn || visibility.source !== state.source)
throw new Error('Duke Vidal lost his original captured visibility.');
if (!leader.capturedBy || typeof leader.capturedBy !== 'string' ||
!leader.capturedBy.trim() || leader.capturedBy === state.controller || leader.gholaBy ||
(hidden && (hidden.captor !== leader.capturedBy ||
typeof hidden.controller !== 'string' || !hidden.controller.trim() ||
hidden.controller === hidden.captor || hidden.dead !== false ||
!Number.isSafeInteger(hidden.deaths) || hidden.deaths < 0 ||
hidden.deaths !== leader.deaths - (leader.dead ? 1 : 0) ||
(hidden.usedAt !== undefined && (typeof hidden.usedAt !== 'string' || !hidden.usedAt)) ||
(state.controller !== null && hidden.controller !== state.controller))))
throw new Error('Duke Vidal lost his original captured custody.');
}
/** A secretly executed captive is back in the Tanks, not still controlled by its captor.
* Keep this exact snapshot until a real public return; it is not foreign-Ghola custody. */
export function returnedDukeCapture(state: DukeState): boolean {
const { leader } = state;
const hidden = leader.concealed;
const visibility = state.captureVisibility;
if (!visibility) return false;
try {
validUnderlay(visibility, Number.MAX_SAFE_INTEGER);
} catch {
return false;
}
return leader.id === DUKE_VIDAL_ID && leader.faction === 'ecaz' &&
leader.strength === 6 && leader.dead === true &&
Number.isSafeInteger(leader.deaths) && leader.deaths > 0 &&
leader.usedAt === undefined && leader.capturedBy === undefined &&
leader.gholaBy === undefined && state.controller === null &&
state.acquiredTurn === null && state.source === null &&
!!hidden && typeof hidden.captor === 'string' && !!hidden.captor.trim() &&
typeof hidden.controller === 'string' && !!hidden.controller.trim() &&
hidden.controller !== hidden.captor && hidden.dead === false &&
Number.isSafeInteger(hidden.deaths) && hidden.deaths >= 0 &&
Math.max(2, hidden.deaths + 1) === leader.deaths &&
(visibility.controller === null || visibility.controller === hidden.controller) &&
(hidden.usedAt === undefined || (typeof hidden.usedAt === 'string' && !!hidden.usedAt));
}
/** Provisional capture policy: retain an unused disc's underlying tenure.
* A just-released battle disc instead requires the caller's original battle witness. */
export function captureDuke(
state: DukeState, captor: string, loser: string, turn: number,
territory: string, releasedWitness?: DukeReleasedCaptureWitness,
): DukeState {
validTurn(turn);
validDisc(state);
validUnderlay(state, turn);
if (typeof captor !== 'string' || !captor.trim() ||
typeof loser !== 'string' || !loser.trim() || captor === loser ||
typeof territory !== 'string' || !territory.trim() ||
state.leader.dead || state.leader.capturedBy || state.leader.gholaBy ||
state.leader.concealed || state.captureVisibility)
throw new Error('Choose the original living losing Duke for capture.');
if (releasedWitness
? state.controller !== null || state.acquiredTurn !== null || state.source !== null ||
!Number.isSafeInteger(releasedWitness.deaths) ||
releasedWitness.deaths !== state.leader.deaths ||
releasedWitness.usedAt !== territory || state.leader.usedAt !== territory
: dukeController(state) !== loser || !!state.leader.usedAt ||
(state.source !== 'ecaz' && state.acquiredTurn !== turn))
throw new Error('Duke Vidal no longer matches the original losing battle custody.');
const captured = cloneDuke(state);
captured.captureVisibility = {
controller: state.controller, acquiredTurn: state.acquiredTurn, source: state.source,
};
captured.leader.capturedBy = captor;
captured.leader.concealed = {
captor, controller: loser, dead: false, deaths: state.leader.deaths,
...(state.leader.usedAt === undefined ? {} : { usedAt: state.leader.usedAt }),
};
return captured;
}
/** Early unused release restores valid prior tenure, never native-roster custody.
* Death, battle use and expired temporary tenure all return the same disc set aside. */
export function releaseCapturedDuke(
state: DukeState, captor: string, turn: number,
): DukeState {
validTurn(turn);
validCaptive(state, turn);
if (captor !== state.leader.capturedBy)
throw new Error('Only the actual captor can release Duke Vidal.');
const released = cloneDuke(state);
delete released.leader.capturedBy;
delete released.leader.concealed;
delete released.captureVisibility;
if (released.leader.dead || released.leader.usedAt ||
(released.source !== 'ecaz' && released.acquiredTurn !== turn))
return consumeDuke(released);
return released;
}
/** Original Harkonnen execution returns the actual dead disc to Ecaz's Tanks.
* The caller pays the original two-spice bounty; unrelated seats retain their snapshot. */
export function executeCapturedDuke(
state: DukeState, captor: string, turn: number,
): DukeState {
validTurn(turn);
validCaptive(state, turn);
if (state.leader.dead || captor !== state.leader.capturedBy || !state.leader.concealed)
throw new Error('Only the actual captor can execute the original living Duke.');
const executed = cloneDuke(state);
executed.leader.dead = true;
executed.leader.deaths = Math.max(2, executed.leader.deaths + 1);
delete executed.leader.usedAt;
delete executed.leader.capturedBy;
executed.controller = null;
executed.acquiredTurn = null;
executed.source = null;
return executed;
}
/**
* The caller authorizes the faction power and resolves any applicable Karama.
* Ecaz Nexus Cunning independently revives the set-aside shared disc from Tanks.
* Captured/ghola acquisition remains an unresolved implementation boundary,
* not a verified blanket restriction on the printed powers.
* Ecaz Nexus Cunning liberates an actual captive; no foreign-Ghola source is legal.
*/
export function acquireDuke(
state: DukeState,
@@ -52,22 +206,35 @@ export function acquireDuke(
source: DukeSource,
): DukeState {
validTurn(turn);
validDisc(state);
validUnderlay(state, turn);
if (typeof controller !== 'string' || !controller.trim())
throw new Error('Choose a player to control Duke Vidal.');
if (!['moritani', 'ecaz', 'ecazNexus', 'ally'].includes(source))
throw new Error('Choose a valid source of Duke Vidal custody.');
if (state.leader.dead && source !== 'ecazNexus')
throw new Error('Duke Vidal is in the Tanks and cannot be acquired by this source.');
if (state.leader.capturedBy || state.leader.gholaBy)
throw new Error(
'Acquiring Duke Vidal from captured or ghola custody is still being implemented.',
);
if (state.leader.gholaBy)
throw new Error('Duke Vidal cannot be acquired from foreign Ghola custody.');
if (state.leader.capturedBy) {
if (source !== 'ecazNexus' || state.leader.dead)
throw new Error('Only Ecaz Nexus Cunning can acquire the living captured Duke.');
validCaptive(state, turn);
} else if ((state.leader.concealed || state.captureVisibility) &&
!(source === 'ecazNexus' && returnedDukeCapture(state))) {
throw new Error('Duke Vidal has unresolved concealed custody.');
}
if (state.leader.dead &&
(state.controller !== null || state.acquiredTurn !== null || state.source !== null))
throw new Error('Duke Vidal must be set aside in the Tanks before Ecaz Nexus acquisition.');
if (source === 'ecazNexus' && !state.leader.dead && state.leader.usedAt)
throw new Error('Duke Vidal has already been used in battle this turn.');
const acquired = cloneDuke(state);
if (source === 'ecazNexus') {
delete acquired.leader.capturedBy;
delete acquired.leader.concealed;
delete acquired.captureVisibility;
}
if (state.leader.dead) {
acquired.leader.dead = false;
// An independent resurrection permits a new battle use without erasing deaths.
@@ -81,12 +248,14 @@ export function acquireDuke(
/** The battle caller resolves leader death and bounty before releasing temporary control. */
export function consumeDuke(state: DukeState): DukeState {
return {
...cloneDuke(state),
controller: null,
acquiredTurn: null,
source: null,
};
const released = cloneDuke(state);
delete released.leader.capturedBy;
delete released.leader.concealed;
delete released.captureVisibility;
released.controller = null;
released.acquiredTurn = null;
released.source = null;
return released;
}
/**
@@ -127,7 +296,7 @@ export const DUKE_VIDAL_RULES = Object.freeze({
/** Developer metadata: the guide describes rules beyond this module's custody helpers. */
implementation: Object.freeze({
custody: 'implemented',
capture: 'not-implemented',
capture: 'original-harkonnen-provisional-return',
revival: 'ecaz-only-set-aside',
ambassadorAcquisition: 'ecaz-self-implemented',
ambassadorAlliance: 'consent-implemented',
+5 -2
View File
@@ -1,8 +1,10 @@
import type { Game } from './engine';
import type { FactionId } from './catalog';
import { acquireDuke, DUKE_VIDAL_ID, type DukeState } from './duke-vidal';
import type { FactionModuleProfile } from './faction-module-profile';
import { standaloneE3NexusProfile } from './nexus-module-profile';
export type EcazDukeContext = {
export type EcazDukeContext = Partial<FactionModuleProfile> & {
status: Game['status'];
turn: number;
advanced: boolean;
@@ -54,7 +56,8 @@ export function quoteEcazDukeAcquisition(
'context',
);
requireDuke(
!(g.advanced && g.players.some((p) => p.faction === 'harkonnen')),
!(g.advanced && g.players.some((p) => p.faction === 'harkonnen')) ||
(!!g.expansions && standaloneE3NexusProfile(g as FactionModuleProfile)),
tableBlock,
'table',
);
+10 -4
View File
@@ -1,9 +1,11 @@
import type { Game, Player } from './engine';
import type { Leader } from './cards';
import { DUKE_VIDAL_ID, type DukeState } from './duke-vidal';
import { DUKE_VIDAL_ID, returnedDukeCapture, type DukeState } from './duke-vidal';
import type { FactionModuleProfile } from './faction-module-profile';
import { standaloneE3NexusProfile } from './nexus-module-profile';
export const DUKE_REVIVAL_NORMAL_COST = 5 as const;
export type EcazDukeRevivalContext = {
export type EcazDukeRevivalContext = Partial<FactionModuleProfile> & {
status: Game['status'];
turn: number;
advanced: boolean;
@@ -82,7 +84,8 @@ export function resolveEcazDukeRevival(
'owner',
);
requireDuke(
!(g.advanced && g.players.some((p) => p.faction === 'harkonnen')),
!(g.advanced && g.players.some((p) => p.faction === 'harkonnen')) ||
(!!g.expansions && standaloneE3NexusProfile(g as FactionModuleProfile)),
'table',
);
const duke = g.dukeVidal;
@@ -108,7 +111,10 @@ export function resolveEcazDukeRevival(
(typeof leader.usedAt === 'string' && leader.usedAt.length > 0)) &&
leader.capturedBy === undefined &&
leader.gholaBy === undefined &&
leader.concealed === undefined,
(leader.concealed === undefined ||
(returnedDukeCapture(duke) &&
g.players.some(player => player.id === leader.concealed?.captor && player.faction === 'harkonnen') &&
g.players.some(player => player.id === leader.concealed?.controller))),
);
requireDuke(
g.players.every(
+98 -53
View File
@@ -460,6 +460,11 @@ import {
acquireDuke,
consumeDuke,
expireDuke,
dukeController,
captureDuke,
releaseCapturedDuke,
executeCapturedDuke,
type DukeReleasedCaptureWitness,
DUKE_VIDAL_ID,
type DukeState,
} from './duke-vidal';
@@ -2186,7 +2191,7 @@ export type Game = {
} | null;
hajr: string[];
decision: Decision | null;
pendingCapture: { player: string; loser: string; territory: string } | null;
pendingCapture: { player: string; loser: string; territory: string; duke?: DukeReleasedCaptureWitness } | null;
moritaniRetention?: MoritaniRetention | null;
response: ResponseWindow | null;
wormRides: string[];
@@ -17452,7 +17457,7 @@ function ecazAllianceQuote(g: Game, owner: string, entrant: string) {
}
}
function ecazAmbassadorLoanBlock(g: Game, owner: Player, entrant: Player) {
if (g.advanced && byFaction(g, 'harkonnen'))
if (g.advanced && byFaction(g, 'harkonnen') && !standaloneE3NexusProfile(g))
return 'Advanced Duke loans with Harkonnen capture remain guarded.';
if (owner.ally !== entrant.id || entrant.ally !== owner.id)
return 'The new Ecaz alliance is no longer current.';
@@ -18494,11 +18499,7 @@ function offerMoritaniDuke(g: Game) {
g.dukeVidal ??= createDukeVidal();
const duke = g.dukeVidal;
if (duke.leader.dead || duke.controller === owner.id) return false;
if (
duke.leader.capturedBy ||
duke.leader.gholaBy ||
(g.advanced && byFaction(g, 'harkonnen'))
) {
if (duke.leader.capturedBy || duke.leader.gholaBy) {
log(
g,
'Duke Vidal acquisition with exceptional capture or ghola custody is still being implemented.',
@@ -20583,13 +20584,7 @@ function controlledLeaders(g: Game, p: Player) {
owner.leaders.filter((l) => controlsLeader(p, l)),
);
const duke = g.dukeVidal;
if (
duke &&
duke.controller === p.id &&
!duke.leader.capturedBy &&
!duke.leader.gholaBy &&
!(g.advanced && byFaction(g, 'harkonnen'))
)
if (duke && !duke.leader.gholaBy && dukeController(duke) === p.id)
native.push(duke.leader);
return native;
}
@@ -20603,25 +20598,47 @@ function returnCaptives(g: Game, captor: Player) {
delete l.concealed;
returned++;
}
if (g.dukeVidal?.leader.capturedBy === captor.id) {
g.dukeVidal = releaseCapturedDuke(g.dukeVidal, captor.id, g.turn);
returned++;
}
if (returned)
log(
g,
`${captor.name} returned ${returned} captive leaders because all native leaders are dead.`,
);
}
function captureCandidates(g: Game, loser: string, t: string) {
return controlledLeaders(g, getPlayer(g, loser)).filter(
(l) => !isAuditorLeader(l) && !l.dead && (!l.usedAt || l.usedAt === t),
);
/** Original random pool; the optional witness preserves a just-used living
* losing Duke after his separate one-battle release, never a second disc. */
export function captureCandidates(g: Game, loser: string, t: string,
released?: DukeReleasedCaptureWitness): Leader[] {
const candidates = controlledLeaders(g, getPlayer(g, loser)).filter(
(leader) => !isAuditorLeader(leader) && !leader.dead && (!leader.usedAt || leader.usedAt === t));
if (released) {
const duke = g.dukeVidal;
requireRule(duke && Object.keys(released).sort().join(',') === 'deaths,usedAt' &&
Number.isSafeInteger(released.deaths) && released.deaths >= 0 &&
released.usedAt === t && duke.leader.id === DUKE_VIDAL_ID && !duke.leader.dead &&
!duke.leader.capturedBy && !duke.leader.gholaBy &&
duke.leader.deaths === released.deaths && duke.leader.usedAt === released.usedAt &&
duke.controller === null && duke.acquiredTurn === null && duke.source === null,
'The original released Duke capture source changed before selection.');
requireRule(!candidates.some(leader => leader.id === DUKE_VIDAL_ID),
'The original shared Duke cannot occupy two capture slots.');
candidates.push(duke.leader);
}
return candidates;
}
function leaderHiddenFrom(g: Game, leader: Leader, viewer: string): boolean {
const concealed = leader.concealed;
if (!concealed) return false;
const owner = g.players.find(player => player.faction === leader.faction);
return ![concealed.controller ?? owner?.id, concealed.captor].includes(viewer) &&
!(getPlayer(g, viewer).faction === 'tleilaxu' && leader.dead && !leader.capturedBy);
}
function projectLeader(g: Game, l: Leader, viewer: string): Leader {
const { concealed, capturedBy, ...visible } = l;
const owner = g.players.find((p) => p.faction === l.faction);
if (
concealed &&
![concealed.controller ?? owner?.id, concealed.captor].includes(viewer) &&
!(getPlayer(g, viewer).faction === 'tleilaxu' && l.dead && !l.capturedBy)
) {
if (concealed && leaderHiddenFrom(g, l, viewer)) {
const result = {
...visible,
dead: concealed.dead,
@@ -20633,6 +20650,16 @@ function projectLeader(g: Game, l: Leader, viewer: string): Leader {
}
return { ...visible, ...(capturedBy ? { capturedBy } : {}) };
}
function projectedSharedDuke(g: Game, viewer: string):
Pick<DukeState, 'leader' | 'controller' | 'acquiredTurn' | 'source'> | null {
const duke = g.dukeVidal;
if (!duke) return null;
const leader = projectLeader(g, duke.leader, viewer);
const visibility = leaderHiddenFrom(g, duke.leader, viewer) ? duke.captureVisibility : undefined;
const controller = visibility ? visibility.controller : leader.capturedBy ?? leader.gholaBy ?? duke.controller;
return { controller, acquiredTurn: visibility ? visibility.acquiredTurn : duke.acquiredTurn,
source: visibility ? visibility.source : duke.source, leader: { ...leader, controller } };
}
/** Public alternatives deliberately never read a foreign No-Field token value. */
function stonePublicPools(
g: Game,
@@ -23001,8 +23028,12 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
] as [Player, Plan, Leader | undefined][]) {
if (l && !l.dead) l.usedAt = b.territory;
if (l?.capturedBy === p.id) {
delete l.capturedBy;
delete l.concealed;
if (l.id === DUKE_VIDAL_ID && g.dukeVidal)
g.dukeVidal = releaseCapturedDuke(g.dukeVidal, p.id, g.turn);
else {
delete l.capturedBy;
delete l.concealed;
}
}
if (plan.kwisatz) {
p.kwisatz ??= { dead: false };
@@ -23021,12 +23052,18 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
if (harkonnen) returnCaptives(g, harkonnen);
if (g.advanced && winner?.faction === 'harkonnen') {
const loser = winner.id === a.id ? d : a;
if (captureCandidates(g, loser.id, b.territory).length)
const candidates = captureCandidates(g, loser.id, b.territory);
if (candidates.length) {
const losingPlan = winner.id === a.id ? dp : ap;
g.pendingCapture = {
player: winner.id,
loser: loser.id,
territory: b.territory,
...(losingPlan.leader === DUKE_VIDAL_ID &&
candidates.some(leader => leader.id === DUKE_VIDAL_ID)
? { duke: { deaths: g.dukeVidal!.leader.deaths, usedAt: b.territory } } : {}),
};
}
}
if (winner) {
const loser = winner.id === a.id ? d : a;
@@ -25631,21 +25668,27 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
g.pendingCapture = null;
if (canceled) finishBattle(g);
else {
const candidates = captureCandidates(g, capture.loser, capture.territory);
const l = shuffle(candidates)[0];
const candidates = captureCandidates(g, capture.loser, capture.territory, capture.duke);
const selected = shuffle(candidates)[0];
const l = selected?.id === DUKE_VIDAL_ID
? (g.dukeVidal = captureDuke(g.dukeVidal!, capture.player, capture.loser,
g.turn, capture.territory, capture.duke)).leader : selected;
if (!l) {
finishBattle(g);
return;
}
const owner = g.players.find((p) => p.leaders.includes(l))!;
l.concealed = {
captor: capture.player,
controller: capture.loser,
dead: l.dead,
deaths: l.deaths,
usedAt: l.usedAt,
};
l.capturedBy = capture.player;
const owner = l.id === DUKE_VIDAL_ID ? getPlayer(g, capture.loser) :
g.players.find((player) => player.leaders.includes(l))!;
if (l.id !== DUKE_VIDAL_ID) {
l.concealed = {
captor: capture.player,
controller: capture.loser,
dead: l.dead,
deaths: l.deaths,
usedAt: l.usedAt,
};
l.capturedBy = capture.player;
}
const capturedSkill = g.leaderSkills?.assignments.find((a) => a.leader === l.id);
// This unique card and its leader were publicly assigned. The expansion
// requires the card to accompany the captive, identifying that capture.
@@ -30840,13 +30883,19 @@ function applyActionInner(
action.mode === 'keep' || action.mode === 'execute',
'Choose keep or execute for the captured leader.',
);
const owner = getPlayer(g, decision.owner),
l = owner.leaders.find((l) => l.id === decision.leader)!;
const owner = getPlayer(g, decision.owner);
const l = decision.leader === DUKE_VIDAL_ID ? g.dukeVidal?.leader :
owner.leaders.find((leader) => leader.id === decision.leader);
requireRule(l?.capturedBy === p.id, 'This captive is no longer controlled by its original captor.');
if (action.mode === 'execute') {
l.dead = true;
l.deaths = Math.max(2, l.deaths + 1);
delete l.usedAt;
delete l.capturedBy;
if (l.id === DUKE_VIDAL_ID && g.dukeVidal)
g.dukeVidal = executeCapturedDuke(g.dukeVidal, p.id, g.turn);
else {
l.dead = true;
l.deaths = Math.max(2, l.deaths + 1);
delete l.usedAt;
delete l.capturedBy;
}
p.spice += 2;
log(g, `${p.name} executed a captive for 2 spice.`);
} else {
@@ -33365,6 +33414,7 @@ export function viewGame(state: Game, id: string) {
settleAdvisors(g);
normalizeBattle(g);
const me = getPlayer(g, id);
const sharedDuke = projectedSharedDuke(g, id);
const choamPlayer = byFaction(g, 'choam');
let victoryProgress: StrongholdProgress[] = [];
let fremenVictory: FremenVictoryProgress | null = null;
@@ -33480,9 +33530,7 @@ export function viewGame(state: Game, id: string) {
rihani: projectedRihani(g, id),
advanced: g.advanced,
advancedPreview: g.advancedPreview === true,
dukeVidal: g.dukeVidal
? { ...g.dukeVidal, leader: projectLeader(g, g.dukeVidal.leader, id) }
: null,
dukeVidal: sharedDuke,
moritaniTerror: g.moritaniTerror
? projectTerror(g.moritaniTerror, me.faction === 'moritani')
: null,
@@ -34189,12 +34237,9 @@ export function viewGame(state: Game, id: string) {
.filter((c) => c.revealed)
.map((c) => c.leader),
leaders: [
...(g.dukeVidal?.controller === p.id &&
!g.dukeVidal.leader.capturedBy &&
!g.dukeVidal.leader.gholaBy &&
!(g.advanced && byFaction(g, 'harkonnen'))
? [{ ...g.dukeVidal.leader, controller: p.id }]
: []),
...(sharedDuke?.controller === p.id &&
(!sharedDuke.leader.capturedBy || p.id === id) && !sharedDuke.leader.gholaBy
? [{ ...sharedDuke.leader, controller: p.id }] : []),
...p.leaders.map((l) => projectLeader(g, l, id)),
...g.players
.filter((owner) => owner.id !== p.id)
+160 -18
View File
@@ -1,6 +1,6 @@
import type { FactionId } from './catalog';
import type { Game } from './engine';
import { acquireDuke, DUKE_VIDAL_ID, type DukeSource } from './duke-vidal';
import { acquireDuke, dukeController, DUKE_VIDAL_ID, type DukeSource } from './duke-vidal';
import { nexusCardMode } from './nexus-cards';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { mixedE3NexusLeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile } from './leader-skill-profile';
@@ -19,7 +19,7 @@ type DukeBefore = {
};
/** A completed physical card play, not a second copy of the Duke's current custody. */
export type NexusEcazDukeReceipt = {
type NexusEcazDukeLivingReceipt = {
event: string;
owner: string;
ownerFaction: 'ecaz';
@@ -35,6 +35,31 @@ export type NexusEcazDukeReceipt = {
signature: string;
};
/** Version two exists only for a real captive, retaining its private original underlay. */
export type NexusEcazDukeCapturedReceipt = Omit<NexusEcazDukeLivingReceipt, 'before' | 'after'> & {
version: 2;
before: Omit<DukeBefore, 'leader'> & {
leader: {
id: typeof DUKE_VIDAL_ID;
dead: false;
deaths: number;
usedAt: null;
capturedBy: string;
gholaBy: null;
concealed: {
captor: string; controller: string; dead: false; deaths: number; usedAt: string | null;
} | null;
};
};
after: NexusEcazDukeLivingReceipt['after'] & {
leader: {
id: typeof DUKE_VIDAL_ID; dead: false; deaths: number; usedAt: null;
capturedBy: null; gholaBy: null; concealed: null;
};
};
};
export type NexusEcazDukeReceipt = NexusEcazDukeLivingReceipt | NexusEcazDukeCapturedReceipt;
export function nexusEcazDukeEvent(
turn: number, phase: number, owner: string, sequence: number,
beforeController: string | null, beforeAcquiredTurn: number | null,
@@ -44,6 +69,20 @@ export function nexusEcazDukeEvent(
}
export function nexusEcazDukeSignature(receipt: NexusEcazDukeReceipt): string {
if ('version' in receipt) {
const leader = receipt.before.leader;
const hidden = leader.concealed;
const after = receipt.after;
return JSON.stringify(['nexusEcazDukeCapturedReceipt', receipt.version,
receipt.event, receipt.owner, receipt.ownerFaction, receipt.card, receipt.mode,
receipt.roster, receipt.advanced, receipt.turn, receipt.phase, receipt.sequence,
receipt.before.controller, receipt.before.acquiredTurn, receipt.before.source,
leader.id, leader.dead, leader.deaths, leader.usedAt, leader.capturedBy, leader.gholaBy,
hidden && [hidden.captor, hidden.controller, hidden.dead, hidden.deaths, hidden.usedAt],
after.controller, after.acquiredTurn, after.source,
after.leader.id, after.leader.dead, after.leader.deaths, after.leader.usedAt,
after.leader.capturedBy, after.leader.gholaBy, after.leader.concealed]);
}
return JSON.stringify(['nexusEcazDukeReceipt', receipt.event, receipt.owner,
receipt.ownerFaction, receipt.card, receipt.mode, receipt.roster,
receipt.advanced, receipt.turn, receipt.phase, receipt.sequence,
@@ -61,10 +100,14 @@ export function quoteNexusEcazDuke(g: Game, owner: string, automaticPending = fa
if (!holder || holder.faction !== 'ecaz' ||
g.nexusCards?.cards?.hands[owner] !== 'ecaz') return null;
const duke = g.dukeVidal;
const hidden = duke?.leader.concealed;
const masked = !!hidden && owner !== hidden.captor && owner !== hidden.controller;
const eventCustody = masked && duke?.captureVisibility ? duke.captureVisibility : duke;
const roster = g.players.map(player => player.faction);
let blocked: string | null = null;
if (g.advanced && g.players.some(player => player.faction === 'harkonnen'))
blocked = 'Duke Vidal battle use is unavailable with Advanced Harkonnen.';
if (g.advanced && g.players.some(player => player.faction === 'harkonnen') &&
!standaloneE3NexusProfile(g))
blocked = 'Duke Vidal battle use is unavailable with Advanced Harkonnen in this profile.';
else if (!nexusEcazDukeModeSupported(g) || nexusCardMode('ecaz', holder.faction, roster) !== 'cunning')
blocked = 'Ecaz Nexus Cunning requires a supported original paired or standalone Ecaz Nexus profile.';
else if (holder.ally) blocked = 'Use Ecaz Nexus Cunning while unallied.';
@@ -72,28 +115,38 @@ export function quoteNexusEcazDuke(g: Game, owner: string, automaticPending = fa
blocked = 'Use Ecaz Nexus Cunning at the Battle phase boundary.';
else if (nexusCleanPlayBlocked(g, automaticPending))
blocked = 'Finish the current interaction before using Ecaz Nexus Cunning.';
else if (!duke || duke.leader.id !== DUKE_VIDAL_ID ||
duke.leader.capturedBy || duke.leader.gholaBy)
blocked = 'Only an uncaptured, non-Ghola Duke Vidal can be acquired.';
else if (duke.leader.dead &&
(duke.controller !== null || duke.acquiredTurn !== null || duke.source !== null))
blocked = 'Duke Vidal must be set aside in the Tanks before Ecaz Nexus acquisition.';
else if (!duke.leader.dead && duke.leader.usedAt)
blocked = 'Duke Vidal has already been used in battle this turn.';
else if (duke.controller === owner)
blocked = 'Ecaz already controls Duke Vidal.';
else if (!duke || duke.leader.id !== DUKE_VIDAL_ID || duke.leader.gholaBy)
blocked = 'Only the shared non-Ghola Duke Vidal can be acquired.';
else if (duke.leader.capturedBy && (!g.advanced ||
!g.players.some(player => player.id === duke.leader.capturedBy && player.faction === 'harkonnen')))
blocked = 'Duke Vidal lacks an original Harkonnen capture source.';
else {
try {
acquireDuke(duke, owner, g.turn, 'ecazNexus');
if (dukeController(duke) === owner) blocked = 'Ecaz already controls Duke Vidal.';
} catch {
blocked = duke.leader.usedAt && !duke.leader.dead
? 'Duke Vidal has already been used in battle this turn.'
: 'Duke Vidal is unavailable for Ecaz Nexus acquisition.';
}
}
return {
event: nexusEcazDukeEvent(g.turn, 6, owner, g.nexusEcazDukeHistory?.length ?? 0,
duke?.controller ?? null, duke?.acquiredTurn ?? null),
eventCustody?.controller ?? null, eventCustody?.acquiredTurn ?? null),
blocked,
dukeController: duke?.controller ?? null,
dukeController: duke
? masked
? hidden!.controller ?? duke.controller
: dukeController(duke)
: null,
};
}
export function nexusEcazDukeModeSupported(g: Game): boolean {
if ((mixedE3NexusProfile(g) || standaloneE3NexusProfile(g) || pairedE3NexusProfile(g)) &&
g.players.some(player => player.faction === 'ecaz') &&
!g.sandtrout && !(g.advanced && g.players.some(player => player.faction === 'harkonnen')))
!g.sandtrout && (!(g.advanced && g.players.some(player => player.faction === 'harkonnen')) ||
standaloneE3NexusProfile(g)))
return !!g.nexusCards?.cards && g.players.length >= 3;
if (g.leaderSkills && (mixedE3NexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3NexusLeaderSkillsProfile(g)) &&
g.players.some(player => player.faction === 'ecaz') && !g.sandtrout)
@@ -118,9 +171,41 @@ export function createNexusEcazDukeReceipt(
if (!offer || offer.blocked || sequence !== (g.nexusEcazDukeHistory?.length ?? 0))
throw new Error(offer?.blocked ?? 'Ecaz Nexus Cunning is unavailable.');
const duke = g.dukeVidal!;
// History records actual server custody, not an unrelated owner's masked action token.
const event = nexusEcazDukeEvent(g.turn, 6, owner, sequence, duke.controller, duke.acquiredTurn);
const after = acquireDuke(duke, owner, g.turn, 'ecazNexus');
if (duke.leader.capturedBy) {
const hidden = duke.leader.concealed;
const receipt: NexusEcazDukeCapturedReceipt = {
version: 2,
event, owner, ownerFaction: 'ecaz', card: 'ecaz', mode: 'cunning',
roster: g.players.map(player => player.faction), advanced: g.advanced,
turn: g.turn, phase: 6, sequence,
before: {
controller: duke.controller, acquiredTurn: duke.acquiredTurn, source: duke.source,
leader: {
id: DUKE_VIDAL_ID, dead: false, deaths: duke.leader.deaths, usedAt: null,
capturedBy: duke.leader.capturedBy, gholaBy: null,
concealed: hidden ? {
captor: hidden.captor, controller: hidden.controller!, dead: false,
deaths: hidden.deaths, usedAt: hidden.usedAt ?? null,
} : null,
},
},
after: {
controller: after.controller!, acquiredTurn: after.acquiredTurn!, source: 'ecazNexus',
leader: {
id: DUKE_VIDAL_ID, dead: false, deaths: after.leader.deaths, usedAt: null,
capturedBy: null, gholaBy: null, concealed: null,
},
},
signature: '',
};
receipt.signature = nexusEcazDukeSignature(receipt);
return receipt;
}
const receipt: NexusEcazDukeReceipt = {
event: offer.event, owner, ownerFaction: 'ecaz', card: 'ecaz', mode: 'cunning',
event, owner, ownerFaction: 'ecaz', card: 'ecaz', mode: 'cunning',
roster: g.players.map(player => player.faction), advanced: g.advanced,
turn: g.turn, phase: 6, sequence,
before: {
@@ -146,6 +231,10 @@ const turn = (value: unknown): value is number =>
export function validateNexusEcazDuke(
g: Game, receipt: NexusEcazDukeReceipt, index: number,
): void {
if (object(receipt) && 'version' in receipt) {
validateCapturedReceipt(g, receipt as NexusEcazDukeCapturedReceipt, index);
return;
}
const before = receipt?.before;
const after = receipt?.after;
const leader = before?.leader;
@@ -188,3 +277,56 @@ export function validateNexusEcazDuke(
receipt.signature !== nexusEcazDukeSignature(receipt))
throw new Error('Ecaz Nexus Cunning lost its original Duke custody or physical play.');
}
function validateCapturedReceipt(g: Game, receipt: NexusEcazDukeCapturedReceipt, index: number): void {
const before = receipt.before;
const leader = before?.leader;
const hidden = leader?.concealed;
const after = receipt.after;
const returned = after?.leader;
const controller = g.players.find(player => player.id === before?.controller);
const captor = g.players.find(player => player.id === leader?.capturedBy);
if (!keys(receipt, 'advanced,after,before,card,event,mode,owner,ownerFaction,phase,roster,sequence,signature,turn,version') ||
receipt.version !== 2 ||
!object(before) || !keys(before, 'acquiredTurn,controller,leader,source') ||
!object(leader) || !keys(leader, 'capturedBy,concealed,dead,deaths,gholaBy,id,usedAt') ||
!object(after) || !keys(after, 'acquiredTurn,controller,leader,source') ||
!object(returned) || !keys(returned, 'capturedBy,concealed,dead,deaths,gholaBy,id,usedAt') ||
!nexusEcazDukeModeSupported(g) ||
!g.players.some(player => player.id === receipt.owner && player.faction === 'ecaz') ||
receipt.ownerFaction !== 'ecaz' || receipt.card !== 'ecaz' || receipt.mode !== 'cunning' ||
!Array.isArray(receipt.roster) || receipt.roster.length !== g.players.length ||
receipt.roster.some((faction, position) => faction !== g.players[position]?.faction) ||
nexusCardMode('ecaz', receipt.ownerFaction, receipt.roster) !== 'cunning' ||
typeof receipt.advanced !== 'boolean' || receipt.advanced !== g.advanced ||
!turn(receipt.turn) || receipt.turn > g.turn ||
receipt.phase !== 6 || (receipt.turn === g.turn && g.phase < 6) ||
!Number.isSafeInteger(index) || index < 0 || receipt.sequence !== index ||
leader.id !== DUKE_VIDAL_ID || leader.dead !== false ||
!Number.isSafeInteger(leader.deaths) || leader.deaths < 0 ||
leader.usedAt !== null || leader.gholaBy !== null ||
!captor || captor.faction !== 'harkonnen' || !receipt.advanced ||
leader.capturedBy === receipt.owner || leader.capturedBy === before.controller ||
(before.controller === null
? before.acquiredTurn !== null || before.source !== null
: !controller || !turn(before.acquiredTurn) || before.acquiredTurn > receipt.turn ||
!['moritani', 'ecaz', 'ecazNexus', 'ally'].includes(before.source as string) ||
(before.source === 'moritani' && controller.faction !== 'moritani') ||
((before.source === 'ecaz' || before.source === 'ecazNexus') && controller.faction !== 'ecaz')) ||
!(hidden === null || (object(hidden) &&
keys(hidden, 'captor,controller,dead,deaths,usedAt') &&
hidden.captor === leader.capturedBy && hidden.dead === false &&
hidden.deaths === leader.deaths &&
(hidden.usedAt === null || (typeof hidden.usedAt === 'string' && hidden.usedAt.length > 0)) &&
hidden.controller !== hidden.captor &&
g.players.some(player => player.id === hidden.controller) &&
(before.controller === null || hidden.controller === before.controller))) ||
returned.id !== DUKE_VIDAL_ID || returned.dead !== false ||
returned.deaths !== leader.deaths || returned.usedAt !== null ||
returned.capturedBy !== null || returned.gholaBy !== null || returned.concealed !== null ||
after.controller !== receipt.owner || after.acquiredTurn !== receipt.turn || after.source !== 'ecazNexus' ||
receipt.event !== nexusEcazDukeEvent(receipt.turn, receipt.phase, receipt.owner, receipt.sequence,
before.controller, before.acquiredTurn) ||
receipt.signature !== nexusEcazDukeSignature(receipt))
throw new Error('Ecaz Nexus Cunning lost its original captured Duke custody or physical play.');
}
+7 -7
View File
@@ -4561,7 +4561,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The separate fresh local leader-skills --nexus-cards entry combines classic base33/all14 Skills/all12 Nexus in Basic or Advanced, two through six seats, optionally adding original Discovery7/8 through --discoveries. Original Tech requires three or more seats; Stronghold Cards require Advanced. Starting hands/offers, actual end-Spice closing draws, first-Storm Tech and end-Mentat held custody remain. Extra/ordinary free-three returns earn original phase-end Axlotl without changing skill custody. Emperor Cunning composes skill posture, support and Suk rescue; Arrakeen reduces payer cost, not committed support or rescue eligibility. Winner cleanup precedes mandatory Tech reward. Unrelated overlays and public starts remain guarded.',
'The original all14 skill/Nexus envelope admits single or paired one-family E1/E2, BOTH selected Ix+CHOAM native families/union47, and standalone OR paired E3/ecaz33 with classics, Basic/Advanced2–6, optional Homeworlds/Discovery7/8/Tech3+/Advanced Strongholds. Full Suboid support composes winning typed fixed-loss Suk before equal actual Cyborg Tank substitution. Held subsidy changes debit, not rescue entitlement. Original cleanup/winner Tech precedes Face Dance; the replaced trainer dies/returns its skill once without another bounty, while earlier held rewards stay with the original winner. Original Auditor exclusion, signed Richese invoices, CHOAM fuel, native cache and pending-effect guards remain.',
'Standalone OR paired Ecaz/Moritani original skill/Nexus entries use ecaz33/all14/all12 and classics, Basic/Advanced2–6, optional Homeworlds/Discovery/Tech3+/Advanced Strongholds. Each original roster/capture guard remains, including Advanced Harkonnen exclusion and no shared-Duke training. Quiet living/unclaimed Duke Cunning and native Advanced assassination use original distinct discs and exact continuation. In paired tables, native Ecaz may be a different opposing-faction assassination target; normal foreign-world Traitor prohibition does not become an assassination ban. Exceptional Duke custody, mixed E3/three-family, enhanced Terror/HMS/Grumman and pending effects remain separate.',
'The original nexus entry also admits standalone Ecaz OR Moritani with classics, ecaz33/all12 and NO Skills, Basic/Advanced2–6, optionally Homeworlds, Discovery7/8, Tech3+ and Advanced Strongholds2+. Original Traitors/native setup precede a qualifying closing draw, with no starting Nexus hand. Quiet Duke Cunning stays separate from shared-disc training; no-Skills Ecaz may face Advanced Harkonnen without lifting the Cunning exclusion. Advanced Moritani excludes Harkonnen, retains post-loss private assassination/normal-call forfeiture and Mentat replacement, and uses supply-only Terror Cunning. Original Ecaz-selected coalition losses/payments and source-local held benefits/Tech remain. A sole loser token transfers automatically after cleanup; a real multi-token choice remains mandatory. Other native/mixed families, borrowed free-return/companion and relocation rulings, public starts and save conversion remain separate.',
'The original nexus entry also admits standalone Ecaz OR Moritani with classics, ecaz33/all12 and NO Skills, Basic/Advanced2–6, optionally Homeworlds, Discovery7/8, Tech3+ and Advanced Strongholds2+. Original Traitors/native setup precede a qualifying closing draw, with no starting Nexus hand. No-Skills Ecaz facing Advanced Harkonnen now connects shared-Duke acquisition, original capture/return and Cunning liberation without shared-disc training. Advanced Moritani still excludes Harkonnen, retains post-loss private assassination/normal-call forfeiture and Mentat replacement, and uses supply-only Terror Cunning. Original Ecaz-selected coalition losses/payments and source-local held benefits/Tech remain. A sole loser token transfers automatically after cleanup; a real multi-token choice remains mandatory. Other native/mixed families, borrowed free-return/companion and relocation rulings, public starts and save conversion remain separate.',
'Atreides Cunning adds a second, different opposing plan element after the first native answer. Both remain binding. Use it in your own battle before sealing your plan; Karama may cancel the extra attempt while preserving the first disclosure.',
'With Atreides absent, Secret Ally inspects one opposing element in your battle. A sealed plan supplies only that selected answer automatically. Otherwise the opponent chooses an answer that permits a legal plan. Other players receive no private answer.',
'The No-Field restriction names Atreides, so Cunning cannot inspect that dial. A non-Atreides Secret Ally holder may request it without learning the token denomination or other plan elements.',
@@ -4600,7 +4600,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'When Moritani is absent, a defeated combatant with an eligible publicly played card receives the same postbattle choice whether or not they hold Moritani Nexus. An unallied holder may spend it to retain one played card that a winner could keep; other played cards follow ordinary discard. Without a publicly eligible card there is no prompt. The choice is after the battle result and before later income/capture; the public prompt protects hidden custody and is an interface inference. This Secret Ally effect has no invented ordinary Karama response. Native Moritani allied retention remains a different, cancelable ability.',
'With actual Advanced Stronghold Cards, Arrakeen bank support changes the real payer debit, not personal income. Emperor Cunning temporary Sardaukar remain physically ordinary counters and retain ordinary Tanks custody. Original losing Moritani retention precedes mandatory original-winner Tech, then CHOAM inspection excludes used opposing cards even if retained. Ordinary city Collection remains separate; none of these windows repeats the original support, card cost or casualties.',
'Ecaz Betrayal is available to an unallied holder with Ecaz seated and reciprocally allied in a shared Arrakis territory. At the opening of Shipment and Movement, choose one such territory; the ally returns its entire normal and starred force group from every sector there to reserves, leaving Ecaz forces in place. The Nexus card is spent before a Karama response. Cancellation preserves the board but not the card. Only the base/Ecaz Nexus roster without combined modules is supported.',
'Ecaz Cunning lets an unallied native holder spend its physical Nexus at a quiet Battle boundary to take the existing uncaptured, non-Ghola Duke for this turn, including a Moritani-controlled living disc or the set-aside disc in Tanks. Tanks acquisition preserves death history, revives the same disc and clears its old battle-use stamp under an explicit provisional independent-resurrection policy, without a paid revival charge or ordinary allowance. Original standalone OR paired Ecaz/Moritani/classic profiles keep their supported optional modules. The shared Duke stays outside ordinary training and is set aside after use or at turn end. Captured/Ghola custody, Advanced Harkonnen and unqualified reaction timing remain guarded; the quiet Battle boundary is product timing, not a printed restriction.',
'Ecaz Cunning lets an unallied native holder spend its physical Nexus at a quiet Battle boundary to take the existing Duke for this turn, including living control, original captured custody or the set-aside disc in Tanks. Tanks preserve deaths and independently clear old use without spice or ordinary allowance; a living already-used disc gains no fresh use. Standalone no-Skills Ecaz/Advanced Harkonnen now connects original capture Keep/Execute and native return. Provisional liberation clears captivity permanently; use or turn expiry sets Duke aside, never back to the captor. Actual Ecaz Ghola returns Duke alive/set aside before separate Cunning; foreign-Ghola revival remains forbidden. Original other profile/module and shared-training guards remain. Quiet Battle timing and capture priority/return are product interpretations, not publisher-specific rulings.',
'Fremen Betrayal has two bounded advance-declaration alternatives for an unallied holder in a classic Nexus table. Before the first Spice Blow or worm appearance, spend the physical card to prevent Fremen ordinary and Cunning worm rides for the turn; worm survival, destruction and Nexus still resolve. This clean timing is a product boundary, not an official post-appearance priority ruling. Instead, before Fremen move during Shipment and Movement, spend it to suppress their native two-territory advantage for that turn while ordinary movement and independent ornithopters remain. Only one alternative can be used even if the card is recycled later that turn. Owner controls show the currently available choice; combined modules and reactive post-worm timing remain gated.',
'When Ecaz is absent, an unallied holder may spend its Nexus card at a quiet play boundary to ask whether any one player, including themself, holds one of the holder’s native faction leaders as an ordinary Traitor Card. The owner receives a private yes/no answer saved at the moment of use; no name or count is disclosed. Dead or captured native leaders count, but foreign Gholas, Duke Vidal, Cheap Hero and Face Dancers do not. Native-leader scope and private-only audience are explicit product interpretations, not publisher rulings; exceptional controlled-leader and public-disclosure combinations remain open.',
'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Select the explicit shipment checkbox; forces, elite identity, destination restrictions, Homeworld reserve sources and normal movement remain unchanged. Fremen remain free within their normal reinforcement range; Guild and an active Karama rate retain their existing tariff and payment routing. This is not a concealed No-Field or an extra shipment.',
@@ -4615,11 +4615,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
checklist: [
{area:'Implementation',status:'Partial',detail:"Existing independent revival and seller/Ambassador/joint special sources remain. Normal bank purchase now admits canonical native Nexus public profiles and their optional envelopes, preserving exact old classic admission. Actual own positive price, no ally share, native card/capacity/bonus/next inspection and unchanged bank receipt remain; no subsidy or Richese cache/seller source is invented. Strict legacy physical revival stays classic. Complete combinations and assurance are open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts"]},
{area:'Player controls',status:'Partial',detail:"Original uniform bank winner payment and eligible private Nexus Use now work for native rosters; no hidden-face-only pause. Existing Pay/Karama, native bonus and next inspection controls are reused. Actual390px Richese5 to5/Ecaz12 to12 preserve Homeworld/Tech, Basic Tleilaxu5 to5 and wrong-face CHOAM Pay14 to12 preserve one card and next index. No overflow/errors. Existing seller/extra/joint controls remain; full-mode acceptance stays open.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:'AI',status:'Partial',detail:"Existing minimal bank adapter uses the actual shared legal projection for native buyers, or original Pay for an irrelevant face, without rival private inspection. Genuine Easy CHOAM continuations cover both faces. Existing extra/seller/joint adapters and conservative budgets remain; strategy and four-tier calibration are deferred and unfinished.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:'Documentation',status:'Partial',detail:"Printed bank purchase is no longer fenced to classic prototypes; canonical native admission and exact old classic parity are explicit. Uniform payment timing retains its provisional privacy recommendation, not a new publisher ruling. Legacy physical revival remains separately guarded. Seller/Ambassador/joint interpretations, unsupported native aliases and full-mode gates remain distinct.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md"]},
{area:'Verification',status:'Partial',detail:"Native bank affected16-file union217/217, types/lint/build and thirty-six actual API profiles pass: all six natives Basic/Advanced Pay/Use and Advanced Homeworld/Tech Pay/Use. One actual card, original wallets/module inventory and next-lot inspection remain. Four actual390px surfaces prove Richese5 to5/Ecaz12 to12 with modules unchanged, Basic Tleilaxu5 to5 and Guild-card CHOAM Pay14 to12; actual Atreides sees next Maula Pistol. No overflow/errors or saved/deployed rooms touched. Forced-response and five-disc Auditor-exclusion fixture pins were deleted. Prior joint112/112, Ambassador111/111, seller108/108 and other evidence remains; broad assurance/deployed gates stay closed.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts"Line truncated
{area:"Implementation",status:'Partial',detail:"Original standalone Ecaz/classic no-Skills Nexus now connects the actual Advanced Harkonnen lottery, including the just-used losing Duke, retained tenure, Keep/Execute, captive battle use/all-native-dead return and real Cunning liberation. Exact private snapshots preserve original secret capture/execution. Actual Ecaz Ghola returns set aside before separate Cunning; foreign revival remains forbidden. Prior bank/seller/Ambassador/joint and revival sources remain. Broader Skills/paired/timing and full-combination assurance stay open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts"]},
{area:"Player controls",status:'Partial',detail:"Actual390px production controls retain/execute the shared captive, liberate with one Nexus, select Duke6 in a real battle and release once through Collection. Separate actual Ecaz Ghola returns set aside before a distinct Cunning spend. Original bank/seller/Ambassador/joint controls remain; no overflow/errors observed. Wider timing, shared training and complete journeys remain open.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:"AI",status:'Partial',detail:"Existing minimal captive and Duke policies reuse projected actual shared-disc ownership and availability without a second native roster. Four-profile existing Ecaz/Duke source regressions remain legal under the connected native path. No strategic or calibration work is claimed; prior purchase/revival adapters remain.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Printed capture/Tanks Cunning and Ecaz-only revival are distinguished from provisional capture-before-final-release and return destinations. Legal Ecaz Ghola then separate Cunning does not manufacture forbidden foreign custody. Standalone no-Skills Advanced Harkonnen is connected, not paired/Skills or arbitrary timing. Prior bank/seller/direct source interpretations and full-mode gates remain.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md"]},
{area:"Verification",status:'Partial',detail:"Captured-Duke related13-file union127/127, types/lint/build pass. Actual390px Keep, used Execute2, Cunning liberation, Duke6 battle/release into Collection and original Ecaz Ghola then separate Cunning preserve one shared identity, death history, original costs and no ordinary leader slot. No overflow/browser errors or saved/deployed rooms touched. Incidental SSR/wiring/wording tests are removed, not re-pinned. Prior bank/seller/joint evidence remains; broad assurance and full-mode acceptance stay open.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.ts", "tests/nexus-emperor-bank-runtime.test.ts", "tests/duke-captured-custody.test.ts", "tests/nexus-ecaz-duke-captured.test.ts", "tests/fixture-nexus-duke-capture.ts", "tests/nexus-duke-capture-runtime.test.ts"]},
],
},
{