Add private Ecaz Nexus Secret Ally inquiry

This commit is contained in:
Quintine committed 2026-09-28 11:45:14 +10:00
1 parent 886a8f4478
commit 0c6d28a706
14 files changed
+496 -26

No files matched your search

+3
View File
@@ -16,6 +16,7 @@ import { nexusGuildCunningAction, nexusGuildCunningActive, nexusGuildHajrAction,
import { nexusMoritaniBotActions } from './nexus-moritani-options';
import { moritaniBetrayalBotActions } from './nexus-moritani-betrayal-options';
import { ecazBetrayalBotActions } from './nexus-ecaz-betrayal-options';
import { ecazInquiryBotActions } from './nexus-ecaz-inquiry-options';
import { ecazBetrayalOffer } from './nexus-ecaz-betrayal';
import { ECAZ_START_FORCES, ECAZ_START_LOCATIONS, quoteEcazStartingForces } from './ecaz-setup';
import { choamPowerAction, choamPowerBotPlay } from './choam-power-options';
@@ -3875,6 +3876,8 @@ export function botActions(g: GameView): Action[] {
if (moritaniBetrayal.length) return moritaniBetrayal;
const ecazBetrayal = ecazBetrayalBotActions(g);
if (ecazBetrayal.length) return ecazBetrayal;
const ecazInquiry = ecazInquiryBotActions(g);
if (ecazInquiry.length) return ecazInquiry;
const sardaukar = nexusSardaukarBotActions(g);
if (sardaukar.length) return sardaukar;
const advisorConversion = nexusAdvisorBotActions(g);
+57
View File
@@ -36,6 +36,7 @@ import { quoteNexusChoamTrade, validateNexusChoamTrade, nexusChoamTradeSignature
import { quoteNexusChoamInspection, sampleNexusChoamInspection, nexusChoamInspectionSignature, type NexusChoamInspectionReceipt } from './nexus-choam-inspection';
import { quoteNexusMoritaniRetention, nexusMoritaniRetentionSignature, type NexusMoritaniRetentionReceipt } from './nexus-moritani-retention';
import { quoteNexusChoamBetrayal, validateNexusChoamBetrayal, nexusChoamBetrayalSignature, type NexusChoamBetrayal } from './nexus-choam-betrayal';
import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySignature, validateEcazInquiry, type EcazInquiryReceipt } from './nexus-ecaz-inquiry';
import { EMPEROR_NEXUS_REVIVALS, emperorNexusEvent, emperorNexusSignature, emperorNexusModeSupported, emperorNexusPools, emperorNexusRevivalElites, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
import { FREMEN_NEXUS_FREE_FORCES, fremenNexusRevivalOffer, fremenNexusRevivalPools, fremenNexusRevivalSignature, validateFremenNexusRevival, type FremenNexusRevival } from './nexus-fremen-revival';
import { captureFremenCunningOccurrence, quoteFremenCunning, quoteFremenCunningRide, validateFremenCunningSelection, type FremenCunningOccurrence, type FremenCunningRideAuthorization, type FremenCunningSelection } from './nexus-fremen-cunning';
@@ -1046,6 +1047,8 @@ export type Game = {
nexusChoamTradeLast?: { event: string; stage: NexusChoamTrade['stage'] };
nexusChoamBetrayals?: NexusChoamBetrayal[];
nexusChoamBetrayalLast?: { event: string; stage: NexusChoamBetrayal['stage'] };
nexusEcazInquiries?: EcazInquiryReceipt[];
nexusEcazInquiryLast?: { event: string };
nexusEmperorSecretHistory?: NexusEmperorRevival[];
nexusEmperorSecretEvents?: string[];
nexusFremenRevivalHistory?: FremenNexusRevival[];
@@ -1950,6 +1953,27 @@ function nexusChoamBetrayalIntegrity(g: Game) {
getPlayer(g, last.target).hand.length === last.handBefore - 1,
'The CHOAM Nexus Betrayal no longer matches its random discard.');
}
function nexusEcazInquiryIntegrity(g: Game) {
const history = g.nexusEcazInquiries;
if (history === undefined) {
requireRule(!g.nexusEcazInquiryLast, 'The Ecaz Nexus inquiry lost its history.');
return;
}
requireRule(Array.isArray(history) && history.length > 0 && !!g.nexusCards?.cards,
'The Ecaz Nexus inquiry lost its physical card history.');
for (const [index, receipt] of history.entries()) {
nexusRule(() => validateEcazInquiry(g, receipt, index));
if (index > 0) {
const previous = history[index - 1];
requireRule(previous.turn < receipt.turn ||
(previous.turn === receipt.turn && previous.phase <= receipt.phase),
'The Ecaz Nexus inquiries changed their original order.');
}
}
requireRule(g.nexusEcazInquiryLast?.event === history.at(-1)!.event,
'The last Ecaz Nexus inquiry lost its original answer.');
}
function currentNexusChoamTrade(g: Game, owner: string) {
return nexusRule(() => quoteNexusChoamTrade(g, owner,
g.phase === 7 && grummanCollectionAutomatic(g)));
@@ -2319,6 +2343,28 @@ function playNexusChoamBetrayal(g: Game, p: Player, action: Action) {
log(g, `${p.name} spent CHOAM Nexus Betrayal. ${target.name} discarded one randomly selected Treachery card without receiving spice.`,
{ faction: p.faction, name: 'CHOAM Nexus Betrayal' });
}
function playNexusEcazInquiry(g: Game, p: Player, action: Action) {
const offer = quoteEcazInquiry(g, p.id,
g.grummanCollection?.stage === 'waiting' && grummanCollectionAutomatic(g));
requireRule(Object.keys(action).sort().join(',') === 'event,target,type' &&
offer && !offer.blocked && action.event === offer.event &&
offer.targets.some(target => target.id === action.target),
offer?.blocked ?? 'Choose the current Ecaz Nexus inquiry and a seated player.');
const target = getPlayer(g, action.target as string);
const receipt: EcazInquiryReceipt = {
event: offer.event, owner: p.id, ownerFaction: p.faction,
target: target.id, targetFaction: target.faction,
turn: g.turn, phase: g.phase, scope: 'native',
answer: ecazInquiryAnswer(p, target), signature: '',
};
receipt.signature = ecazInquirySignature(receipt);
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
(g.nexusEcazInquiries ??= []).push(receipt);
g.nexusEcazInquiryLast = { event: receipt.event };
log(g, `${p.name} spent Ecaz Nexus Secret Ally to ask whether ${target.name} holds any of their native leaders as Traitor Cards. The yes/no answer was delivered privately.`,
{ faction: p.faction, name: 'Ecaz Nexus inquiry' });
}
function playMoritaniBetrayal(g: Game, p: Player, action: Action) {
const offer = moritaniBetrayalOffer(g, p.id,
g.grummanCollection?.stage === 'waiting' && grummanCollectionAutomatic(g));
@@ -21635,6 +21681,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
nexusChoamInspectionIntegrity(state);
nexusMoritaniRetentionIntegrity(state);
nexusChoamBetrayalIntegrity(state);
nexusEcazInquiryIntegrity(state);
marketGholaIntegrity(state);
homeworldRule(() => homeworldGameIntegrity(state));
homeworldBattleLossIntegrity(state);
@@ -21796,6 +21843,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
nexusChoamInspectionIntegrity(g);
nexusMoritaniRetentionIntegrity(g);
nexusChoamBetrayalIntegrity(g);
nexusEcazInquiryIntegrity(g);
marketGholaIntegrity(g);
homeworldRule(() => homeworldGameIntegrity(g));
homeworldBattleLossIntegrity(g);
@@ -21911,6 +21959,7 @@ export function normalizeAutomaticGame(state: Game): Game {
nexusChoamInspectionIntegrity(state);
nexusMoritaniRetentionIntegrity(state);
nexusChoamBetrayalIntegrity(state);
nexusEcazInquiryIntegrity(state);
marketGholaIntegrity(state);
homeworldRule(() => homeworldGameIntegrity(state));
homeworldBattleLossIntegrity(state);
@@ -21944,6 +21993,7 @@ export function normalizeAutomaticGame(state: Game): Game {
nexusChoamInspectionIntegrity(g);
nexusMoritaniRetentionIntegrity(g);
nexusChoamBetrayalIntegrity(g);
nexusEcazInquiryIntegrity(g);
marketGholaIntegrity(g);
homeworldRule(() => homeworldGameIntegrity(g));
homeworldBattleLossIntegrity(g);
@@ -22043,6 +22093,7 @@ function applyActionInner(
if (t === 'discovery') { playDiscovery(g, p, action); return g; }
if (t === 'nexusChoamTrade') { playNexusChoamTrade(g, p, action); return g; }
if (t === 'nexusChoamBetrayal') { playNexusChoamBetrayal(g, p, action); return g; }
if (t === 'nexusEcazInquiry') { playNexusEcazInquiry(g, p, action); return g; }
if (t === 'nexusEmperorRevive') { playNexusEmperorRevive(g,p,action); return g; }
if (t === 'nexusEmperorBetrayal') { playNexusEmperorBetrayal(g,p,action); return g; }
if (t === 'nexusBgBetrayal') { playNexusBgBetrayal(g,p,action); return g; }
@@ -25714,6 +25765,7 @@ export function viewGame(state: Game, id: string) {
nexusChoamInspectionIntegrity(state);
nexusMoritaniRetentionIntegrity(state);
nexusChoamBetrayalIntegrity(state);
nexusEcazInquiryIntegrity(state);
marketGholaIntegrity(state);
homeworldRule(() => homeworldGameIntegrity(state));
homeworldBattleLossIntegrity(state);
@@ -26030,6 +26082,11 @@ export function viewGame(state: Game, id: string) {
nexusChoamInspection: quoteNexusChoamInspection(g, id),
nexusChoamBetrayal: quoteNexusChoamBetrayal(g, id,
g.grummanCollection?.stage === 'waiting' && grummanCollectionAutomatic(g)),
nexusEcazInquiry: {
offer: quoteEcazInquiry(g, id,
g.grummanCollection?.stage === 'waiting' && grummanCollectionAutomatic(g)),
history: ecazInquiryHistory(g, id),
},
nexusEmperorSecretAlly: nexusEmperorSecretAllyOffer(g,id),
nexusEmperorBetrayal: nexusEmperorBetrayalOffer(g, id),
nexusBgBetrayal: nexusBgBetrayalOffer(g, id),
+2 -16
View File
@@ -1,6 +1,7 @@
import type { Card } from './cards';
import type { Game } from './engine';
import { nexusCardMode } from './nexus-cards';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
export type NexusChoamBetrayalOffer = Readonly<{
event: string;
@@ -46,22 +47,7 @@ export function quoteNexusChoamBetrayal(
let blocked: string | null = null;
if (holder.ally) blocked = 'Use a Nexus card while unallied.';
else if (g.status !== 'playing') blocked = 'Use CHOAM Betrayal during play.';
else if (automaticPending || g.response || g.decision || g.truthtrance || g.phaseOpening ||
g.pendingKarama || g.pendingTreacheryDiscard || g.pendingNullentropy ||
g.pendingExchange || g.pendingAmbassador || g.pendingRicheseGift ||
g.pendingRichesePurchaseIncome || g.pendingRevival || g.pendingCapture ||
g.pendingShipment || g.pendingHomeworldShipment || g.pendingIxMove ||
g.pendingFremenMove || g.pendingMobileMove || g.pendingTerrorEntry ||
g.pendingMoritaniPlacement || g.pendingEcazPlacement || g.pendingChoamMove ||
g.pendingChoamWorthless || g.pendingChoamBattleIncome ||
g.pendingChoamMarketGhola || g.pendingAuditor || g.pendingFaceDance ||
g.pendingTech || g.pendingIxTechnology || g.pendingIxAlly ||
g.pendingIxSubstitution || g.pendingIxRicheseTechnology ||
g.pendingWinnerDiscards || g.pendingSukRescue || g.ornithopter ||
g.summonedWorm || g.battle || g.auction || g.richeseAuction ||
g.choamMarket || g.nexusTraitorPending ||
g.nexusTraitorExchanges?.some(record => record.stage === 'return') ||
g.nexusCards?.phase?.stage === 'drawing')
else if (nexusCleanPlayBlocked(g, automaticPending))
blocked = 'Finish the current interaction before using CHOAM Betrayal.';
else if (handSize === 0) blocked = 'CHOAM has no Treachery cards to discard.';
return {
+17
View File
@@ -0,0 +1,17 @@
import type { Action, GameView } from './engine';
/** The target list is public; only the server reads its hidden Traitor Cards. */
export function ecazInquiryAction(game: GameView, target: string): Action | null {
const offer = game.nexusEcazInquiry.offer;
const owner = game.players.find(player => player.id === game.me);
if (game.status !== 'playing' || game.automaticContinuationPending || !owner ||
owner.ally || game.nexusCards?.card !== 'ecaz' || !offer || offer.blocked ||
!offer.targets.some(player => player.id === target)) return null;
return { type: 'nexusEcazInquiry', event: offer.event, target };
}
export function ecazInquiryBotActions(game: GameView): Action[] {
const target = game.nexusEcazInquiry.offer?.targets.find(player => player.id !== game.me);
const action = target && ecazInquiryAction(game, target.id);
return action ? [action] : [];
}
+80
View File
@@ -0,0 +1,80 @@
import type { Game, Player } from './engine';
import type { FactionId } from './catalog';
import { DUKE_VIDAL_ID } from './duke-vidal';
import { nexusCardMode } from './nexus-cards';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
export type EcazInquiryReceipt = {
event: string;
owner: string;
ownerFaction: FactionId;
target: string;
targetFaction: FactionId;
turn: number;
phase: number;
scope: 'native';
answer: boolean;
signature: string;
};
export function ecazInquiryEvent(turn: number, phase: number, owner: string, index: number): string {
return JSON.stringify(['nexusEcazInquiry', turn, phase, owner, index]);
}
export function ecazInquirySignature(receipt: EcazInquiryReceipt): string {
return JSON.stringify([receipt.event, receipt.owner, receipt.ownerFaction, receipt.target,
receipt.targetFaction, receipt.turn, receipt.phase, receipt.scope, receipt.answer]);
}
/** No target hand or leader custody is read while offering every public target. */
export function quoteEcazInquiry(g: Game, owner: string, automaticPending = false) {
const holder = g.players.find(player => player.id === owner);
if (!holder || g.nexusCards?.cards?.hands[owner] !== 'ecaz' ||
nexusCardMode('ecaz', holder.faction, g.players.map(player => player.faction)) !== 'secretAlly')
return null;
let blocked: string | null = null;
if (holder.ally) blocked = 'Use a Nexus card while unallied.';
else if (g.status !== 'playing') blocked = 'Use Ecaz Secret Ally during play.';
else if (nexusCleanPlayBlocked(g, automaticPending))
blocked = 'Finish the current interaction before using Ecaz Secret Ally.';
return {
event: ecazInquiryEvent(g.turn, g.phase, owner, g.nexusEcazInquiries?.length ?? 0),
blocked,
targets: g.players.map(player => ({ id: player.id, name: player.name })),
};
}
/** Native printed leader identity survives death, capture and later revival.
* Foreign Ghola discs and the separate Duke/Cheap Hero identities do not enter. */
export function ecazInquiryAnswer(owner: Player, target: Player): boolean {
return owner.leaders.some(leader => leader.faction === owner.faction &&
leader.id !== DUKE_VIDAL_ID && target.traitors.includes(leader.id));
}
export function ecazInquiryHistory(g: Game, owner: string) {
const visible: Pick<EcazInquiryReceipt, 'event' | 'target' | 'turn' | 'phase' | 'answer'>[] = [];
for (const receipt of g.nexusEcazInquiries ?? [])
if (receipt.owner === owner)
visible.push({ event: receipt.event, target: receipt.target,
turn: receipt.turn, phase: receipt.phase, answer: receipt.answer });
return visible;
}
/** Historical answers do not re-read a hand changed after the accepted play. */
export function validateEcazInquiry(g: Game, receipt: EcazInquiryReceipt, index: number): void {
const owner = g.players.find(player => player.id === receipt?.owner);
const target = g.players.find(player => player.id === receipt?.target);
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) ||
Object.keys(receipt).sort().join(',') !==
'answer,event,owner,ownerFaction,phase,scope,signature,target,targetFaction,turn' ||
!owner || !target || !!g.players.find(player => player.faction === 'ecaz') ||
!g.nexusCards?.cards || owner.faction !== receipt.ownerFaction ||
target.faction !== receipt.targetFaction || receipt.scope !== 'native' ||
typeof receipt.answer !== 'boolean' ||
!Number.isSafeInteger(receipt.turn) || receipt.turn < 1 || receipt.turn > g.turn ||
!Number.isSafeInteger(receipt.phase) || receipt.phase < 0 || receipt.phase > 8 ||
(receipt.turn === g.turn && receipt.phase > g.phase) ||
receipt.event !== ecazInquiryEvent(receipt.turn, receipt.phase, receipt.owner, index) ||
receipt.signature !== ecazInquirySignature(receipt))
throw new Error('Ecaz Nexus inquiry lost its original private answer or seated factions.');
}
+21
View File
@@ -0,0 +1,21 @@
import type { Game } from './engine';
/** Optional no-phase Nexus effects must not interrupt a committed parent. */
export function nexusCleanPlayBlocked(g: Game, automaticPending = false): boolean {
return !!(automaticPending || g.response || g.decision || g.truthtrance || g.phaseOpening ||
g.pendingKarama || g.pendingTreacheryDiscard || g.pendingNullentropy ||
g.pendingExchange || g.pendingAmbassador || g.pendingRicheseGift ||
g.pendingRichesePurchaseIncome || g.pendingRevival || g.pendingCapture ||
g.pendingShipment || g.pendingHomeworldShipment || g.pendingIxMove ||
g.pendingFremenMove || g.pendingMobileMove || g.pendingTerrorEntry ||
g.pendingMoritaniPlacement || g.pendingEcazPlacement || g.pendingChoamMove ||
g.pendingChoamWorthless || g.pendingChoamBattleIncome ||
g.pendingChoamMarketGhola || g.pendingAuditor || g.pendingFaceDance ||
g.pendingTech || g.pendingIxTechnology || g.pendingIxAlly ||
g.pendingIxSubstitution || g.pendingIxRicheseTechnology ||
g.pendingWinnerDiscards || g.pendingSukRescue || g.ornithopter ||
g.summonedWorm || g.battle || g.auction || g.richeseAuction ||
g.choamMarket || g.nexusTraitorPending ||
g.nexusTraitorExchanges?.some(record => record.stage === 'return') ||
g.nexusCards?.phase?.stage === 'drawing');
}
+6 -5
View File
@@ -4041,7 +4041,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'Moritani Cunning modifies its normal Mentat placement: choose one available supply token and a territory on the printed Arrakis board, including a territory already containing Terror. The explicit Cunning toggle keeps ordinary placement and relocation unchanged. Homeworlds, the Hidden Mobile Stronghold, Cunning relocation and combined Grumman use are unavailable.',
'With Moritani seated, an unallied holder of its Nexus card may spend Betrayal at a clean play boundary to return one placed Terror token to hidden Moritani supply without revealing its face. Choose the public location and token, not its secret effect; this does not use Moritani’s Mentat placement or trigger that token.',
'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.',
'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; Duke Cunning and Secret Ally inquiry remain unavailable.',
'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; Duke Cunning remains unavailable.',
'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.',
'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable.',
'With Guild absent, its Secret Ally is an explicit optional source for one ordinary shipment at Guild prices. It supports physical reserve shipment, cross-shipment and return to abstract reserves, plus half-price independently legal world-to-world shipment. Fremen may retain free reinforcement or choose the paid card route beyond its normal range, outside the storm. Native faction, typed sources and normal movement stay unchanged. Homeworld reserve return awaits a ruling; arbitrary foreign-world transport and concealed No-Fields are not granted.',
@@ -4050,11 +4051,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion and native Voice Betrayal suppression, temporary Emperor Sardaukar and actual Sardaukar Betrayal suppression, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return, and Richese Secret Ally reserve shipping priced as one. Borrowed Voice, other Betrayal reactions, Emperor purchase effects, Tleilaxu revival, Fremen worm protection and combined Cunning interactions remain unfinished.', 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/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Private lifecycle, inspections, exchanges, Tleilaxu refresh, Ixian activation, advisor selection, temporary Sardaukar and CHOAM effects with native Karama responses, Moritani Betrayal token selection, Ecaz Betrayal shared-territory choice, Richese shipment pricing and Fremen Cunning offer/remote ride. Remaining effect controls are unfinished.', 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/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All profiles handle integrated effects through private offers, including Moritani Betrayal public-token selection, Ecaz Betrayal public-territory selection and legal Fremen Cunning offer/ride decisions. Complete module strategy and calibration remain 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-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion and native Voice Betrayal suppression, temporary Emperor Sardaukar and actual Sardaukar Betrayal suppression, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return and bounded native-leader Secret Ally inquiry, and Richese Secret Ally reserve shipping priced as one. Ecaz inquiry has a private holder control, legal AI action and saved historical yes/no; native scope and audience remain product interpretations. Borrowed Voice, other Betrayal reactions, Emperor purchase effects, Tleilaxu revival, Fremen worm protection and combined Cunning interactions remain unfinished.', 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-inquiry-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'components/nexus-ecaz-inquiry.tsx', 'tests/nexus-ecaz-inquiry.test.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Private lifecycle, inspections, exchanges, Tleilaxu refresh, Ixian activation, advisor selection, temporary Sardaukar and CHOAM effects with native Karama responses, Moritani Betrayal token selection, Ecaz Betrayal shared-territory choice and owner-only Secret Ally inquiry, Richese shipment pricing and Fremen Cunning offer/remote ride. Remaining effect controls are unfinished.', 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/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All profiles handle integrated effects through private offers, including Moritani Betrayal public-token selection, Ecaz Betrayal public-territory selection and legal Secret Ally inquiry, and Fremen Cunning offer/ride decisions. Complete module strategy and calibration remain 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-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Common rules, twelve printed faces and integrated effect families retain explicit unresolved boundaries, including Ecaz Duke/inquiry, a second Cunning offer during an interrupted Fremen control, Great Maker and combined modules, fresh advisors, storm and borrowed Voice.', 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_GUILD_CUNNING_RUNTIME.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Physical custody, commitments, support solver, territory-selection controls, bot and in-memory SQLite recovery regressions include Moritani Betrayal, bounded Fremen Cunning with a genuine special summon, and Ecaz Betrayal. Local natural and summoned Fremen offer/ride and Ecaz holder/Karama response browser surfaces were checked with isolated fixture views without a room write. Full module games and combined interactions remain separate.', evidence: ['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-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-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Physical custody, commitments, support solver, territory-selection controls, bot and in-memory SQLite recovery regressions include Moritani Betrayal, bounded Fremen Cunning with a genuine special summon, Ecaz Betrayal and private Secret Ally inquiry. Local natural and summoned Fremen offer/ride, Ecaz Betrayal holder/Karama and Ecaz inquiry owner/rival browser surfaces were checked with isolated fixture views without a room write. Full module games and combined interactions remain separate.', evidence: ['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-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-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts'] },
],
},
{