Compose selected E1/E2 independent cards with Nexus rules

Retain selected decks and original optional modules through normal setup, distinguish native Ixian Betrayal from absent-faction replacement, route explicit CHOAM fuel before printed effects, and preserve native No-Field invoices and pair continuation. Connect canonical native cancellation stock and existing human/minimal AI consumers.

Validation: 194 affected tests across 19 files, types/lint/build, bounded native invoice review and seven finite original-action/rendered-control programmes. Runtime ec5ad6c18996d8d958ec0d01132ef3e3ea6d23d36188902eca9a63d4db2efbf8; documentation-only proof overlay a5892004937434429916496497c07a3838a324b4a5d64d4a0eb804ab5212d5bd. Other envelopes, full combinations, comprehensive assurance and deployed acceptance remain open. No saved-room/provider changes.
This commit is contained in:
Quintine committed 2026-10-18 10:46:21 +11:00
1 parent 999edb67e6
commit 8422cf1f2d
20 files changed
+1413 -97

No files matched your search

+103 -44
View File
@@ -1,5 +1,5 @@
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { classicIndependentNexusProfile, classicNexusCoreProfile, nativeFactionTechProfile } from './faction-module-profile';
import { independentNexusProfile, nexusCoreProfile, nativeFactionTechProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
@@ -9674,7 +9674,7 @@ export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game
return initializeNexusGameForAudit(state);
}
function harkonnenNexusBetrayalModeSupported(g: Game): boolean {
if (classicNexusCoreProfile(g)) return !!byFaction(g, 'harkonnen');
if (nexusCoreProfile(g)) return !!byFaction(g, 'harkonnen');
return typeof g.advanced === 'boolean' && !!g.nexusCards && g.expansions.length === 0 &&
g.players.length >= 2 && g.players.length <= 6 && !!byFaction(g,'harkonnen') &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
@@ -9883,6 +9883,7 @@ export function initializeIxianNexusBetrayalGameForAudit(state: Game): Game {
return g;
}
function ixianNexusBetrayalModeSupported(g: Game): boolean {
if (nexusCoreProfile(g)) return !!byFaction(g, 'ixians');
return typeof g.advanced === 'boolean' && !!g.nexusCards &&
g.expansions.length === 1 && g.expansions[0] === 'ix' &&
g.players.length >= 2 && g.players.length <= 6 &&
@@ -9898,8 +9899,12 @@ function ixianNexusBetrayalModeSupported(g: Game): boolean {
function ixianNexusBetrayalContext(g: Game) {
return {status:g.status,phase:g.phase,turn:g.turn,advanced:g.advanced,
sequence:g.nexusIxianBetrayalCursor!.sequence,players:g.players,
...(g.ecazTreachery ? {expansions:g.expansions,ecazTreachery:true} : {}),
cards:g.nexusCards!.cards!,physicalCards:physicalTreacheryCards(g)};
}
function ixianNexusCardConfiguration(g: Game) {
return g.ecazTreachery ? {expansions:g.expansions,ecazTreachery:true,richese:!!byFaction(g,'richese')} : undefined;
}
function ixianNexusBetrayalAuthority(g: Game, response: ResponseWindow) {
requireRule(response.kind === 'ixAuction' || response.kind === 'ixTechnology',
'The saved Nexus continuation is not an original native Ixian window.');
@@ -9926,6 +9931,9 @@ function ixianNexusBetrayalParent(g: Game, response: ResponseWindow): string {
forces:p.forces,reserves:p.reserves,tanks:p.tanks,elites:p.elites,advisors:p.advisors})),
deck:g.deck,discard:g.discard,aid:g.aid,nexus:g.nexusCards,decision:g.decision,response:g.response,
phaseOpening:g.phaseOpening,truthtrance:g.truthtrance,pendingKarama:g.pendingKarama,
...(g.ecazTreachery ? {selectedDecks:g.expansions,ecazTreachery:true,
richeseCache:g.richeseCache,richeseRemoved:g.richeseRemoved,richeseAuction:g.richeseAuction,
homeworlds:g.homeworlds} : {}),
pendingNullentropy:g.pendingNullentropy,pendingTreacheryDiscard:g.pendingTreacheryDiscard});
}
function ixianNexusBetrayalIntegrity(g: Game): void {
@@ -9940,7 +9948,7 @@ function ixianNexusBetrayalIntegrity(g: Game): void {
Array.isArray(g.nexusIxianBetrayalHistory), 'The native Ixian Nexus profile lost its history or cursor.');
nexusCardsIntegrity(g);
nexusRule(() => validateIxianBetrayalHistory(g.nexusIxianBetrayalHistory!,
g.nexusIxianBetrayalCursor!,physicalTreacheryCards(g)));
g.nexusIxianBetrayalCursor!,physicalTreacheryCards(g),ixianNexusCardConfiguration(g)));
if (!frame) {
if (g.ixAuction?.cards.length === 0 || g.pendingIxTechnology) {
const continuation = g.pendingTreacheryDiscard?.continuation;
@@ -9978,7 +9986,7 @@ function offerIxianNexusBetrayal(g: Game,response: ResponseWindow): boolean {
// finish. That spent attempt fizzles without exposing a Nexus offer.
if (!getPlayer(g, response.owner).hand.some(c => c.id === g.pendingIxTechnology!.card)) return false;
}
const required = nexusRule(() => ixianBetrayalResponders(g.nexusCards!.cards!,g.players));
const required = nexusRule(() => ixianBetrayalResponders(g.nexusCards!.cards!,g.players,ixianNexusCardConfiguration(g)));
if (!required.length) return false;
const source = nexusRule(() => createIxianBetrayalSource(ixianNexusBetrayalContext(g),
ixianNexusBetrayalAuthority(g,response),ixianNexusBetrayalParent(g,response)));
@@ -9989,7 +9997,7 @@ function ixianNexusBetrayalView(g: Game,id: string) {
const frame = g.pendingNexusIxianBetrayal;
if (!frame) return null;
const hasPassed = frame.passed.includes(id), canPass = frame.required.includes(id) && !hasPassed;
const eligible = canPass && nexusRule(() => ixianBetrayalEligible(g.nexusCards!.cards!,g.players,id));
const eligible = canPass && nexusRule(() => ixianBetrayalEligible(g.nexusCards!.cards!,g.players,id,ixianNexusCardConfiguration(g)));
return {event:frame.source.event,kind:frame.source.kind,provider:frame.source.provider,
canPass,hasPassed,canUse:eligible,blocked:null as string | null};
}
@@ -10002,7 +10010,7 @@ function decideIxianNexusBetrayal(g: Game,id: string,action: Action): void {
frame.required.includes(id) && !frame.passed.includes(id),
'A current required seat must acknowledge only the original Ixian event.');
const use = action.type === 'nexusIxianBetrayalUse';
if (use) requireRule(nexusRule(() => ixianBetrayalEligible(g.nexusCards!.cards!,g.players,id)),
if (use) requireRule(nexusRule(() => ixianBetrayalEligible(g.nexusCards!.cards!,g.players,id,ixianNexusCardConfiguration(g))),
'You do not own an eligible unallied Ixian Nexus card.');
if (!use) {
frame.passed.push(id);
@@ -10037,7 +10045,7 @@ export function initializeIxianNexusReplacementGameForAudit(state: Game): Game {
return g;
}
function ixianNexusReplacementModeSupported(g: Game): boolean {
if (classicNexusCoreProfile(g)) return true;
if (nexusCoreProfile(g)) return !byFaction(g, 'ixians');
return !!g.nexusCards && g.players.length >= 2 && g.players.length <= 6 &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
((!g.expansions.length && g.players.every(p => CLASSIC_FACTIONS[p.faction] === true)) ||
@@ -10221,7 +10229,7 @@ export function initializeGuildBetrayalLeaderSkillsGameForAudit(state: Game): Ga
return initializeLeaderSkillsGameForAudit(g);
}
function guildBetrayalModeSupported(g: Game): boolean {
if (classicNexusCoreProfile(g)) return !!byFaction(g, 'guild');
if (nexusCoreProfile(g)) return !!byFaction(g, 'guild');
return !!g.nexusCards && g.expansions.length === 0 && g.players.length >= 2 &&
g.players.length <= 6 && !!byFaction(g, 'guild') &&
g.players.every(p => CLASSIC_FACTIONS[p.faction] === true) &&
@@ -10248,6 +10256,11 @@ function guildBetrayalParentSignature(g: Game, continuation: GuildBetrayalContin
pendingGuildTransport: g.pendingGuildTransport,
...(cunning ? { nexusGuildCunning: cunning } : {}),
...(richese ? { nexusRichese: richese } : {}),
...(g.ecazTreachery && g.expansions.length ? {
nativeMarkers:g.players.map(p => ({id:p.id,noField:p.noField,noFieldEvent:p.noFieldEvent,
noFieldBlockedTurn:p.noFieldBlockedTurn})),
richeseCunning:g.nexusRicheseCunningHistory, richeseAllyOffer:g.richeseAllyOffer,
} : {}),
...(g.techTokens ? { techTokens: g.techTokens } : {}),
...(g.strongholdCards ? { strongholdCards: g.strongholdCards } : {}),
});
@@ -10273,6 +10286,7 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation
Object.keys(continuation).sort().join(',') === producerKeys[continuation.source],
'The saved Guild shipment producer contains foreign action fields.');
let shipper: string, price: number, allyPayment: number, originalReceiver: string | null;
let approvedSupplier: {donor:string; amount:number} | null = null;
const guild = byFaction(g, 'guild')!;
switch (continuation.source) {
case 'reserve': {
@@ -10283,19 +10297,44 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation
'smuggler', 'smugglerCompanion', 'guildSecretEvent', 'guildNexusEvent', 'nexusEvent',
'richesePair', 'noField', 'alliedNoField', 'noFieldSkillProof'].includes(key)),
'The saved reserve shipment contains foreign source fields.');
requireRule(!shipment.source && !shipment.noField && !shipment.alliedNoField &&
!shipment.richesePair && !shipment.guildSecretEvent &&
((shipment.smuggler && classicNexusCoreProfile(g)) || (!shipment.smuggler && !shipment.smugglerCompanion)) &&
((!shipment.guildNexusEvent && !shipment.nexusEvent) || classicNexusCoreProfile(g)),
requireRule(!shipment.source && !shipment.guildSecretEvent &&
((!shipment.noField && !shipment.alliedNoField && !shipment.richesePair) || nexusCoreProfile(g)) &&
(nexusCoreProfile(g) || (!shipment.smuggler && !shipment.smugglerCompanion)) &&
((!shipment.guildNexusEvent && !shipment.nexusEvent) || nexusCoreProfile(g)),
'This Guild Betrayal profile requires a supported original physical shipment source.');
validatePhysicalShipment(g, shipment);
validateShipmentArrival(g, shipment);
checkShipmentIncomeFunding(g, getPlayer(g, shipment.player), shipment.cost, shipment.allyPayment);
const p = getPlayer(g, shipment.player);
if (shipment.noField) {
requireRule(!shipment.alliedNoField && !shipment.nexusEvent && !shipment.guildNexusEvent &&
p.faction === 'richese' && p.noField && p.noFieldEvent === shipment.noField.event &&
!p.noField.deployed && g.active === p.id && shipmentAvailable(g,p) && shipment.amount === 1 &&
shipment.elite === 0 && shipment.advisors === false,
'The funded No-Field invoice needs its original unused native marker.');
noFieldRule(() => deployRicheseNoField(p.noField!, {tokenId:shipment.noField!.tokenId,
controller:p.id,location:{territory:shipment.territory,sector:shipment.sector}}));
const cost = reserveShipmentCost({faction:p.faction,halfRate:shipmentHalfRate(g,p)},
territory(shipment.territory).type,1);
requireRule(shipment.cost === cost, 'The native No-Field invoice changed its one-marker price.');
} else if (shipment.alliedNoField) {
requireRule(!shipment.nexusEvent && !shipment.guildNexusEvent && !shipment.richesePair &&
!shipment.smuggler && !shipment.smugglerCompanion && shipment.allyPayment === 0 &&
g.active === p.id && shipmentAvailable(g,p),
'An allied No-Field invoice needs its original supplier-approved source.');
const quote = alliedNoFieldQuote(g,shipment.alliedNoField);
requireRule(quote.amount === shipment.amount && quote.cost === shipment.cost &&
shipment.elite >= quote.eliteMin && shipment.elite <= quote.eliteMax &&
shipment.advisors === quote.advisors,
'The allied No-Field allocation changed before funded payment.');
approvedSupplier = {donor:shipment.alliedNoField.owner,amount:quote.ownerPayment};
}
if (!approvedSupplier) checkShipmentIncomeFunding(g, p, shipment.cost, shipment.allyPayment);
checkShipmentPromises(g, getPlayer(g, shipment.player), shipment);
shipper = shipment.player; price = shipment.cost; allyPayment = shipment.allyPayment;
const p = getPlayer(g, shipper);
shipper = shipment.player; price = shipment.cost;
allyPayment = approvedSupplier?.amount ?? shipment.allyPayment;
originalReceiver = guildShipmentIncome({guild: guild.id, shipper, ally: p.ally,
cost: price, allyPayment, bankOnly: g.karamaShipping?.player === shipper}) > 0 ? guild.id : null;
cost: price, allyPayment, bankOnly: g.karamaShipping?.player === shipper ||
(!!shipment.alliedNoField && alliedNoFieldPricing(g,p).bankOnly)}) > 0 ? guild.id : null;
break;
}
case 'guildTransport': {
@@ -10317,7 +10356,7 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation
['player', 'destination', 'sources', 'route', 'event', 'turn', 'amount', 'elite', 'cost',
'allyPayment', 'pools', 'guildSecretEvent', 'guildNexusEvent'].includes(key)),
'The saved Homeworld shipment contains foreign source fields.');
requireRule(!shipment.guildSecretEvent && (!shipment.guildNexusEvent || classicNexusCoreProfile(g)),
requireRule(!shipment.guildSecretEvent && (!shipment.guildNexusEvent || nexusCoreProfile(g)),
'This Guild Betrayal profile requires a supported original Homeworld shipment source.');
const quote = validateHomeworldShipment(g, shipment);
shipper = shipment.player; price = quote.cost; allyPayment = shipment.allyPayment;
@@ -10341,10 +10380,11 @@ function guildBetrayalAuthority(g: Game, continuation: GuildBetrayalContinuation
default: throw new RuleError('The saved Guild Betrayal producer is unsupported.');
}
const p = getPlayer(g, shipper);
contribution(g, p, price, allyPayment);
if (!approvedSupplier) contribution(g, p, price, allyPayment);
return {source: continuation.source, sourceSignature: guildBetrayalParentSignature(g, continuation),
shipper, price, allyPayment, ownSpice: p.spice, allyEscrow: aidFor(g, p)?.amount ?? 0,
donor: allyPayment ? p.ally! : null, originalReceiver};
shipper, price, allyPayment, ownSpice: p.spice,
allyEscrow: approvedSupplier?.amount ?? aidFor(g, p)?.amount ?? 0,
donor: allyPayment ? (approvedSupplier?.donor ?? p.ally!) : null, originalReceiver};
}
function guildBetrayalFrameSignature(frame: NonNullable<Game['pendingGuildBetrayal']>): string {
return JSON.stringify([frame.invoice.signature, frame.invoice.sourceSignature, frame.required, frame.passed]);
@@ -10755,7 +10795,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const homeworlds = homeworldLeaderSkillsProfile(state);
const independent = independentTreacheryLeaderSkillsProfile(state);
if (state.ecazTreachery) {
requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules, or classic Nexus with original World/phase modules; unrelated preview contexts remain gated.');
requireRule(independent, 'Independent Skills cards require supported selected decks with original World/phase modules and supported Nexus selections; unrelated preview contexts remain gated.');
requireFreshBaseRuntime(state);
requireFreshFactionInventory(state);
}
@@ -10775,7 +10815,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const classicNexus = classicNexusLeaderSkillsProfile(g);
const pairedE3 = pairedE3NexusLeaderSkillsProfile(g);
const mixedE3 = mixedE3NexusLeaderSkillsProfile(g);
const initialized = initializeSetupGameForAudit(g, homeworlds, classicNexus || pairedNexusLeaderSkillsProfile(g) ||
const initialized = initializeSetupGameForAudit(g, homeworlds, nexusCoreProfile(g) || classicNexus || pairedNexusLeaderSkillsProfile(g) ||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
mixedE3 || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3 || guildBetrayalNexusLeaderSkillsProfile(g), false,
classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || (g.discoveryEnabled === true && classicNexus), true,
@@ -10802,7 +10842,7 @@ function independentCardModuleRoster(g: Game): boolean {
(g.expansions.length === 1 && g.expansions[0] === 'ecaz'));
}
function ecazHomeworldProfile(g: Game): boolean {
if (g.homeworlds && classicIndependentNexusProfile(g)) return true;
if (g.homeworlds && independentNexusProfile(g)) return true;
if (g.homeworlds && g.leaderSkills && !g.sandtrout && independentTreacheryLeaderSkillsProfile(g)) return true;
return !!g.homeworlds && g.ecazTreachery === true && independentCardModuleRoster(g) &&
g.players.length >= 2 && g.players.length <= 6 &&
@@ -10818,7 +10858,7 @@ function ecazHomeworldProfile(g: Game): boolean {
}
/** Independent cards compose original phase modules, not unrelated previews. */
function independentCardModulesProfile(g: Game): boolean {
if ((g.techTokens || g.strongholdCards) && classicIndependentNexusProfile(g)) return true;
if ((g.techTokens || g.strongholdCards) && independentNexusProfile(g)) return true;
if ((g.techTokens || g.strongholdCards) && g.leaderSkills && !g.sandtrout &&
independentTreacheryLeaderSkillsProfile(g)) return true;
if (!g.ecazTreachery || (!g.techTokens && !g.strongholdCards) || !independentCardModuleRoster(g) ||
@@ -10842,8 +10882,8 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
g.ecazTreachery = true;
const homeworlds = !!g.homeworlds;
requireRule(!homeworlds || ecazHomeworldProfile(g),
'Independent World cards need a supported selected-deck roster and original phase modules, or classic Nexus with original phase modules.');
const initialized = initializeSetupGameForAudit(g, homeworlds, classicIndependentNexusProfile(g), false, false, false, false, true, true);
'Independent World cards need a supported selected-deck roster with original phase modules and supported Nexus selections.');
const initialized = initializeSetupGameForAudit(g, homeworlds, independentNexusProfile(g), false, false, false, false, true, true);
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
return initialized;
@@ -10865,7 +10905,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
classicDiscoveryNexusProfile(g) || pairedDiscoveryNexusProfile(g) ||
mixedE3DiscoveryNexusProfile(g) || mixedE1E2DiscoveryNexusProfile(g) || pairedE3DiscoveryNexusProfile(g) ||
(leaderSkills && (classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || classicNexusLeaderSkillsProfile(g))));
const nexusModules = nexus && (classicNexusModulesProfile(g) || pairedNexusModulesProfile(g) ||
const nexusModules = nexus && (nexusCoreProfile(g) || classicNexusModulesProfile(g) || pairedNexusModulesProfile(g) ||
mixedE3NexusProfile(g) || singleE1E2NexusProfile(g) || mixedE1E2NexusProfile(g) || pairedE3NexusProfile(g) || standaloneE3NexusProfile(g) ||
(leaderSkills && (classicNexusLeaderSkillsProfile(g) || pairedNexusLeaderSkillsProfile(g) ||
mixedE3NexusLeaderSkillsProfile(g) || singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
@@ -10915,7 +10955,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
g.spiceBankerIncomePreview = true;
g.spiceBankerIncome = bankerIncomeRule(() => createBankerIncomeState(bankerIncomeContext(g)));
}
if (classicNexusCoreProfile(g)) {
if (nexusCoreProfile(g)) {
requireRule(g.nexusIxianReplacementPreview === undefined &&
g.nexusIxianReplacementHistory === undefined && g.nexusIxianReplacementCursor === undefined &&
!g.pendingNexusIxianReplacement && !g.pendingNexusIxianSpecialPurchase &&
@@ -10925,11 +10965,22 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
g.guildBetrayalPreview === undefined && g.guildBetrayal === undefined &&
g.nexusGuildBetrayalHistory === undefined && g.guildBetrayalSourceReceipts === undefined &&
!g.pendingGuildBetrayal,
'Native Nexus reactions initialize only with fresh classic setup.');
g.nexusIxianReplacementPreview = true;
g.pendingNexusIxianReplacement = null;
g.nexusIxianReplacementHistory = [];
g.nexusIxianReplacementCursor = initialIxianReplacementCursor();
'Native Nexus reactions initialize only with fresh supported core setup.');
requireRule(g.nexusIxianBetrayalPreview === undefined && g.nexusIxianBetrayalHistory === undefined &&
g.nexusIxianBetrayalCursor === undefined && !g.pendingNexusIxianBetrayal,
'Native Ixian Nexus reactions require original undealt effect state.');
if (!byFaction(g, 'ixians')) {
g.nexusIxianReplacementPreview = true;
g.pendingNexusIxianReplacement = null;
g.nexusIxianReplacementHistory = [];
g.nexusIxianReplacementCursor = initialIxianReplacementCursor();
}
if (byFaction(g, 'ixians')) {
g.nexusIxianBetrayalPreview = true;
g.pendingNexusIxianBetrayal = null;
g.nexusIxianBetrayalHistory = [];
g.nexusIxianBetrayalCursor = initialIxianBetrayalCursor();
}
if (byFaction(g, 'harkonnen')) {
g.nexusHarkonnenBetrayalPreview = true;
g.pendingNexusHarkonnenBetrayal = null;
@@ -13942,7 +13993,7 @@ function recruitsModeSupported(g: Game) {
const modules = independentCardModulesProfile(g);
return (
(!g.homeworlds || ecazHomeworldProfile(g)) &&
(!g.nexusCards || classicIndependentNexusProfile(g)) &&
(!g.nexusCards || independentNexusProfile(g)) &&
(!g.leaderSkills || independentTreacheryLeaderSkillsProfile(g)) &&
(!g.techTokens || modules) &&
(!g.strongholdCards || modules) &&
@@ -28136,7 +28187,7 @@ function projectedNexusGuildCunning(g: Game, owner: string) {
hajrAvailable:record.stage !== 'pending' && !g.hajr.includes(owner)}};
if (g.nexusCards?.cards?.hands[owner] !== 'guild') return null;
let blocked: string | null = null;
if (g.guildBetrayalPreview && !classicNexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Cunning second-shipment overlay.';
if (g.guildBetrayalPreview && !nexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Cunning second-shipment overlay.';
else if (p.ally) blocked = 'Guild Cunning requires an unallied native Guild player.';
else if (g.status !== 'playing' || g.phase !== 5 || g.active !== owner)
blocked = 'Use Cunning when finishing your own Shipment and Movement turn.';
@@ -28338,7 +28389,7 @@ function nexusRicheseOffer(g: Game, owner: string) {
const p = g.players.find(p => p.id === owner);
if (!p || g.nexusCards?.cards?.hands[owner] !== 'richese' || byFaction(g,'richese')) return null;
let blocked: string | null = null;
if (g.guildBetrayalPreview && !classicNexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Richese Secret Ally shipment overlay.';
if (g.guildBetrayalPreview && !nexusCoreProfile(g)) blocked = 'This payment audit profile supports the original native shipment, not a Richese Secret Ally shipment overlay.';
else if (p.ally) blocked = 'Richese Secret Ally requires an unallied holder.';
else if (g.status !== 'playing' || g.phase !== 5 || g.active !== owner || p.shipped)
blocked = 'Use Secret Ally for your unused reserve shipment before moving.';
@@ -28353,6 +28404,7 @@ const RICHESE_CUNNING_ROSTER: Partial<Record<FactionId, true>> = {
beneGesserit: true, guild: true, richese: true, choam: true,
};
function richeseCunningModeSupported(g: Game) {
if (nexusCoreProfile(g)) return !!byFaction(g, 'richese');
const mixed = mixedE3NexusProfile(g) || mixedE1E2NexusProfile(g) ||
(!!g.leaderSkills && (mixedE3NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g)));
if (mixed) return !g.sandtrout;
@@ -28441,7 +28493,10 @@ function finishRichesePairShipment(g: Game, shipment: PendingShipment, stage: 's
}
function richesePairIntegrity(g: Game) {
const history = g.nexusRicheseCunningHistory;
const pending = g.pendingShipment;
const invoice = g.pendingGuildBetrayal?.continuation;
const invoicedPair = invoice?.source === 'reserve' && invoice.shipment.richesePair ? invoice.shipment : null;
const pending = g.pendingShipment ?? invoicedPair;
requireRule(!g.pendingShipment || !invoicedPair, 'Richese Cunning cannot own two shipment continuations.');
if (history === undefined) {
requireRule(!g.nexusRicheseCunningLast && !pending?.richesePair,
'Richese Cunning lost its saved shipment history.');
@@ -28483,8 +28538,8 @@ function richesePairIntegrity(g: Game) {
currentRichesePairShipment(g, pending);
const responses = savedNoFieldResponses(g).filter(response => response.kind === 'richeseNoField');
const decisions = homeworldSavedDecisions(g).filter(decision => decision.kind === 'guildShipment');
requireRule(responses.length + decisions.length === 1,
'Richese Cunning lost its unique Karama response or Guild interception decision.');
requireRule(responses.length + decisions.length + Number(!!invoicedPair) === 1,
'Richese Cunning lost its unique Karama, native Guild interception or funded invoice continuation.');
for (const response of responses)
requireRule(response.owner === pending.player && response.noFieldSkillProof === undefined,
'Richese Cunning changed its native No-Field response.');
@@ -30606,6 +30661,12 @@ function applyActionInner(
decideNexusEmperorAmbassadorPurchase(g, p, action);
return g;
}
// Conversion spends the selected card as fuel; its printed effect must not
// intercept that explicit mode (including Recruits and other special faces).
if (t === 'card' && action.mode === 'choam') {
playChoamWorthless(g, p, action);
return g;
}
if (t === 'card' && action.card === RECRUITS_CARD_ID) {
requireRule(
Object.keys(action).sort().join(',') === 'card,type',
@@ -30845,10 +30906,6 @@ function applyActionInner(
}
return g;
}
if (t === 'card' && action.mode === 'choam') {
playChoamWorthless(g, p, action);
return g;
}
if (t === 'card' && action.mode === 'special') {
requireRule(
!g.choamMarket,
@@ -31196,7 +31253,9 @@ function applyActionInner(
// Verify the entire arrival continuation before the recipient commits.
// Unsupported simultaneous reactions leave this offer declineable.
const trial = structuredClone(g);
commitShipment(trial, trial.pendingShipment!);
// Preflight the native arrival, not an invoice inside this still-owned
// acceptance decision. The real commit opens its invoice after counters.
commitShipment(trial, trial.pendingShipment!, {recipient:null});
g.richeseAllyOffer = null;
g.response = { kind: 'richeseNoField', owner: offer.owner, passed: [] };
log(
+37 -8
View File
@@ -26,36 +26,65 @@ export type FactionModuleProfile = {
nexusHarkonnenBetrayalPreview?: unknown;
};
type ClassicNexusProfile = Partial<FactionModuleProfile> & Pick<FactionModuleProfile, 'expansions'> & {
type NexusCoreConfiguration = Partial<FactionModuleProfile> & Pick<FactionModuleProfile, 'expansions'> & {
sandtrout?: unknown; moritaniAssassinatePreview?: unknown;
};
/** Classic Nexus12 with selected base33/independent36, optional all14, original
* Homeworlds and phase modules. Native effect stamps are not selected overlays. */
export function classicNexusCoreProfile(game: ClassicNexusProfile): boolean {
export function classicNexusCoreProfile(game: NexusCoreConfiguration): boolean {
return nexusCoreRosterProfile(game, false);
}
/** Selected E1/E2 decks with the independent three-card variant. A deck selection
* does not require seating its native faction. Original optional modules compose. */
export function selectedIndependentNexusProfile(game: NexusCoreConfiguration): boolean {
return game.ecazTreachery === true && game.expansions.length > 0 && nexusCoreRosterProfile(game, true);
}
/** Common consumers of the classic and selected-independent fresh Nexus core. */
export function nexusCoreProfile(game: NexusCoreConfiguration): boolean {
return classicNexusCoreProfile(game) || selectedIndependentNexusProfile(game);
}
function nexusCoreRosterProfile(game: NexusCoreConfiguration, selectedDecks: boolean): boolean {
const players = game.players;
if (typeof game.advanced !== 'boolean' || !game.nexusCards ||
game.expansions.length !== 0 || !players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 ||
!players || players.length < (game.techTokens ? 3 : 2) || players.length > 6 ||
(game.strongholdCards && !game.advanced) || game.sandtrout ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.semutaPreview || game.moritaniAssassinatePreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.richeseBetrayalPreview ||
game.nexusIxianBetrayalPreview) return false;
(game.nexusIxianBetrayalPreview && (!selectedDecks || !players.some(player => player.faction === 'ixians')))) return false;
if (selectedDecks) {
if (game.expansions.length > 2) return false;
for (let i = 0; i < game.expansions.length; i++) {
if (game.expansions[i] !== 'ix' && game.expansions[i] !== 'choam') return false;
for (let j = 0; j < i; j++) if (game.expansions[j] === game.expansions[i]) return false;
}
} else if (game.expansions.length !== 0) return false;
for (let i = 0; i < players.length; i++) {
for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false;
let classic = false;
let permitted = false;
for (const faction of FACTIONS)
if (faction.id === players[i].faction) { classic = faction.expansion === 'base'; break; }
if (!classic) return false;
if (faction.id === players[i].faction) {
permitted = faction.expansion === 'base' || (selectedDecks && game.expansions.includes(faction.expansion));
break;
}
if (!permitted) return false;
}
return true;
}
/** Card consumers require the actual independent-variant selection. */
export function classicIndependentNexusProfile(game: ClassicNexusProfile): boolean {
export function classicIndependentNexusProfile(game: NexusCoreConfiguration): boolean {
return game.ecazTreachery === true && classicNexusCoreProfile(game);
}
export function independentNexusProfile(game: NexusCoreConfiguration): boolean {
return game.ecazTreachery === true && nexusCoreProfile(game);
}
/** Original E1/E2 families or standalone Ecaz/Moritani; explicit Semuta context only. */
export function nativeFactionTechProfile(game: FactionModuleProfile, semuta = false): boolean {
if (typeof game.advanced !== 'boolean' || !game.techTokens ||
+4 -4
View File
@@ -1,5 +1,5 @@
import { FACTIONS } from './catalog';
import { classicIndependentNexusProfile, classicNexusCoreProfile, mixedE3NexusRosterProfile } from './faction-module-profile';
import { independentNexusProfile, classicNexusCoreProfile, nexusCoreProfile, mixedE3NexusRosterProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
/** Public configuration only; never gate a skill on a hidden card or random deal. */
@@ -68,7 +68,7 @@ export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boole
/** Fresh classic Nexus/Skills composed only with the bounded Guild Betrayal preview. */
export function guildBetrayalNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (game.guildBetrayalPreview && classicNexusCoreProfile(game))
if (game.guildBetrayalPreview && nexusCoreProfile(game))
return game.players!.some(player => player.faction === 'guild');
if (game.expansions.length || !game.guildBetrayalPreview || game.homeworlds ||
game.techTokens || game.strongholdCards || game.semutaPreview ||
@@ -463,7 +463,7 @@ function homeworldLeaderSkillModulesSupported(game: LeaderSkillProfile): boolean
/** Original Homeworld/Skills under the existing classic or native Nexus roster contracts. */
export function homeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (!game.homeworlds) return false;
if (classicNexusCoreProfile(game)) return true;
if (nexusCoreProfile(game)) return true;
if (game.expansions.length) return nativeExpansionLeaderSkillsProfile(game);
if (!homeworldLeaderSkillModulesSupported(game)) return false;
const players = game.players!;
@@ -479,7 +479,7 @@ export function homeworldLeaderSkillsProfile(game: LeaderSkillProfile): boolean
/** Independent cards with registered selected-family all14 setup and original module/role guards. */
export function independentTreacheryLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (classicIndependentNexusProfile(game)) return true;
if (independentNexusProfile(game)) return true;
if (game.ecazTreachery !== true || game.expansions.length > 3 ||
typeof game.advanced !== 'boolean' || !game.players ||
game.players.length < (game.techTokens ? 3 : 2) || game.players.length > 6 ||
+4 -2
View File
@@ -18,7 +18,7 @@ import type { RicheseAuction } from './richese-auction';
import { quoteRicheseSettlement, type RicheseSettlementQuote } from './richese-settlement';
import { quoteRicheseBetrayalInvoice } from './nexus-richese-betrayal';
import { RICHESE_CARD_DEFINITIONS, richeseCardDefinition } from './richese-cards';
import { classicNexusCoreProfile } from './faction-module-profile';
import { nexusCoreProfile } from './faction-module-profile';
export type EmperorNexusBankPurchase = {
kind: 'purchase';
@@ -381,7 +381,7 @@ function nativeBankNexusProfile(g: Game): boolean {
* with the Emperor absent, the concealed face and any buyer wallet or hand left
* to the caller. */
export function emperorNexusBankPurchaseModeSupported(g: Game): boolean {
if (classicNexusCoreProfile(g))
if (nexusCoreProfile(g))
return !g.players.some(p => p.faction === 'emperor') && !g.sandtrout;
if (!g.nexusCards || g.players.some(p => p.faction === 'emperor') ||
g.ecazTreachery || g.sandtrout) return false;
@@ -391,6 +391,8 @@ export function emperorNexusBankPurchaseModeSupported(g: Game): boolean {
/** Reuse native Nexus profiles; the existing Richese Betrayal preview composes
* only on its original bare paired CHOAM/Richese roster, not arbitrary overlays. */
export function emperorNexusSellerPurchaseModeSupported(g: Game): boolean {
if (nexusCoreProfile(g))
return !g.players.some(p => p.faction === 'emperor') && g.players.some(p => p.faction === 'richese');
if (!g.nexusCards || g.players.some(p => p.faction === 'emperor') ||
!g.players.some(p => p.faction === 'richese') || g.ecazTreachery || g.sandtrout)
return false;
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Game, Player } from './engine';
import { eliteRevivalRemaining, revivalPrevented } from './revival';
import { FACTIONS } from './catalog';
import { classicNexusCoreProfile } from './faction-module-profile';
import { nexusCoreProfile } from './faction-module-profile';
import {
classicNexusModulesProfile,
pairedNexusModulesProfile,
@@ -206,7 +206,7 @@ export function validateEmperorNexusRevival(
/** Reuse original fresh roster/module predicates, independently of purchase admission. */
function revivalModeSupported(g: Game): boolean {
if (classicNexusCoreProfile(g)) return true;
if (nexusCoreProfile(g)) return true;
if (g.semutaPreview || g.advancedPreview || g.kullPreview || g.nexusKullPreview ||
g.guildBetrayalPreview || g.richeseBetrayalPreview || g.nexusIxianReplacementPreview ||
g.nexusIxianBetrayalPreview || g.nexusHarkonnenBetrayalPreview || g.ecazTreachery)
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Game } from './engine';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { classicNexusCoreProfile } from './faction-module-profile';
import { nexusCoreProfile } from './faction-module-profile';
export type FremenNexusBetrayal = {
event: string;
@@ -40,7 +40,7 @@ export function fremenBetrayalOffer(
if (holder.ally) blocked = 'An allied player cannot use a Nexus card.';
else if (g.status !== 'playing' || (g.phase !== 1 && g.phase !== 5))
blocked = 'Use Fremen Betrayal before the first Spice Blow or before Fremen movement.';
else if (!classicNexusCoreProfile(g) &&
else if (!nexusCoreProfile(g) &&
(g.expansions.length || g.homeworlds || g.leaderSkills || g.discoveryEnabled ||
g.discoveries || g.discoveryStash || g.greatMaker || g.ecazTreachery ||
g.techTokens || g.strongholdCards ||
+54 -22
View File
@@ -1,4 +1,7 @@
import { treacheryDeck, type Card } from './cards';
import { FACTIONS } from './catalog';
import { ecazTreacheryCards } from './ecaz-cards';
import { richeseCards } from './richese-cards';
import {
nexusCardMode, validateNexusCards, validateNexusPlayers,
type NexusPlayer, type NexusState,
@@ -15,7 +18,17 @@ export type IxianBetrayalAuthority = {
nativeRequired: readonly string[];
nativePassed: readonly string[];
};
export type IxianBetrayalContext = {
/** Public selections, never inferred from a hidden hand or a historical receipt.
* Omission retains the original native Ixian deck and roster contract. */
export type IxianBetrayalSelection = {
expansions?: readonly string[];
ecazTreachery?: boolean;
};
export type IxianBetrayalPhysicalSelection = IxianBetrayalSelection & {
/** History has no live roster; its caller supplies public native Richese presence. */
richese?: boolean;
};
export type IxianBetrayalContext = IxianBetrayalSelection & {
status: string;
phase: number;
turn: number;
@@ -52,12 +65,7 @@ export class IxianBetrayalError extends Error {
this.name = 'IxianBetrayalError';
}
}
const canonical = new Map(treacheryDeck(['ix']).map(card => [card.id, card]));
const genesis = 'ixian-betrayal:0';
const permitted: Readonly<Record<string, true>> = {
atreides: true, harkonnen: true, emperor: true, fremen: true, guild: true,
beneGesserit: true, ixians: true, tleilaxu: true,
};
const plain = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value) &&
[Object.prototype, null].includes(Object.getPrototypeOf(value));
@@ -79,36 +87,59 @@ function members(value: unknown): value is readonly string[] {
function sameMembers(one: readonly string[], two: readonly string[]): boolean {
return one.length === two.length && one.every((id, index) => id === two[index]);
}
function canonicalCard(value: unknown): value is Card {
function selectedExpansions(selection: IxianBetrayalSelection): readonly string[] {
const expansions = selection.expansions ?? ['ix'];
requireBetrayal(Array.isArray(expansions) && expansions.length >= 1 && expansions.length <= 2 &&
expansions.includes('ix') && expansions.every((expansion, index) =>
(expansion === 'ix' || expansion === 'choam') && expansions.indexOf(expansion) === index) &&
(selection.ecazTreachery === undefined || typeof selection.ecazTreachery === 'boolean'),
'Ixian Betrayal requires an original selected Ixian deck, optionally CHOAM and independent cards.');
return expansions;
}
function canonicalCard(value: unknown, canonical: ReadonlyMap<string, Card>): value is Card {
if (!plain(value) || !text(value.id)) return false;
const expected = canonical.get(value.id);
return !!expected && exact(value, ['id', 'name', 'kind',
...(expected.effect === undefined ? [] : ['effect'])]) &&
value.name === expected.name && value.kind === expected.kind && value.effect === expected.effect;
}
function validatePhysical(cards: readonly Card[]): void {
function validatePhysical(cards: readonly Card[], selection: IxianBetrayalPhysicalSelection = {}): void {
const expansions = selectedExpansions(selection);
requireBetrayal((selection.richese === undefined || typeof selection.richese === 'boolean') &&
(!selection.richese || expansions.includes('choam')),
'Ixian Betrayal needs the selected CHOAM deck for its native Richese cache.');
const canonical = new Map([
...treacheryDeck(expansions),
...(selection.ecazTreachery ? ecazTreacheryCards() : []),
...(selection.richese ? richeseCards() : []),
].map(card => [card.id, card]));
requireBetrayal(Array.isArray(cards) && cards.length === canonical.size &&
cards.every(canonicalCard) && new Set(cards.map(card => card.id)).size === canonical.size,
'Ixian Betrayal requires the complete canonical Ixian deck in unique physical custody.');
cards.every(card => canonicalCard(card, canonical)) && new Set(cards.map(card => card.id)).size === canonical.size,
'Ixian Betrayal requires the complete canonical selected deck and native cache in unique physical custody.');
}
function validateRoster(players: readonly NexusPlayer[]): void {
function validateRoster(players: readonly NexusPlayer[], selection: IxianBetrayalPhysicalSelection = {}): void {
const expansions = selectedExpansions(selection);
validateNexusPlayers(players);
requireBetrayal(players.every(player => Object.hasOwn(permitted, player.faction)) &&
players.filter(player => player.faction === 'ixians').length === 1,
'Ixian Betrayal needs its native Ixian and permitted classic or Tleilaxu seats.');
requireBetrayal(players.every(player => FACTIONS.some(faction => faction.id === player.faction &&
(faction.expansion === 'base' || expansions.includes(faction.expansion)))) &&
players.filter(player => player.faction === 'ixians').length === 1 &&
(selection.richese === undefined || selection.richese === players.some(player => player.faction === 'richese')),
'Ixian Betrayal needs its native Ixian and registered classic or selected E1/E2 seats.');
}
/** A neutral acknowledgment depends on public held-card presence, never its face. */
export function ixianBetrayalResponders(cards: NexusState, players: readonly NexusPlayer[]): string[] {
validateRoster(players);
export function ixianBetrayalResponders(cards: NexusState, players: readonly NexusPlayer[],
selection: IxianBetrayalPhysicalSelection = {}): string[] {
validateRoster(players, selection);
validateNexusCards(cards, players);
return players.filter(player => !player.ally && player.faction !== 'ixians' &&
cards.hands[player.id] !== null).map(player => player.id);
}
export function ixianBetrayalEligible(
cards: NexusState, players: readonly NexusPlayer[], holder: string,
selection: IxianBetrayalPhysicalSelection = {},
): boolean {
validateRoster(players);
validateRoster(players, selection);
validateNexusCards(cards, players);
const seat = players.find(player => player.id === holder);
return !!seat && !seat.ally && seat.faction !== 'ixians' && cards.hands[holder] === 'ixians' &&
@@ -167,9 +198,9 @@ function validateContext(ctx: IxianBetrayalContext): void {
requireBetrayal(plain(ctx) && ctx.status === 'playing' && ctx.phase === 3 && whole(ctx.turn) &&
ctx.turn > 0 && whole(ctx.sequence) && typeof ctx.advanced === 'boolean',
'Ixian Betrayal needs its current Bidding phase and independent cursor.');
validateRoster(ctx.players);
validateRoster(ctx.players, ctx);
validateNexusCards(ctx.cards, ctx.players);
validatePhysical(ctx.physicalCards);
validatePhysical(ctx.physicalCards, {...ctx, richese: ctx.players.some(player => player.faction === 'richese')});
}
export function validateIxianBetrayalSource(
ctx: IxianBetrayalContext, source: IxianBetrayalSource,
@@ -187,7 +218,7 @@ export function validateIxianBetrayalSource(
source.nativeWindow === authority.nativeWindow && source.nativeContext === authority.nativeContext &&
sameMembers(source.nativeRequired, authority.nativeRequired) &&
sameMembers(source.nativePassed, authority.nativePassed) &&
sameMembers(source.required, ixianBetrayalResponders(ctx.cards, ctx.players)) &&
sameMembers(source.required, ixianBetrayalResponders(ctx.cards, ctx.players, ctx)) &&
source.eligible === (ctx.players.find(player => !player.ally && player.faction !== 'ixians' &&
ctx.cards.hands[player.id] === 'ixians')?.id ?? null),
'Ixian Betrayal no longer matches its current native attempt, parent or public responders.');
@@ -201,7 +232,7 @@ export function createIxianBetrayalSource(
...authority, nativeRequired: [...authority.nativeRequired], nativePassed: [...authority.nativePassed],
version: 1, event: ixianBetrayalEvent(ctx.turn, ctx.sequence, authority.kind, authority.provider),
turn: ctx.turn, phase: 3, advanced: ctx.advanced, sequence: ctx.sequence,
required: ixianBetrayalResponders(ctx.cards, ctx.players),
required: ixianBetrayalResponders(ctx.cards, ctx.players, ctx),
eligible: ctx.players.find(player => !player.ally && player.faction !== 'ixians' &&
ctx.cards.hands[player.id] === 'ixians')?.id ?? null,
parent, signature: '',
@@ -262,11 +293,12 @@ export function closeIxianBetrayalSource(
export function validateIxianBetrayalHistory(
history: readonly IxianBetrayalReceipt[], cursor: IxianBetrayalCursor,
physical?: readonly Card[],
selection: IxianBetrayalPhysicalSelection = {},
): void {
validateCursor(cursor);
requireBetrayal(array(history) && history.length === cursor.sequence,
'Ixian Betrayal history has missing or duplicate closed attempts.');
if (physical !== undefined) validatePhysical(physical);
if (physical !== undefined) validatePhysical(physical, selection);
let previous = genesis, turn = 0;
const events = new Set<string>();
for (const [index, receipt] of history.entries()) {
+3 -3
View File
@@ -4682,7 +4682,7 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'A separate twelve-card deck rewards remaining unallied. Private draws and selected Atreides, Harkonnen, Tleilaxu, Ixian, Bene Gesserit, Emperor, Fremen, CHOAM, Moritani, Richese and Guild effects are connected. Other effects and complete module play remain in development.',
steps: [
'This optional module can be combined with Basic or Advanced rules independently of the factions and other expansion modules selected. Include one Nexus Card for each of the twelve factions.',
'Fresh classic2–6 Basic/Advanced Nexus core admits base33 or selected independent36, optionally all14, original Homeworlds, Tech3+ and Advanced Strongholds2+. Actual Treachery, twelve Nexus, fourteen Skills, three Tech and six held-card stocks remain distinct; no selection is silently added. Original fresh Guild/Harkonnen/Ixian states use actual role/card authority. Starting hands/training, real closing draws, first-Storm tokens and end-Mentat claims stay native. Other families, Discovery/Semuta and reaction envelopes remain separate.',
'Fresh classic2–6 Nexus core admits base33/independent36. Selected Ixian, CHOAM or combined decks also compose the independent three-card variant with Nexus12, optionally all14, original Homeworlds, Tech3+ and Advanced Strongholds2+. A selected deck need not seat its native faction. Ordinary stock is38/50 plus any native Richese cache10. Actual native Ixians get Betrayal; only their absence permits purchased-card Secret Ally replacement. Signed Richese two-marker shipments and actual Guild fees retain original continuation. Other E3, Discovery/Semuta and reaction envelopes remain separate; this is not complete Nexus acceptance.',
'Recruits changes native Revival rates and the ordinary seven ceiling, not the fixed Tleilaxu Secret Ally group or its one-spice price. Five paid card counters may leave two ordinary free returns, using the one leader slot once. Emperor Cunning still makes five ordinary board counters temporarily Sardaukar: independent reserve fees use separate physical reserves, Harass returns actual undialed counters, and native Karama changes only the original Nexus effect. Native winner/inspection and Collection continue without extra cleanup prompts.',
'At the end of the entire Spice Blow and Nexus phase, if a Nexus occurred and at least one alliance exists, each unallied player may draw one card or discard their held card and draw a replacement. You may decline. The two Advanced spice piles do not grant two closing draws.',
'Keep your card secret until it is used. You may hold at most one Nexus Card. It is separate from your Treachery hand and does not occupy a Treachery hand slot.',
@@ -4752,11 +4752,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:"Native cross/return permission and rate remain separate; Cunning preserves its opportunity. Independent typed purchases and explicit immune Junction source choice retain their original tariffs/payees. Richese Secret Ally now keeps one billed counter and all five physical forces through native Guild rate allowance/cancellation; signed pending/settled sources stay bound. Unfunded release retains spent Nexus without debt/refund/reusable coupon and preserves ordinary shipment. Marker approvals still cannot silently increase; full acceptance remains 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", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts", "game/junction-transport-options.ts"]},
{area:"Implementation",status:'Partial',detail:"Selected E1/E2 independent/Nexus now shares fresh setup and original native card/reaction consumers, including typed Ix fees/Harass/substitution, CHOAM fuel and Richese paired invoices. Native cross/return permission and rate remain separate; Cunning preserves its opportunity. Independent typed purchases and explicit immune Junction source choice retain their original tariffs/payees. Richese Secret Ally now keeps one billed counter and all five physical forces through native Guild rate allowance/cancellation; signed pending/settled sources stay bound. Unfunded release retains spent Nexus without debt/refund/reusable coupon and preserves ordinary shipment. Marker approvals still cannot silently increase; full acceptance remains open.",evidence:['game/faction-module-profile.ts','tests/selected-independent-nexus-entry.test.ts','tests/fixture-selected-independent-nexus-entry.ts','tests/selected-independent-nexus-battles.test.ts','tests/fixture-selected-independent-nexus-battles.ts','tests/selected-independent-nexus-cards.test.ts','tests/fixture-selected-independent-nexus-cards.ts','tests/selected-independent-ixian-betrayal.test.ts','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", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts", "game/junction-transport-options.ts"]},
{area:"Player controls",status:'Partial',detail:"Four current original-action390px journeys prove native Junction5/full2, poor1/full2 release plus ordinary one-force replacement, still-held source sand2 disabled then five-force stronghold1, and independent purchased1. The response explains the spent-card consequence. No overflow, browser error or application exception; two tool-only stale-node errors were resolved by fresh evidence without replay. Earlier Junction/loan/refund surfaces remain scoped evidence.",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", "components/junction-transport.tsx"]},
{area:"AI",status:'Partial',detail:"All four original policies execute a legal still-held Richese full-rate one-counter alternative after native cancellation, with actual physical delivery and card cost. Shared human/AI quotes no longer promise a canceled discount. Native permission alternatives, recipient-specific lender prices and immune Junction source choice remain. Optional non-reoffer is not forced. No strategic first-candidate pin, tuning or calibration.",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:"Richese printed one-counter pricing and native Guild eligibility stay distinct from absent-faction cancellation, physical force count and independent purchased price. Unfunded spent-card/no-refund release is selected existing no-debt composition, not a new publisher worked example. Immune Junction and earlier permission/refund/loan interpretations remain explicit. Full rules and public gates stay open.",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/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.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", "docs/JUNCTION_TRANSPORT_RULES.md", "docs/JUNCTION_TRANSPORT_RUNTIME.md"]},
{area:"Verification",status:'Partial',detail:"Classic core retains original World/Tech/Stronghold modules with base33/selected36 and optional14. Independent World cases use native setup, population-sensitive Recruits, typed foreign Harass and actual own-Kaitain Reinforcements/Cunning. The local fee reduces only its paid temporary roles; original count5 and real stars remain. Earlier phase/card proofs are dated in the canonical contract. Native-family/Discovery/Semuta envelopes, full-mode and comprehensive assurance remain open.",evidence:['tests/nexus-world-independent-cards.test.ts','tests/nexus-world-independent-battles.test.ts','tests/reinforcements-temporary-role.test.ts','tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', '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/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.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-empeLine truncated
{area:"Verification",status:'Partial',detail:"Selected independent entry, native battle/card and canonical native-Ix cancellation cases now join original action paths; frozen proof is recorded in the canonical Nexus contract. Classic core retains original World/Tech/Stronghold modules with base33/selected36 and optional14. Independent World cases use native setup, population-sensitive Recruits, typed foreign Harass and actual own-Kaitain Reinforcements/Cunning. The local fee reduces only its paid temporary roles; original count5 and real stars remain. Earlier phase/card proofs are dated in the canonical contract. Native-family/Discovery/Semuta envelopes, full-mode and comprehensive assurance remain open.",evidence:['tests/nexus-world-independent-cards.test.ts','tests/nexus-world-independent-battles.test.ts','tests/reinforcements-temporary-role.test.ts','tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', '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/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.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-empLine truncated
],
},
{