Prototype Emperor Nexus bank-auction purchase

This commit is contained in:
Quintine committed 2026-09-28 13:38:49 +10:00
1 parent 23ce406872
commit 1bda2ec31e
18 files changed
+916 -49

No files matched your search

+3 -1
View File
@@ -79,7 +79,7 @@ import { quoteSmugglerShipment } from './smuggler-shipment';
import { quoteSmugglerNoField } from './smuggler-no-field';
import { spiceBankerBattleMaximum, spiceBankerModeSupported } from './spice-banker';
import { nexusGuildSecretAllyAction, nexusGuildSecretAllyCanAct, nexusGuildSecretAllyQuote } from './nexus-guild-secret-ally-options';
import { nexusEmperorRevivalAction } from './nexus-emperor-secret-ally-options';
import { nexusEmperorPurchaseAction, nexusEmperorRevivalAction } from './nexus-emperor-secret-ally-options';
import { nexusFremenRevivalAction } from './nexus-fremen-revival-options';
import { nexusRicheseAction, nexusRicheseQuote } from './nexus-richese-options';
import { liveShipmentPromises, matchesShipment } from './shipment-promises';
@@ -3824,6 +3824,8 @@ export function botActions(g: GameView): Action[] {
const action = nexusEmperorRevivalAction(g, elite);
if (action) return [action];
}
const emperorPurchase = nexusEmperorPurchaseAction(g);
if (emperorPurchase) return [emperorPurchase];
const bureaucrat = g.bureaucrat?.pending;
if (bureaucrat) {
if (bureaucrat.owner !== g.me || g.decision?.kind !== 'bureaucratPayment' ||
+97 -17
View File
@@ -39,6 +39,7 @@ import { quoteNexusChoamBetrayal, validateNexusChoamBetrayal, nexusChoamBetrayal
import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySignature, validateEcazInquiry, type EcazInquiryReceipt } from './nexus-ecaz-inquiry';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { EMPEROR_NEXUS_REVIVALS, emperorNexusEvent, emperorNexusSignature, emperorNexusModeSupported, emperorNexusPools, emperorNexusRevivalElites, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
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';
import { truthKnowledgeOf } from './truthtrance-knowledge';
@@ -1056,6 +1057,8 @@ export type Game = {
nexusEcazInquiryLast?: { event: string };
nexusEmperorSecretHistory?: NexusEmperorRevival[];
nexusEmperorSecretEvents?: string[];
nexusEmperorPurchaseHistory?: EmperorNexusPurchase[];
nexusEmperorPurchaseEvents?: string[];
nexusFremenRevivalHistory?: FremenNexusRevival[];
nexusFremenRevivalEvents?: string[];
treacheryDiscardSequence?: number;
@@ -2093,10 +2096,42 @@ function playNexusBgBetrayal(g: Game, p: Player, action: Action) {
log(g, `${p.name} spent Bene Gesserit Nexus Betrayal to prevent ${getPlayer(g, record.target).name}'s Voice in this battle. The battle and any later Prescience remain available.`,
{ faction: p.faction, name: 'Bene Gesserit Nexus Betrayal' });
}
/** The public payment window must not depend on who privately holds Emperor Nexus. */
function emperorNexusUniformAuctionPayment(g: Game): boolean {
return !!g.nexusCards?.cards && !byFaction(g,'emperor') &&
emperorNexusModeSupported(g) && !g.ecazTreachery && !g.sandtrout &&
!!g.auction && !g.currentAuctionSale;
}
function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
const p = g.players.find(p => p.id === owner);
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor' || byFaction(g,'emperor')) return null;
const event = emperorNexusEvent(g.turn,g.phase,owner,g.nexusEmperorSecretHistory?.length ?? 0);
const purchasing = g.status === 'playing' && g.phase === 3 &&
g.decision?.kind === 'auctionPayment' && g.decision.player === owner &&
g.auction?.bidder === owner && !g.currentAuctionSale;
const event = purchasing
? emperorNexusPurchaseEvent(g.turn,owner,g.nexusEmperorPurchaseHistory?.length ?? 0,g.auction!.index,g.auction!.bid)
: emperorNexusEvent(g.turn,g.phase,owner,g.nexusEmperorSecretHistory?.length ?? 0);
let purchase: {blocked:string|null;price:number}|null = null;
if (purchasing) {
let purchaseBlock:string|null = null;
if (p.ally) purchaseBlock = 'Emperor Nexus Secret Ally requires an unallied buyer.';
else if (!emperorNexusModeSupported(g) || g.ecazTreachery || g.sandtrout)
purchaseBlock = 'Emperor Nexus purchase currently requires classic factions and Nexus alone.';
else if (g.response || g.truthtrance || g.phaseOpening || g.pendingKarama ||
g.pendingTreacheryDiscard || g.pendingNullentropy || g.pendingExchange ||
g.pendingRicheseGift || g.pendingRichesePurchaseIncome || g.pendingAmbassador ||
g.pendingCapture || g.battle || g.nexusTraitorPending || g.nexusCards?.phase?.stage === 'drawing')
purchaseBlock = 'Finish the current interaction before this auction payment.';
else {
try {
normalKaramaAuction(g,p,true);
quoteEmperorNexusPurchase(g.auction!.bid,p.spice,g.auction!.allyPayment ?? 0);
}
catch (error) { purchaseBlock = error instanceof Error ? error.message : 'The buyer cannot prove the full price.'; }
}
purchase = {blocked:purchaseBlock,price:g.auction!.bid};
}
let blocked:string|null = null;
if (p.ally) blocked = 'Emperor Nexus Secret Ally requires an unallied holder.';
else if (!emperorNexusModeSupported(g)) blocked = 'Emperor Nexus Secret Ally currently requires base factions and Nexus alone; expansion and other module combinations remain pending.';
@@ -2110,24 +2145,42 @@ function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
blocked = 'Emperor Nexus revival with free-revival suppression or expanded revival effects remains pending.';
const eliteOptions = emperorNexusRevivalElites(p,g.advanced);
if (!blocked && !eliteOptions.length) blocked = 'Emperor Nexus needs exactly three eligible forces in Tanks; a smaller return remains pending.';
return {event,revival:{blocked,eliteOptions},purchase:null};
return {event,revival:{blocked,eliteOptions},purchase};
}
function nexusEmperorSecretIntegrity(g:Game) {
const history = g.nexusEmperorSecretHistory, events = g.nexusEmperorSecretEvents;
if (history === undefined) {
requireRule(events === undefined,'Emperor Nexus has lost its saved use history.'); return;
if (history === undefined)
requireRule(events === undefined,'Emperor Nexus has lost its saved revival history.');
else {
requireRule(g.nexusCards?.cards && emperorNexusModeSupported(g) && Array.isArray(history) && history.length > 0 &&
Array.isArray(events) && events.length === history.length && new Set(events).size === events.length,
'Emperor Nexus has lost its physical module or independent use markers.');
for (const [index,record] of history.entries()) {
nexusRule(() => validateEmperorNexusRevival(g,record));
requireRule(record.sequence === index && events[index] === record.event,
'Emperor Nexus has duplicated or reordered a completed revival.');
}
const last = history.at(-1)!;
if (last.turn === g.turn) requireRule(g.nexusCards.cards.discard.includes('emperor'),
'Emperor Nexus has reopened its already spent physical card.');
}
requireRule(g.nexusCards?.cards && emperorNexusModeSupported(g) && Array.isArray(history) && history.length > 0 &&
Array.isArray(events) && events.length === history.length && new Set(events).size === events.length,
'Emperor Nexus has lost its physical module or independent use markers.');
for (const [index,record] of history.entries()) {
nexusRule(() => validateEmperorNexusRevival(g,record));
requireRule(record.sequence === index && events[index] === record.event,
'Emperor Nexus has duplicated or reordered a completed use.');
const purchases = g.nexusEmperorPurchaseHistory, purchaseEvents = g.nexusEmperorPurchaseEvents;
if (purchases === undefined)
requireRule(purchaseEvents === undefined,'Emperor Nexus has lost its saved purchase history.');
else {
requireRule(g.nexusCards?.cards && emperorNexusModeSupported(g) && !g.ecazTreachery && !g.sandtrout &&
Array.isArray(purchases) && purchases.length > 0 && Array.isArray(purchaseEvents) &&
purchases.length === purchaseEvents.length && new Set(purchaseEvents).size === purchaseEvents.length,
'Emperor Nexus purchase has lost its physical module or independent use markers.');
for (const [index,record] of purchases.entries()) {
nexusRule(() => validateEmperorNexusPurchase(g,record));
requireRule(record.sequence === index && purchaseEvents[index] === record.event,
'Emperor Nexus purchase has duplicated or reordered a completed use.');
}
const last = purchases.at(-1)!;
if (last.turn === g.turn) requireRule(g.nexusCards.cards.discard.includes('emperor'),
'Emperor Nexus purchase has reopened its already spent physical card.');
}
const last = history.at(-1)!;
if (last.turn === g.turn) requireRule(g.nexusCards.cards.discard.includes('emperor'),
'Emperor Nexus has reopened its already spent physical card.');
}
function playNexusEmperorRevive(g:Game,p:Player,action:Action) {
const offer = nexusEmperorSecretAllyOffer(g,p.id);
@@ -2147,6 +2200,31 @@ function playNexusEmperorRevive(g:Game,p:Player,action:Action) {
log(g,`${p.name} spent Emperor Nexus Secret Ally to return three forces${elite ? `, including ${elite} Fedaykin` : ''} from Tanks to reserves for free. Ordinary force and free-revival allowances are unchanged.`,
{faction:p.faction,name:'Emperor Nexus revival'});
}
function playNexusEmperorPurchase(g:Game,p:Player,action:Action) {
const offer = nexusEmperorSecretAllyOffer(g,p.id), purchase = offer?.purchase;
requireRule(purchase && !purchase.blocked && action.event === offer.event &&
Object.keys(action).sort().join(',') === 'event,type',
purchase?.blocked ?? 'Choose your current Emperor Nexus bank-auction payment.');
const auction = g.auction!;
normalKaramaAuction(g,p,true);
nexusRule(() => quoteEmperorNexusPurchase(auction.bid,p.spice,auction.allyPayment ?? 0));
const purchased = auction.cards[auction.index], auctionIndex = auction.index;
requireRule(purchased && auction.bidder === p.id && g.decision?.kind === 'auctionPayment' &&
g.decision.player === p.id,'The original winning bank-auction lot is no longer available.');
const beforeSpice = p.spice;
g.decision = null;
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!,p.id,g.players));
settleAuction(g,true,false,true);
const record:EmperorNexusPurchase = {kind:'purchase',event:offer.event,owner:p.id,turn:g.turn,
phase:3,sequence:g.nexusEmperorPurchaseHistory?.length ?? 0,faction:p.faction,
advanced:g.advanced,price:auction.bid,auctionIndex,card:purchased.id,beforeSpice,afterSpice:p.spice,signature:''};
record.signature = signEmperorNexusPurchase(record);
nexusRule(() => validateEmperorNexusPurchase(g,record));
(g.nexusEmperorPurchaseHistory ??= []).push(record);
(g.nexusEmperorPurchaseEvents ??= []).push(record.event);
log(g,`${p.name} spent Emperor Nexus Secret Ally after proving the ${record.price}-spice winning bid, bought the Treachery card and kept their own spice. The physical Nexus card was discarded.`,
{faction:p.faction,name:'Emperor Nexus purchase'});
}
function nexusEcazBetrayalIntegrity(g: Game) {
const history = g.nexusEcazBetrayalHistory;
if (history === undefined) return;
@@ -8614,7 +8692,7 @@ function auctionNext(g: Game) {
a.active = next.player;
g.active = a.active;
}
function settleAuction(g: Game, free = false, automatic = false) {
function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false) {
const a = g.auction!;
const winner = getPlayer(g, a.bidder!);
if (!free) payWithAlly(g, winner, a.bid, a.allyPayment ?? 0);
@@ -8629,7 +8707,7 @@ function settleAuction(g: Game, free = false, automatic = false) {
};
log(
g,
`${faction(winner.faction).name} won a treachery card for ${free ? 'a Karama' : `${a.bid} spice`}.${automatic ? ' The declared spice payment was the only available payment method.' : ''}`,
`${faction(winner.faction).name} won a treachery card for ${emperorNexus ? `the Emperor Nexus card after proving ${a.bid} spice` : free ? 'a Karama' : `${a.bid} spice`}.${automatic ? ' The declared spice payment was the only available payment method.' : ''}`,
automatic
? { faction: winner.faction, name: 'Auction payment' }
: undefined,
@@ -9909,7 +9987,8 @@ function finishAutomaticDecision(g: Game): boolean {
// split or consume a card on behalf of a player, including a BG conversion.
if (
auctionSpicePaymentFunded(g, p) &&
!p.hand.some((card) => !karamaSpendingBlock(g, p, card))
!p.hand.some((card) => !karamaSpendingBlock(g, p, card)) &&
!emperorNexusUniformAuctionPayment(g)
) {
g.decision = null;
settleAuction(g, false, true);
@@ -22328,6 +22407,7 @@ function applyActionInner(
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 === 'nexusEmperorPurchase') { playNexusEmperorPurchase(g,p,action); return g; }
if (t === 'nexusEmperorBetrayal') { playNexusEmperorBetrayal(g,p,action); return g; }
if (t === 'nexusBgBetrayal') { playNexusBgBetrayal(g,p,action); return g; }
if (t === 'nexusFremenRevive') { playNexusFremenRevive(g,p,action); return g; }
+139
View File
@@ -0,0 +1,139 @@
import { FACTIONS, type FactionId } from './catalog';
import { emperorNexusEvent } from './nexus-emperor-secret-ally';
export type EmperorNexusPurchase = {
kind: 'purchase';
event: string;
owner: string;
turn: number;
phase: 3;
sequence: number;
faction: FactionId;
advanced: boolean;
price: number;
auctionIndex: number;
card: string;
beforeSpice: number;
afterSpice: number;
signature: string;
};
/** Bind an offer to its public auction position and final bid, never its hidden card face. */
export function emperorNexusPurchaseEvent(
turn: number, owner: string, sequence: number, auctionIndex: number, price: number,
): string {
return `${emperorNexusEvent(turn, 3, owner, sequence)}:${auctionIndex}:${price}`;
}
export class EmperorNexusPurchaseError extends Error {
constructor(message: string) {
super(message);
this.name = 'EmperorNexusPurchaseError';
}
}
/** Only a personally affordable, bank-paid winning bid can be reimbursed. */
export function quoteEmperorNexusPurchase(
price: number,
ownSpice: number,
allyPayment: number,
): void {
if (
!Number.isSafeInteger(price) ||
price < 1 ||
!Number.isSafeInteger(ownSpice) ||
ownSpice < price ||
allyPayment !== 0
)
throw new EmperorNexusPurchaseError(
'Emperor Nexus purchase requires a positive whole bid paid entirely from the winner\'s own spice.',
);
}
/** Consistency signature, not cryptographic authentication; field order is fixed. */
export function signEmperorNexusPurchase(record: EmperorNexusPurchase): string {
return JSON.stringify([
'emperorNexusPurchase',
record.kind,
record.event,
record.owner,
record.turn,
record.phase,
record.sequence,
record.faction,
record.advanced,
record.price,
record.auctionIndex,
record.card,
record.beforeSpice,
record.afterSpice,
]);
}
const whole = (value: unknown): value is number =>
Number.isSafeInteger(value) && (value as number) >= 0;
const text = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0;
const plain = (value: unknown): value is Record<string, unknown> =>
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
[Object.prototype, null].includes(Object.getPrototypeOf(value));
/** Historical purchase evidence survives subsequent auctions and card transfers. */
export function validateEmperorNexusPurchase(
context: {
turn: number;
advanced: boolean;
players: readonly { id: string; faction: FactionId }[];
},
record: EmperorNexusPurchase,
): void {
const validContext =
context &&
whole(context.turn) &&
context.turn > 0 &&
typeof context.advanced === 'boolean' &&
Array.isArray(context.players) &&
context.players.length > 0 &&
context.players.every(
(player) =>
player &&
text(player.id) &&
FACTIONS.some((faction) => faction.id === player.faction),
) &&
new Set(context.players.map((player) => player.id)).size ===
context.players.length &&
new Set(context.players.map((player) => player.faction)).size ===
context.players.length;
if (
!validContext ||
!plain(record) ||
Object.keys(record).sort().join(',') !==
'advanced,afterSpice,auctionIndex,beforeSpice,card,event,faction,kind,owner,phase,price,sequence,signature,turn' ||
record.kind !== 'purchase' ||
record.phase !== 3 ||
!whole(record.turn) ||
record.turn < 1 ||
record.turn > context.turn ||
!whole(record.sequence) ||
!text(record.owner) ||
!text(record.card) ||
context.players.find((player) => player.id === record.owner)?.faction !==
record.faction ||
record.advanced !== context.advanced ||
record.event !==
emperorNexusPurchaseEvent(record.turn, record.owner, record.sequence, record.auctionIndex, record.price) ||
!whole(record.auctionIndex) ||
!whole(record.beforeSpice) ||
!whole(record.afterSpice) ||
!whole(record.price) ||
record.price < 1 ||
record.beforeSpice < record.price ||
record.afterSpice !== record.beforeSpice ||
record.signature !== signEmperorNexusPurchase(record)
)
throw new EmperorNexusPurchaseError(
'Emperor Nexus purchase has lost its original bidder, paid price or full reimbursement.',
);
}
+35
View File
@@ -29,3 +29,38 @@ export function nexusEmperorRevivalAction(
return null;
return { type: 'nexusEmperorRevive', event: offer.event, elite };
}
/** Spend the physical card only at the owner's completed, personally funded auction payment. */
export function nexusEmperorPurchaseAction(game: GameView): Action | null {
const offer = game.nexusEmperorSecretAlly;
const purchase = offer?.purchase;
const owner = game.players.find((player) => player.id === game.me);
if (
!offer ||
!purchase ||
purchase.blocked ||
!owner ||
owner.ally ||
game.nexusCards?.card !== 'emperor' ||
game.players.some((player) => player.faction === 'emperor') ||
game.status !== 'playing' ||
game.phase !== 3 ||
game.decision?.kind !== 'auctionPayment' ||
game.decision.player !== game.me ||
game.response ||
game.truthtrance ||
game.phaseOpening ||
game.automaticContinuationPending ||
game.nexusCards.waiting.length ||
game.nexusTraitors?.pending ||
!Number.isSafeInteger(purchase.price) ||
purchase.price < 1 ||
!game.auction ||
game.auction.bidder !== game.me ||
game.auction.bid !== purchase.price ||
(game.auction.allyPayment ?? 0) !== 0 ||
(owner.spice ?? 0) < purchase.price
)
return null;
return { type: 'nexusEmperorPurchase', event: offer.event };
}
+10 -10
View File
@@ -3979,25 +3979,25 @@ export const RULE_TOPICS: RuleTopic[] = [
},
{
id: 'nexus-emperor-secret-ally',
title: 'Emperor Nexus Secret Ally: extra revivals',
title: 'Emperor Nexus Secret Ally: extra revivals and bank-auction purchase',
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'When Emperor is absent, an unallied holder can spend its Nexus card during Revival to return exactly three additional forces free. The purchase alternative and combined-module acceptance remain unfinished.',
summary: 'When Emperor is absent, an unallied holder can spend its Nexus card on exactly three additional free force returns during Revival or on an ordinary bank-auction purchase after proving the full winning bid from personal spice. Seller-paid, allied, non-Bidding and combined-module acceptance remain unfinished.',
steps: [
'Use the private Emperor Secret Ally controls during Revival. Select the physical elite count when more than one legal group is available. The card returns exactly three eligible forces from Tanks to reserves and is discarded once.',
'These returns cost no spice and do not consume ordinary force-revival or free-revival allowances. You may use the card before or after ordinary revivals, while Revival remains open.',
'The Advanced one-per-turn Fedaykin limit still applies across ordinary revivals and this card. The extra total allowance does not permit another elite revival. Fewer than three eligible counters is an unresolved case and remains unavailable.',
'This prototype supports Basic and Advanced base factions with Nexus alone. Other expansion, Homeworld, suppression and combined-module effects retain their separate boundaries. Keeping the card creates no public decision or extra confirmation.',
'The purchase alternative is not yet playable. It requires a choice when buying without revealing concealed card eligibility; its uniform winner-payment interaction awaits a product decision.',
'The revival prototype supports Basic and Advanced base factions with Nexus alone. Other expansion, Homeworld, suppression and combined-module effects retain their separate boundaries. Keeping the card during Revival creates no public decision or extra confirmation.',
'At an eligible normal bank-auction winner payment, prove the positive final bid entirely from your own spice, then spend Emperor Nexus to receive that physical Treachery Card without losing spice. Every winner sees the same payment confirmation even if they hold no Nexus card; this privacy-preserving timing remains a provisional product choice pending the existing user question. Ordinary spice or legal Karama payment is still available.',
],
related: ['nexus-cards', 'revival'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Connected fixed three-force return with preserved ordinary allowances, physical elite limits and saved card-use receipts. Purchase and combined producers remain unfinished.', evidence: ['game/nexus-emperor-secret-ally.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Private optional revival controls, legal elite selection, unavailable reasons and readable card reference.', evidence: ['components/nexus-emperor-secret-ally.tsx', 'components/nexus-cards.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles use the same projected legal return; no rival hand or spice is consulted. Complete Nexus strategy remains uncalibrated.', evidence: ['game/nexus-emperor-secret-ally-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed grant, separate allowances, elite cap and purchase timing boundary are explicit.', evidence: ['docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused rules, controls, bots, JSON and SQLite restoration with stale, duplicate and concurrent actions. Broader module acceptance remains open.', evidence: ['tests/nexus-emperor-secret-ally.test.ts', 'tests/nexus-emperor-secret-ally-controls.test.tsx', 'tests/nexus-emperor-secret-ally-bots.test.ts', 'tests/nexus-emperor-secret-ally-recovery.test.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Fixed three-force return preserves ordinary allowances and physical elite limits. The separate bank-auction path retains the personally funded bid, transfers one physical lot and spends the Nexus with signed saved receipts. Seller/allied, other-purchase and combined producers remain unfinished.', evidence: ['game/nexus-emperor-secret-ally.ts', 'game/nexus-emperor-purchase.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Private revival/auction options, legal elite selection, bid proof and unavailable reasons with readable physical card reference; normal payment remains offered.', evidence: ['components/nexus-emperor-secret-ally.tsx', 'components/nexus-cards.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles use projected legal revival and personally funded bank-purchase choices without rival hand/spice inspection. Complete Nexus strategy remains uncalibrated.', evidence: ['game/nexus-emperor-secret-ally-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Printed alternatives, revival allowances, bid proof, physical purchase and provisional uniform payment timing retain explicit unresolved boundaries.', evidence: ['docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused Basic/Advanced rules, controls, bots, JSON, browser owner/rival projection and SQLite restart/CAS cover stale cross-lot offers, hand/custody rejection and duplicate requests; broader module acceptance remains open.', evidence: ['tests/nexus-emperor-secret-ally.test.ts', 'tests/nexus-emperor-secret-ally-recovery.test.ts', 'tests/nexus-emperor-purchase-engine.test.ts', 'tests/nexus-emperor-purchase-recovery.test.ts', 'tests/nexus-emperor-secret-ally-purchase-controls.test.ts'] },
],
},
{
@@ -4030,7 +4030,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Bene Gesserit Secret Ally borrowed Voice remains unavailable pending its cancellation and timing interpretation. Its printed permission does not make the other unfinished effects available.',
'Bene Gesserit Betrayal can prevent the native Voice while the ordinary pre-plan response is open. In Nexus games every seat responds even without a canceling card, so the window does not reveal the hidden holder. An unallied opponent spends one physical Bene Gesserit Nexus card to remove only that declared Voice; Prescience and plan commitments continue. Truthtrance has priority. This path does not grant borrowed Voice.',
'Emperor Cunning requires an Advanced battle with at least five ordinary Emperor counters and no actual Sardaukar, before submitting your plan. The played Nexus card opens one Karama response. Allowing it makes five ordinary counters count as Sardaukar for this battle; cancellation prevents the entire enhancement and leaves the Nexus card spent.',
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. The separate Secret Ally revival has its own connected controls; Emperor purchase modes remain unfinished.',
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. The separate Secret Ally revival and normal bank-auction purchase have connected controls; seller-paid and forced Emperor payments remain unfinished.',
'Emperor Betrayal has a battle alternative in Advanced play: an unallied opponent holding the Emperor Nexus card can spend it during the ordinary pre-plan Sardaukar response to suppress actual Sardaukar strength for this battle. Every seat receives the same response opportunity even with no card, keeping hidden custody private. The starred pieces remain physical Sardaukar; no card grants a new force or cancels their separate support benefit.',
'With Fremen absent, an unallied holder may spend Fremen Secret Ally during Revival to return exactly three eligible forces from Tanks to reserves at no spice cost. Unlike Emperor Secret Ally, these three consume the ordinary force and free-force allowances, including the Advanced one-elite cap. This first path requires base factions and Nexus alone, before any normal force returns, without combined revival effects. Partial returns, prior returns, higher free rates and separate sandworm protection remain unavailable.',
'Native Fremen Cunning has a bounded Nexus-only path: when a natural, accepted additional, or Advanced special-Karama summoned worm appears in a territory initially containing no forces, Fremen may spend their own Nexus card before the worm resolves. The special summon retains its original Spice Blow parent. After the applicable Nexus, choose some forces from one occupied desert territory and ride to a legal Arrakis destination without using normal movement. The choice appears even without the secret card, so declining does not disclose custody. Karama provisionally prevents only the additional remote ride; the original worm still resolves and the card remains spent. An interrupted existing Cunning control is restored, and a selected ride waits for the summoned Nexus without granting a second Cunning offer; Great Maker and combined optional modules await integration.',