Connect Emperor Nexus retention to original Richese seller lots
This commit is contained in:
1 parent
22948ff6e4
commit
d3e1c2f242
21 files changed
+1314
-67
No files matched your search
@@ -110,6 +110,7 @@ const decisions = {
|
||||
nexusFremenCunningOffer: true,
|
||||
nexusFremenProtection: true,
|
||||
nexusEmperorPayment: true,
|
||||
nexusEmperorSellerPurchase: true,
|
||||
nexusFremenCunningRide: true,
|
||||
moritaniRetention: true,
|
||||
moritaniSpecialKarama: true,
|
||||
|
||||
@@ -101,8 +101,9 @@ export function auctionEmperorIncomeAmount(
|
||||
: sale.richeseContribution ?? 0;
|
||||
}
|
||||
|
||||
/** Deterministic paid-sale continuation only. A returned response, Richese
|
||||
* transition or normal phase-end is a boundary, not a simulated future action.
|
||||
/** Deterministic original-sale continuation, including retained-price free lots.
|
||||
* A returned response, Richese transition or normal phase-end is a boundary,
|
||||
* not a simulated future action.
|
||||
* Never publish this internal quote: returned lot cards remain private. */
|
||||
export function quoteAuctionContinuation(
|
||||
input: AuctionContinuationInput,
|
||||
@@ -236,12 +237,12 @@ function calculate(
|
||||
requireAuction(
|
||||
s &&
|
||||
['cache', 'blackMarket'].includes(s.origin) &&
|
||||
s.free === false &&
|
||||
typeof s.free === 'boolean' &&
|
||||
count(s.amount) &&
|
||||
seated(s.winner) &&
|
||||
s.seller &&
|
||||
seated(s.seller)?.faction === 'richese',
|
||||
'The paid Richese sale receipt is malformed.',
|
||||
'The original Richese sale receipt is malformed.',
|
||||
);
|
||||
requireAuction(
|
||||
lot &&
|
||||
@@ -272,6 +273,20 @@ function calculate(
|
||||
: ['first', 'last'].includes(String(round.position))),
|
||||
'The paid Richese lot has a stale declaration round.',
|
||||
);
|
||||
requireAuction(
|
||||
lot.source !== 'blackMarket' || (input.advanced && s.seller !== s.winner),
|
||||
'Black Market requires an Advanced sale to another buyer; self-purchase awaits a payment and sale-counting ruling.',
|
||||
);
|
||||
requireAuction(
|
||||
!s.free || (
|
||||
s.amount > 0 && s.richeseContribution === undefined &&
|
||||
s.spiceBankerIncomePayment === undefined && s.bureaucratPayment === undefined &&
|
||||
s.bureaucratPaymentEvent === undefined &&
|
||||
(s.occupiedContributions === undefined ||
|
||||
s.occupiedContributions.every(contribution => contribution.amount === 0))
|
||||
),
|
||||
'A free Richese purchase cannot carry an actual payment, donor invoice or generated income.',
|
||||
);
|
||||
sale = { ...s, legacy: false };
|
||||
cardId = lot.cardId;
|
||||
}
|
||||
@@ -365,7 +380,7 @@ function calculate(
|
||||
next: AuctionContinuationQuote['next'],
|
||||
): AuctionContinuationQuote => ({ sale, steps, next });
|
||||
if (stage === 'sale') {
|
||||
if (sale.origin !== 'normal' && sale.seller !== winner.id) {
|
||||
if (!sale.free && sale.origin !== 'normal' && sale.seller !== winner.id) {
|
||||
if (sale.recipient !== 'bank') {
|
||||
const seller = seated(sale.seller!)!;
|
||||
const amount = sale.amount - (sale.richeseContribution ?? 0);
|
||||
|
||||
@@ -2693,6 +2693,9 @@ function policyActions(g: GameView): Action[] {
|
||||
if (d.kind === 'nexusEmperorPayment')
|
||||
return [{ type: 'nexusEmperorPayment', event: d.event,
|
||||
use: !!g.nexusEmperorPayment?.canUse && !g.nexusEmperorPayment.blocked }];
|
||||
if (d.kind === 'nexusEmperorSellerPurchase')
|
||||
return [{ type: 'nexusEmperorSellerPurchase', event: d.event,
|
||||
use: !!g.nexusEmperorSellerPurchase?.canUse && !g.nexusEmperorSellerPurchase.blocked }];
|
||||
if (d.kind === 'nexusFremenCunningOffer')
|
||||
return [{ type: 'decision', event: d.event,
|
||||
accept: g.nexusCards?.card === 'fremen' && me.faction === 'fremen' &&
|
||||
|
||||
+156
-25
@@ -45,7 +45,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, emperorNexusModeSupported, quoteEmperorNexusRevival, createEmperorNexusRevival, settleEmperorNexusRevival, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
|
||||
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, emperorNexusSellerPurchaseModeSupported, quoteEmperorNexusSellerPurchase, createEmperorNexusSellerPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
|
||||
import { createEmperorPaymentReceipt, validateEmperorPaymentReceipt, emperorPaymentUseBlock, emperorPaymentSourceQuote, requiredEmperorPaymentResponders, emperorPaymentReceiptSignature, type EmperorPaymentReceipt } from './nexus-emperor-payment';
|
||||
import { FREMEN_NEXUS_FREE_FORCES, fremenNexusRevivalOffer, createFremenNexusRevival, upgradeFremenNexusRevival, validateFremenNexusRevival, type FremenNexusRevival } from './nexus-fremen-revival';
|
||||
import { captureFremenCunningOccurrence, quoteFremenCunning, quoteFremenCunningRide, validateFremenCunningSelection, type FremenCunningOccurrence, type FremenCunningRideAuthorization, type FremenCunningSelection } from './nexus-fremen-cunning';
|
||||
@@ -934,6 +934,7 @@ export type Decision =
|
||||
| { kind: 'guildTiming'; player: string; next: string; following: string }
|
||||
| { kind: 'auctionPayment'; player: string }
|
||||
| { kind: 'nexusEmperorPayment'; player: string; event: string }
|
||||
| { kind: 'nexusEmperorSellerPurchase'; player: string; event: string }
|
||||
| {
|
||||
kind: 'advisor';
|
||||
player: string;
|
||||
@@ -1201,6 +1202,12 @@ export type Game = {
|
||||
/** Original normal-auction obligation; neutral acknowledgements precede funding. */
|
||||
nexusEmperorPayment?: { sequence: number; current: string | null; completed: EmperorPaymentReceipt[] };
|
||||
pendingNexusEmperorPayment?: { receipt: EmperorPaymentReceipt } | null;
|
||||
/** Uniform winning-buyer confirmation bound to the original unpaid seller lot. */
|
||||
pendingNexusEmperorSellerPurchase?: {
|
||||
event: string; turn: number; winner: string; round: string;
|
||||
lot: RicheseAuction; quote: Extract<RicheseSettlementQuote, {kind:'sold'}>;
|
||||
bank: boolean; signature: string;
|
||||
} | null;
|
||||
/** Explicit development profile; never enabled by a public start or player action. */
|
||||
kullPreview?: boolean;
|
||||
nexusKullPreview?: boolean;
|
||||
@@ -2789,12 +2796,15 @@ function nexusEmperorSecretIntegrity(g:Game) {
|
||||
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 &&
|
||||
requireRule(g.nexusCards?.cards &&
|
||||
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));
|
||||
if (!('version' in record))
|
||||
requireRule(emperorNexusModeSupported(g) && !g.ecazTreachery && !g.sandtrout,
|
||||
'A normal-bank Emperor purchase lost its original supported profile.');
|
||||
requireRule(record.sequence === index && purchaseEvents[index] === record.event,
|
||||
'Emperor Nexus purchase has duplicated or reordered a completed use.');
|
||||
}
|
||||
@@ -11756,17 +11766,32 @@ function richeseBetrayalIntegrity(g: Game): void {
|
||||
const sale = g.currentAuctionSale;
|
||||
const completed = cursor.completed.at(-1);
|
||||
if (completed && completed.lot === g.richeseAuction?.event) {
|
||||
const waiting = g.pendingNexusEmperorSellerPurchase;
|
||||
requireRule(g.status === 'playing' && g.phase === 3 && completed.turn === g.turn &&
|
||||
completed.round === g.richeseBidding?.event &&
|
||||
!(completed.stage === 'used' && completed.invoice.kind === 'purchase') &&
|
||||
sale && sale.winner === completed.invoice.buyer && sale.seller === completed.invoice.target &&
|
||||
sale.origin === completed.invoice.source && sale.amount === completed.invoice.price && sale.free === false &&
|
||||
sale.recipient === (completed.stage === 'used' ? 'bank' : undefined),
|
||||
'The completed Richese reaction lost or reopened its original paid-sale continuation.');
|
||||
const responses = savedNoFieldResponses(g).filter(response =>
|
||||
response.kind === 'emperorIncome' || response.kind === 'harkonnenBonus');
|
||||
requireRule(responses.length === 1,'The completed Richese sale lost its native income or bonus continuation.');
|
||||
currentAuctionContinuationQuote(g,{kind:'cancel',response:responses[0]});
|
||||
!(completed.stage === 'used' && completed.invoice.kind === 'purchase'),
|
||||
'The completed Richese reaction lost its original source.');
|
||||
if (waiting) {
|
||||
requireRule(!sale && waiting.round === completed.round && waiting.lot.event === completed.lot &&
|
||||
waiting.winner === completed.invoice.buyer && waiting.lot.owner === completed.invoice.target &&
|
||||
waiting.lot.source === completed.invoice.source && waiting.quote.amount === completed.invoice.price &&
|
||||
waiting.bank === (completed.stage === 'used'),
|
||||
'The original Richese reaction lost its suspended winning-buyer confirmation.');
|
||||
} else {
|
||||
const retained = sale?.free && g.nexusEmperorPurchaseHistory?.some(receipt =>
|
||||
'version' in receipt && receipt.version === 2 && receipt.lot === completed.lot &&
|
||||
receipt.round === completed.round && receipt.owner === completed.invoice.buyer &&
|
||||
receipt.seller === completed.invoice.target && receipt.origin === completed.invoice.source &&
|
||||
receipt.price === completed.invoice.price);
|
||||
requireRule(sale && sale.winner === completed.invoice.buyer && sale.seller === completed.invoice.target &&
|
||||
sale.origin === completed.invoice.source && sale.amount === completed.invoice.price &&
|
||||
(!sale.free || retained) && sale.recipient === (completed.stage === 'used' ? 'bank' : undefined),
|
||||
'The completed Richese reaction lost or reopened its original sale continuation.');
|
||||
const responses = savedNoFieldResponses(g).filter(response =>
|
||||
response.kind === 'emperorIncome' || response.kind === 'harkonnenBonus');
|
||||
requireRule(responses.length === 1,'The completed Richese sale lost its native income or bonus continuation.');
|
||||
currentAuctionContinuationQuote(g,{kind:'cancel',response:responses[0]});
|
||||
}
|
||||
}
|
||||
if (sale?.recipient) {
|
||||
const receipt = cursor.completed.at(-1);
|
||||
@@ -11859,19 +11884,104 @@ function settleRicheseLot(g: Game) {
|
||||
if (beginRicheseBetrayal(g,quote)) return;
|
||||
settleRicheseSoldLot(g,quote,false);
|
||||
}
|
||||
function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>, bank: boolean) {
|
||||
const EMPEROR_SELLER_POLICY = 'Provisional seller policy: retain your own full original price after proving it. The seller stays unpaid; no Spice Bank reimbursement, payer invoice or payment-derived income is created. Original card acquisition and native bonus/lot continuation still resolve.';
|
||||
function emperorSellerSource(frame: NonNullable<Game['pendingNexusEmperorSellerPurchase']>) {
|
||||
return { round: frame.round, lot: frame.lot, quote: frame.quote, bank: frame.bank };
|
||||
}
|
||||
function emperorSellerSourceSignature(g: Game, frame: NonNullable<Game['pendingNexusEmperorSellerPurchase']>) {
|
||||
const winner = getPlayer(g, frame.winner);
|
||||
return JSON.stringify([frame.event, frame.turn, frame.winner, frame.round,
|
||||
frame.lot, frame.quote, frame.bank, g.richeseBidding?.event,
|
||||
g.richeseBidding?.stage, g.richeseAuction, g.richeseFunding?.[frame.winner],
|
||||
aidFor(g, winner), winner.spice, winner.ally,
|
||||
getPlayer(g, frame.lot.owner).ally, g.nexusEmperorPurchaseHistory?.length ?? 0]);
|
||||
}
|
||||
function nexusEmperorSellerIntegrity(g: Game): void {
|
||||
const frame = g.pendingNexusEmperorSellerPurchase;
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const decision = g.decision?.kind === 'nexusEmperorSellerPurchase' ? g.decision :
|
||||
continuation?.kind === 'truthtranceDiscard' ? continuation.resume.decision : null;
|
||||
if (!frame) {
|
||||
requireRule(decision?.kind !== 'nexusEmperorSellerPurchase',
|
||||
'The original seller purchase lost its current winning-buyer confirmation.');
|
||||
return;
|
||||
}
|
||||
requireRule(Object.keys(frame).sort().join(',') === 'bank,event,lot,quote,round,signature,turn,winner' &&
|
||||
frame.turn === g.turn && typeof frame.event === 'string' && frame.event.length > 0 &&
|
||||
typeof frame.bank === 'boolean' && typeof frame.signature === 'string' &&
|
||||
emperorNexusSellerPurchaseModeSupported(g) && !g.pendingRicheseBetrayal &&
|
||||
decision?.kind === 'nexusEmperorSellerPurchase' && decision.player === frame.winner &&
|
||||
decision.event === frame.event && g.active === frame.winner &&
|
||||
frame.signature === emperorSellerSourceSignature(g, frame),
|
||||
'The original seller purchase lost its immutable lot, price, funding or winning buyer.');
|
||||
const quote = nexusRule(() => quoteEmperorNexusSellerPurchase(g, emperorSellerSource(frame), frame.winner));
|
||||
requireRule(quote.event === frame.event &&
|
||||
JSON.stringify(frame.quote) === JSON.stringify(richeseSettlementQuote(g)),
|
||||
'The original seller purchase lost its exact pre-acquisition source.');
|
||||
}
|
||||
function nexusEmperorSellerOffer(g: Game, id: string) {
|
||||
const frame = g.pendingNexusEmperorSellerPurchase;
|
||||
if (!frame || g.decision?.kind !== 'nexusEmperorSellerPurchase' ||
|
||||
g.decision.player !== id || frame.winner !== id) return null;
|
||||
const quote = nexusRule(() => quoteEmperorNexusSellerPurchase(g, emperorSellerSource(frame), id));
|
||||
const blocked = g.truthtrance || pendingNexusTraitors(g) || g.pendingTreacheryDiscard
|
||||
? 'Finish the current question or private return before confirming this purchase.'
|
||||
: quote.blocked;
|
||||
return { ...quote, blocked, canUse: !blocked, policy: EMPEROR_SELLER_POLICY };
|
||||
}
|
||||
function beginNexusEmperorSellerPurchase(g: Game, quote: Extract<RicheseSettlementQuote, {kind:'sold'}>, bank: boolean): boolean {
|
||||
if (quote.amount <= 0 || !emperorNexusSellerPurchaseModeSupported(g)) return false;
|
||||
requireRule(!g.pendingNexusEmperorSellerPurchase && !g.pendingRicheseBetrayal &&
|
||||
!g.response && !g.decision && !g.truthtrance && !g.phaseOpening &&
|
||||
!g.pendingKarama && !g.pendingTreacheryDiscard && !pendingNexusTraitors(g),
|
||||
'Finish the original reaction before confirming the seller purchase.');
|
||||
const input = { round: g.richeseBidding!.event, lot: g.richeseAuction!, quote, bank };
|
||||
const offer = nexusRule(() => quoteEmperorNexusSellerPurchase(g, input, quote.winner));
|
||||
const frame: NonNullable<Game['pendingNexusEmperorSellerPurchase']> = {
|
||||
event: offer.event, turn: g.turn, winner: quote.winner, round: input.round,
|
||||
lot: structuredClone(input.lot), quote, bank, signature: '',
|
||||
};
|
||||
frame.signature = emperorSellerSourceSignature(g, frame);
|
||||
g.pendingNexusEmperorSellerPurchase = frame;
|
||||
g.active = frame.winner;
|
||||
g.decision = { kind: 'nexusEmperorSellerPurchase', player: frame.winner, event: frame.event };
|
||||
return true;
|
||||
}
|
||||
function decideNexusEmperorSellerPurchase(g: Game, p: Player, action: Action): void {
|
||||
const frame = g.pendingNexusEmperorSellerPurchase!;
|
||||
requireRule(action.type === 'nexusEmperorSellerPurchase' && action.event === frame.event &&
|
||||
typeof action.use === 'boolean' && Object.keys(action).sort().join(',') === 'event,type,use' &&
|
||||
p.id === frame.winner && g.decision?.kind === 'nexusEmperorSellerPurchase' &&
|
||||
g.decision.player === p.id && !g.truthtrance && !pendingNexusTraitors(g),
|
||||
'Confirm only your current original seller purchase with Pay or Use.');
|
||||
nexusEmperorSellerIntegrity(g);
|
||||
const receipt = action.use
|
||||
? nexusRule(() => createEmperorNexusSellerPurchase(g, emperorSellerSource(frame)))
|
||||
: null;
|
||||
if (receipt) {
|
||||
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
|
||||
(g.nexusEmperorPurchaseHistory ??= []).push(receipt);
|
||||
(g.nexusEmperorPurchaseEvents ??= []).push(receipt.event);
|
||||
log(g, `${p.name} spent Emperor Nexus after proving the original ${receipt.price}-spice seller price. They retain their spice; the seller stays unpaid and no bank subsidy or payment invoice is created. Provisional seller interpretation.`);
|
||||
}
|
||||
g.pendingNexusEmperorSellerPurchase = null;
|
||||
g.decision = null;
|
||||
settleRicheseSoldLot(g, frame.quote, frame.bank, !!receipt, true);
|
||||
}
|
||||
function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>, bank: boolean, free = false, confirmed = false) {
|
||||
if (!confirmed && beginNexusEmperorSellerPurchase(g, quote, bank)) return;
|
||||
const lot = g.richeseAuction!, owner = getPlayer(g,lot.owner);
|
||||
const winner = getPlayer(g, quote.winner), card = quote.card;
|
||||
const richeseContribution = g.advanced && winner.id !== owner.id && winner.ally === owner.id
|
||||
const richeseContribution = !free && g.advanced && winner.id !== owner.id && winner.ally === owner.id
|
||||
? quote.allyPayment : 0;
|
||||
const bankAmount = bank || winner.id === owner.id ? quote.amount : richeseContribution;
|
||||
const bankAmount = free ? 0 : bank || winner.id === owner.id ? quote.amount : richeseContribution;
|
||||
const emperor = byFaction(g, 'emperor');
|
||||
const incomePayment = g.spiceBankerIncomePreview && bankAmount > 0 ? bankerIncomePayment(g, 'auction',
|
||||
bank || winner.id === owner.id
|
||||
? [{payer:winner.id,amount:quote.ownPayment,recipient:bank || !emperor ? 'bank' : 'player'},
|
||||
...(quote.allyPayment ? [{payer:winner.ally!,amount:quote.allyPayment,recipient:bank || !emperor ? 'bank' as const : 'player' as const}] : [])]
|
||||
: [{payer:owner.id,amount:richeseContribution,recipient:emperor ? 'player' : 'bank'}]) : undefined;
|
||||
payWithAlly(g, winner, quote.amount, quote.allyPayment);
|
||||
if (!free) payWithAlly(g, winner, quote.amount, quote.allyPayment);
|
||||
g.richeseFunding = {};
|
||||
if (lot.source === 'cache')
|
||||
g.richeseCache = g.richeseCache!.filter((c) => c.id !== card.id);
|
||||
@@ -11879,29 +11989,29 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
|
||||
winner.hand.push(card);
|
||||
g.currentAuctionSale = {
|
||||
...(g.homeworldOccupationPreview ? { occupiedEvent: lot.event,
|
||||
occupiedContributions: [{ payer: winner.id, amount: quote.ownPayment },
|
||||
occupiedContributions: free ? [] : [{ payer: winner.id, amount: quote.ownPayment },
|
||||
...(quote.allyPayment ? [{ payer: winner.ally!, amount: quote.allyPayment }] : [])] } : {}),
|
||||
...(richeseContribution ? {richeseContribution} : {}),
|
||||
...(incomePayment ? {spiceBankerIncomePayment:incomePayment} : {}),
|
||||
winner: winner.id,
|
||||
amount: quote.amount,
|
||||
free: false,
|
||||
free,
|
||||
origin: lot.source,
|
||||
seller: owner.id,
|
||||
...(bank ? {recipient:'bank' as const} : {}),
|
||||
...stampAuctionBureaucrat(g,winner,quote.amount,quote.allyPayment,owner.id),
|
||||
...(!free ? stampAuctionBureaucrat(g,winner,quote.amount,quote.allyPayment,owner.id) : {}),
|
||||
};
|
||||
if (bank || !emperor) awardBankerIncome(g, incomePayment);
|
||||
log(
|
||||
g,
|
||||
`${winner.name} bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'} for ${quote.amount} spice. The funded bid was paid automatically.`,
|
||||
`${winner.name} bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'} for ${quote.amount} spice.${free ? ' Emperor Nexus retained the proven price; no seller or bank payment occurred.' : ' The funded bid was paid automatically.'}`,
|
||||
{ faction: winner.faction, name: 'Auction payment' },
|
||||
);
|
||||
const ixians = byFaction(g, 'ixians');
|
||||
if (lot.source === 'blackMarket' && ixians && winner.ally === ixians.id) {
|
||||
g.pendingIxAlly = { player: winner.id, card: card.id, free: false };
|
||||
g.pendingIxAlly = { player: winner.id, card: card.id, free };
|
||||
g.decision = { kind: 'ixAllyCard', player: winner.id };
|
||||
} else continueAuctionSale(g, false);
|
||||
} else continueAuctionSale(g, free);
|
||||
}
|
||||
function beginRicheseLot(
|
||||
g: Game,
|
||||
@@ -28599,6 +28709,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
nexusEmperorSellerIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -28689,9 +28800,10 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
if (g.pendingNexusIxianReplacement) return g;
|
||||
if (g.pendingGuildBetrayal) {guildBetrayalIntegrity(g); return g;}
|
||||
if (g.pendingRicheseBetrayal) {richeseBetrayalIntegrity(g);return g;}
|
||||
if (g.pendingNexusEmperorPayment) {
|
||||
if (g.pendingNexusEmperorPayment || g.pendingNexusEmperorSellerPurchase) {
|
||||
finishActionContinuations(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
nexusEmperorSellerIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
if (g.pendingKull) {
|
||||
@@ -28811,6 +28923,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
richesePairIntegrity(g);
|
||||
richeseBetrayalIntegrity(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
nexusEmperorSellerIntegrity(g);
|
||||
marketGholaIntegrity(g);
|
||||
homeworldRule(() => homeworldGameIntegrity(g));
|
||||
homeworldBattleLossIntegrity(g);
|
||||
@@ -28841,6 +28954,7 @@ function finishActionContinuations(g: Game) {
|
||||
if (g.pendingTreacheryDiscard?.reaction) return;
|
||||
requireRule(!g.pendingTreacheryDiscard, 'The automatic discard chain did not finish.');
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingNexusEmperorSellerPurchase) return;
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
observeOccupation(g);
|
||||
resumeHomeworldRevivalReturn(g);
|
||||
@@ -28901,6 +29015,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingGuildBetrayal) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingNexusEmperorSellerPurchase) return;
|
||||
if (g.pendingKull?.stage === 'offer') return;
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
if (g.pendingNullentropy) return;
|
||||
@@ -28911,6 +29026,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingNexusIxianReplacement) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingNexusEmperorSellerPurchase) return;
|
||||
syncTupileSlots(g);
|
||||
prepareTupileCleanup(g);
|
||||
if (g.pendingHomeworldTupileCleanup || tupileSourceBlock(g)) return;
|
||||
@@ -28970,6 +29086,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingGuildBetrayal) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingNexusEmperorSellerPurchase) return;
|
||||
// Opposing/automatic consequences can release a now-impossible promise;
|
||||
// they are not the original actor voluntarily spending a promised resource.
|
||||
reconcileBattlePromises(g);
|
||||
@@ -28994,6 +29111,7 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
nexusEmperorSellerIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -29034,9 +29152,10 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
if (g.pendingNexusIxianReplacement) return g;
|
||||
if (g.pendingGuildBetrayal) return g;
|
||||
if (g.pendingRicheseBetrayal) return g;
|
||||
if (g.pendingNexusEmperorPayment) {
|
||||
if (g.pendingNexusEmperorPayment || g.pendingNexusEmperorSellerPurchase) {
|
||||
finishActionContinuations(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
nexusEmperorSellerIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
if (g.pendingKull?.stage === 'offer') return g;
|
||||
@@ -29070,6 +29189,7 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
richesePairIntegrity(g);
|
||||
richeseBetrayalIntegrity(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
nexusEmperorSellerIntegrity(g);
|
||||
marketGholaIntegrity(g);
|
||||
prepareTupileCleanup(g);
|
||||
homeworldRule(() => homeworldGameIntegrity(g));
|
||||
@@ -29105,14 +29225,16 @@ function applyActionInner(
|
||||
'Finish the original occupied Tupile hand cleanup before another action.');
|
||||
ecazTreacheryIntegrity(state);
|
||||
const g = structuredClone(state);
|
||||
if (g.pendingNexusEmperorPayment && !g.truthtrance && !pendingNexusTraitors(g)) {
|
||||
if ((g.pendingNexusEmperorPayment || g.pendingNexusEmperorSellerPurchase) &&
|
||||
!g.truthtrance && !pendingNexusTraitors(g)) {
|
||||
const p = getPlayer(g, id);
|
||||
const opensTruth = action?.type === 'card' && !action.mode &&
|
||||
p.hand.some(card => card.id === action.card && (card.effect === 'truthtrance' ||
|
||||
Array.isArray(action.shrineTruthtrance) && action.shrineTruthtrance.includes(card.id)));
|
||||
if (!opensTruth) {
|
||||
if (action?.type === 'advanceBots') return g;
|
||||
decideNexusEmperorPayment(g, p, action);
|
||||
if (g.pendingNexusEmperorSellerPurchase) decideNexusEmperorSellerPurchase(g, p, action);
|
||||
else decideNexusEmperorPayment(g, p, action);
|
||||
return g;
|
||||
}
|
||||
}
|
||||
@@ -29295,6 +29417,11 @@ function applyActionInner(
|
||||
decideNexusEmperorPayment(g, p, action);
|
||||
return g;
|
||||
}
|
||||
if (g.pendingNexusEmperorSellerPurchase) {
|
||||
if (action.type === 'advanceBots') return g;
|
||||
decideNexusEmperorSellerPurchase(g, p, action);
|
||||
return g;
|
||||
}
|
||||
if (t === 'card' && action.card === RECRUITS_CARD_ID) {
|
||||
requireRule(
|
||||
Object.keys(action).sort().join(',') === 'card,type',
|
||||
@@ -29638,6 +29765,8 @@ function applyActionInner(
|
||||
'Use only the original worm protection event with Use or Pass.');
|
||||
requireRule(decision.kind !== 'nexusEmperorPayment',
|
||||
'Use only the original allied-purchase event with Use or Pass.');
|
||||
requireRule(decision.kind !== 'nexusEmperorSellerPurchase',
|
||||
'Use only the original seller-lot event with Pay or Use.');
|
||||
if (decision.kind === 'nexusIxianReplacement') {
|
||||
requireRule(false, 'Use only the current purchased-card event with Use or Pass.');
|
||||
}
|
||||
@@ -33075,6 +33204,7 @@ export function viewGame(state: Game, id: string) {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
nexusEmperorSellerIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -33518,6 +33648,7 @@ export function viewGame(state: Game, id: string) {
|
||||
nexusEmperorSecretAlly: nexusEmperorSecretAllyOffer(g,id),
|
||||
nexusEmperorBetrayal: nexusEmperorBetrayalOffer(g, id),
|
||||
nexusEmperorPayment: nexusEmperorPaymentOffer(g, id),
|
||||
nexusEmperorSellerPurchase: nexusEmperorSellerOffer(g, id),
|
||||
nexusBgBetrayal: nexusBgBetrayalOffer(g, id),
|
||||
nexusBorrowedVoice: nexusBorrowedVoiceOffer(g, id),
|
||||
nexusFremenRevival: fremenNexusRevivalOffer(g,id),
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { FACTIONS, type FactionId } from './catalog';
|
||||
import { handLimit, type Game } from './engine';
|
||||
import { emperorNexusEvent } from './nexus-emperor-secret-ally';
|
||||
import { nexusCardMode, validateNexusCards } from './nexus-cards';
|
||||
import {
|
||||
pairedNexusModulesProfile, singleE1E2NexusProfile, mixedE1E2NexusProfile, mixedE3NexusProfile,
|
||||
} from './nexus-module-profile';
|
||||
import {
|
||||
pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile,
|
||||
mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile,
|
||||
} from './leader-skill-profile';
|
||||
import type { RicheseAuction } from './richese-auction';
|
||||
import { quoteRicheseSettlement, type RicheseSettlementQuote } from './richese-settlement';
|
||||
import { quoteRicheseBetrayalInvoice } from './nexus-richese-betrayal';
|
||||
|
||||
export type EmperorNexusPurchase = {
|
||||
export type EmperorNexusBankPurchase = {
|
||||
kind: 'purchase';
|
||||
event: string;
|
||||
owner: string;
|
||||
@@ -18,6 +30,29 @@ export type EmperorNexusPurchase = {
|
||||
signature: string;
|
||||
};
|
||||
|
||||
export type EmperorNexusSellerPurchase = Omit<EmperorNexusBankPurchase, 'auctionIndex'> & {
|
||||
version: 2;
|
||||
origin: 'cache' | 'blackMarket';
|
||||
seller: string;
|
||||
round: string;
|
||||
lot: string;
|
||||
};
|
||||
export type EmperorNexusPurchase = EmperorNexusBankPurchase | EmperorNexusSellerPurchase;
|
||||
export type EmperorNexusSellerPurchaseInput = {
|
||||
round: string;
|
||||
lot: RicheseAuction;
|
||||
quote: Extract<RicheseSettlementQuote, { kind: 'sold' }>;
|
||||
bank: boolean;
|
||||
};
|
||||
|
||||
/** Original public source identity, never the cache/Black Market card face. */
|
||||
export function emperorNexusSellerPurchaseEvent(
|
||||
turn: number, owner: string, sequence: number, round: string, lot: string,
|
||||
source: 'cache' | 'blackMarket', price: number,
|
||||
): string {
|
||||
return JSON.stringify(['nexusEmperorSellerPurchase', turn, owner, sequence, round, lot, source, price]);
|
||||
}
|
||||
|
||||
/** 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,
|
||||
@@ -52,6 +87,13 @@ export function quoteEmperorNexusPurchase(
|
||||
|
||||
/** Consistency signature, not cryptographic authentication; field order is fixed. */
|
||||
export function signEmperorNexusPurchase(record: EmperorNexusPurchase): string {
|
||||
if ('version' in record)
|
||||
return JSON.stringify([
|
||||
'emperorNexusSellerPurchase', record.version, record.kind, record.event,
|
||||
record.owner, record.turn, record.phase, record.sequence, record.faction,
|
||||
record.advanced, record.origin, record.seller, record.round, record.lot,
|
||||
record.price, record.card, record.beforeSpice, record.afterSpice,
|
||||
]);
|
||||
return JSON.stringify([
|
||||
'emperorNexusPurchase',
|
||||
record.kind,
|
||||
@@ -106,9 +148,38 @@ export function validateEmperorNexusPurchase(
|
||||
context.players.length &&
|
||||
new Set(context.players.map((player) => player.faction)).size ===
|
||||
context.players.length;
|
||||
if (plain(record) && 'version' in record) {
|
||||
if (
|
||||
!validContext ||
|
||||
Object.keys(record).sort().join(',') !==
|
||||
'advanced,afterSpice,beforeSpice,card,event,faction,kind,lot,origin,owner,phase,price,round,seller,sequence,signature,turn,version' ||
|
||||
record.version !== 2 || 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) ||
|
||||
!text(record.round) || !text(record.lot) || !text(record.seller) ||
|
||||
!['cache', 'blackMarket'].includes(record.origin) ||
|
||||
context.players.some(player => player.faction === 'emperor') ||
|
||||
context.players.find(player => player.id === record.seller)?.faction !== 'richese' ||
|
||||
context.players.find(player => player.id === record.owner)?.faction !== record.faction ||
|
||||
(record.origin === 'blackMarket' && (!record.advanced || record.seller === record.owner)) ||
|
||||
record.advanced !== context.advanced ||
|
||||
!whole(record.price) || record.price < 1 ||
|
||||
!whole(record.beforeSpice) || record.beforeSpice < record.price ||
|
||||
!whole(record.afterSpice) || record.afterSpice !== record.beforeSpice ||
|
||||
record.event !== emperorNexusSellerPurchaseEvent(
|
||||
record.turn, record.owner, record.sequence, record.round, record.lot, record.origin, record.price,
|
||||
) ||
|
||||
record.signature !== signEmperorNexusPurchase(record)
|
||||
)
|
||||
throw new EmperorNexusPurchaseError(
|
||||
'Emperor Nexus seller purchase has lost its original source, buyer, price or retained spice.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!validContext ||
|
||||
!plain(record) ||
|
||||
!('auctionIndex' in record) ||
|
||||
Object.keys(record).sort().join(',') !==
|
||||
'advanced,afterSpice,auctionIndex,beforeSpice,card,event,faction,kind,owner,phase,price,sequence,signature,turn' ||
|
||||
record.kind !== 'purchase' ||
|
||||
@@ -137,3 +208,142 @@ export function validateEmperorNexusPurchase(
|
||||
'Emperor Nexus purchase has lost its original bidder, paid price or full reimbursement.',
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 (!g.nexusCards || g.players.some(p => p.faction === 'emperor') ||
|
||||
!g.players.some(p => p.faction === 'richese') || g.ecazTreachery || g.sandtrout)
|
||||
return false;
|
||||
if (pairedNexusModulesProfile(g) || singleE1E2NexusProfile(g) ||
|
||||
mixedE1E2NexusProfile(g) || mixedE3NexusProfile(g) ||
|
||||
(!!g.leaderSkills && (pairedNexusLeaderSkillsProfile(g) ||
|
||||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
|
||||
mixedE3NexusLeaderSkillsProfile(g)))) return true;
|
||||
return g.expansions.length === 1 && g.expansions[0] === 'choam' &&
|
||||
g.players.length >= 2 && g.players.length <= 6 &&
|
||||
new Set(g.players.map(p => p.faction)).size === g.players.length &&
|
||||
g.players.every(p => FACTIONS.some(f => f.id === p.faction &&
|
||||
(f.expansion === 'base' || f.expansion === 'choam'))) &&
|
||||
(!g.richeseBetrayalPreview || g.players.some(p => p.faction === 'choam')) &&
|
||||
!g.homeworlds && !g.leaderSkills && !g.discoveryEnabled && !g.discoveries &&
|
||||
!g.discoveryStash && !g.greatMaker && !g.techTokens && !g.strongholdCards &&
|
||||
!g.semutaPreview && !g.advancedPreview && !g.kullPreview && !g.nexusKullPreview &&
|
||||
!g.guildBetrayalPreview && !g.nexusIxianReplacementPreview &&
|
||||
!g.nexusIxianBetrayalPreview && !g.nexusHarkonnenBetrayalPreview;
|
||||
}
|
||||
|
||||
/** Pre-acquisition proof only. The caller owns timing, overlays and mutation;
|
||||
* recompute native funding/capacity and the exact original physical source. */
|
||||
export function quoteEmperorNexusSellerPurchase(
|
||||
g: Game, input: EmperorNexusSellerPurchaseInput, owner: string,
|
||||
): { event: string; price: number; seller: string; source: 'cache' | 'blackMarket'; blocked: string | null } {
|
||||
try {
|
||||
if (!plain(input) || Object.keys(input).sort().join(',') !== 'bank,lot,quote,round' ||
|
||||
!text(input.round) || typeof input.bank !== 'boolean' ||
|
||||
!plain(input.lot) || !plain(input.quote) ||
|
||||
Object.keys(input.lot).sort().join(',') !==
|
||||
'acted,active,bid,bidder,cardId,eligible,event,method,order,outcome,owner,passed,sealed,source,tieOrder' ||
|
||||
Object.keys(input.quote).sort().join(',') !== 'allyPayment,amount,card,kind,ownPayment,winner' ||
|
||||
input.quote.kind !== 'sold' ||
|
||||
g.status !== 'playing' || g.phase !== 3 || !whole(g.turn) || g.turn < 1 ||
|
||||
(input.lot.source === 'blackMarket' && !g.advanced) ||
|
||||
!g.richeseBidding || g.richeseBidding.turn !== g.turn ||
|
||||
g.richeseBidding.stage !== 'lot' || g.richeseBidding.event !== input.round ||
|
||||
g.richeseBidding.owner !== input.lot.owner ||
|
||||
(input.lot.source === 'cache'
|
||||
? !['first', 'last'].includes(String(g.richeseBidding.position))
|
||||
: g.richeseBidding.position !== null) ||
|
||||
!g.richeseAuction || JSON.stringify(input.lot) !== JSON.stringify(g.richeseAuction) ||
|
||||
g.auction || g.currentAuctionSale || input.quote.winner !== owner ||
|
||||
input.lot.active !== null || input.lot.bidder !== owner ||
|
||||
input.lot.bid !== input.quote.amount ||
|
||||
(input.bank && (owner === input.lot.owner || input.quote.amount < 1)))
|
||||
throw new EmperorNexusPurchaseError('Emperor Nexus purchase needs its unchanged original terminal Richese lot.');
|
||||
const buyer = g.players.find(p => p.id === owner),
|
||||
seller = g.players.find(p => p.id === input.lot.owner);
|
||||
if (!buyer || seller?.faction !== 'richese')
|
||||
throw new EmperorNexusPurchaseError('Emperor Nexus purchase lost its original buyer or Richese seller.');
|
||||
const funding = g.richeseFunding?.[owner];
|
||||
if (!plain(funding) || Object.keys(funding).sort().join(',') !== 'allyPayment,amount,donor' ||
|
||||
funding.donor !== (input.quote.allyPayment ? buyer.ally : null))
|
||||
throw new EmperorNexusPurchaseError('Emperor Nexus purchase lost its original funding commitment or donor.');
|
||||
const sourceCards = input.lot.source === 'cache' ? g.richeseCache ?? [] : seller.hand;
|
||||
const physicalCards = [
|
||||
...g.players.flatMap(p => p.hand), ...g.deck, ...g.discard,
|
||||
...(g.richeseCache ?? []), ...(g.richeseRemoved ?? []),
|
||||
...(g.ixSetupCards ?? []), ...(g.ixAuction?.cards ?? []),
|
||||
...(g.ornithopter ? [g.ornithopter.card] : []),
|
||||
];
|
||||
// This shared native source proof checks exact card shape/face, unique
|
||||
// custody, cache identity and the Black Market self-purchase boundary.
|
||||
quoteRicheseBetrayalInvoice({
|
||||
players: g.players, lot: input.lot, quote: input.quote,
|
||||
donor: input.quote.allyPayment ? buyer.ally : null,
|
||||
originalRecipient: owner === seller.id
|
||||
? g.players.find(p => p.faction === 'emperor')?.id ?? null : seller.id,
|
||||
sourceCards, physicalCards,
|
||||
});
|
||||
const native = quoteRicheseSettlement({
|
||||
lot: input.lot,
|
||||
owner: { id: seller.id, handCount: seller.hand.length, handLimit: handLimit(seller) },
|
||||
winner: { id: buyer.id, handCount: buyer.hand.length, handLimit: handLimit(buyer), spice: buyer.spice },
|
||||
card: sourceCards.find(c => c.id === input.lot.cardId),
|
||||
funding: g.richeseFunding?.[owner],
|
||||
allyCredit: buyer.ally && g.aid[buyer.ally]?.recipient === owner ? g.aid[buyer.ally] : null,
|
||||
});
|
||||
if (native.kind !== 'sold' || native.amount !== input.quote.amount ||
|
||||
native.winner !== input.quote.winner || native.ownPayment !== input.quote.ownPayment ||
|
||||
native.allyPayment !== input.quote.allyPayment)
|
||||
throw new EmperorNexusPurchaseError('Emperor Nexus purchase lost its exact original nominal price or funding split.');
|
||||
let blocked: string | null = null;
|
||||
if (!emperorNexusSellerPurchaseModeSupported(g))
|
||||
blocked = 'Emperor Nexus seller purchase requires a supported original Richese Nexus profile with Emperor absent.';
|
||||
else if (buyer.ally) blocked = 'Emperor Nexus Secret Ally requires an unallied buyer.';
|
||||
else if (!g.nexusCards?.cards) blocked = 'Hold the real Emperor Nexus card to retain your purchase price.';
|
||||
else {
|
||||
validateNexusCards(g.nexusCards.cards, g.players);
|
||||
if (g.nexusCards.cards.hands[owner] !== 'emperor' ||
|
||||
nexusCardMode('emperor', buyer.faction, g.players.map(p => p.faction)) !== 'secretAlly')
|
||||
blocked = 'Hold the real Emperor Nexus Secret Ally card to retain your purchase price.';
|
||||
else {
|
||||
try { quoteEmperorNexusPurchase(native.amount, buyer.spice, native.allyPayment); }
|
||||
catch (error) {
|
||||
if (!(error instanceof EmperorNexusPurchaseError)) throw error;
|
||||
blocked = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
event: emperorNexusSellerPurchaseEvent(g.turn, owner, g.nexusEmperorPurchaseHistory?.length ?? 0,
|
||||
input.round, input.lot.event, input.lot.source, native.amount),
|
||||
price: native.amount, seller: seller.id, source: input.lot.source, blocked,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof EmperorNexusPurchaseError) throw error;
|
||||
throw new EmperorNexusPurchaseError(error instanceof Error ? error.message : 'Malformed original Richese purchase.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepare consumed-source evidence before payment/card movement. It records
|
||||
* retained spice, not an actual paid invoice or a fabricated seller subsidy. */
|
||||
export function createEmperorNexusSellerPurchase(
|
||||
g: Game, input: EmperorNexusSellerPurchaseInput,
|
||||
): EmperorNexusSellerPurchase {
|
||||
if (!plain(input) || !plain(input.quote) || !text(input.quote.winner))
|
||||
throw new EmperorNexusPurchaseError('Emperor Nexus purchase needs its original sold source and buyer.');
|
||||
const quote = quoteEmperorNexusSellerPurchase(g, input, input.quote.winner);
|
||||
if (quote.blocked) throw new EmperorNexusPurchaseError(quote.blocked);
|
||||
const buyer = g.players.find(p => p.id === input.quote.winner)!;
|
||||
const receipt: EmperorNexusSellerPurchase = {
|
||||
version: 2, kind: 'purchase', event: quote.event, owner: buyer.id, turn: g.turn,
|
||||
phase: 3, sequence: g.nexusEmperorPurchaseHistory?.length ?? 0,
|
||||
faction: buyer.faction, advanced: g.advanced, origin: quote.source,
|
||||
seller: quote.seller, round: input.round, lot: input.lot.event,
|
||||
price: quote.price, card: input.quote.card.id,
|
||||
beforeSpice: buyer.spice, afterSpice: buyer.spice, signature: '',
|
||||
};
|
||||
receipt.signature = signEmperorNexusPurchase(receipt);
|
||||
validateEmperorNexusPurchase(g, receipt);
|
||||
return receipt;
|
||||
}
|
||||
+17
-15
@@ -4342,25 +4342,26 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
{
|
||||
id: 'nexus-emperor-secret-ally',
|
||||
title: 'Emperor Nexus Secret Ally: extra revivals and bank-auction purchase',
|
||||
title: 'Emperor Nexus Secret Ally: extra revivals and original purchases',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
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.',
|
||||
summary: 'With Emperor absent, an unallied holder can choose exactly three additional free returns or retain a personally affordable original purchase price. Classic bank auctions and original Richese cache/Advanced Black Market now have distinct connected paths. Allied funding, direct CHOAM/Ambassador purchases and complete combinations 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.',
|
||||
'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.',
|
||||
'The independent revival grant enters original pending revival. Under the visible provisional policy, native Tleilaxu prevention/income, typed reserves, Tech and eligible Homeworld handling remain; ordinary free-rate blocking does not reprice it. Keeping the card creates no extra public confirmation.',
|
||||
'At an eligible classic normal bank-auction winner payment, prove the positive final bid from your own spice and spend Emperor Nexus to receive the lot without losing spice. Every winner sees the same payment confirmation; the provisional recommendation is selected under the decision priority. Ordinary spice or legal Karama remains available.',
|
||||
'Original Richese cache and Advanced Black Market offer Pay or eligible Emperor Nexus Use after original Richese Betrayal. The provisional seller-unpaid policy retains your full personally affordable price without bank reimbursement. One original lot, native Harkonnen bonus and source successor continue; legal self-cache is included but self-Black remains prohibited. Original self-purchase forced discard cannot consume this Nexus or acquire a card.',
|
||||
],
|
||||
related: ['nexus-cards', 'revival'],
|
||||
checklist: [
|
||||
{ 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'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Independent extra-three enters original pending revival. Distinct classic bank and original Richese seller paths retain personally affordable price and one actual lot with signed source receipts. Original seller/stock/bonus/successor run with no invented reimbursement. Allied and other acquisition producers remain open.', evidence: ['game/nexus-emperor-secret-ally.ts', 'game/nexus-emperor-purchase.ts', 'game/auction-continuation-quote.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Private typed revival and public uniform buyer confirmations with eligible Use, retained-price proof and visible provisional seller-unpaid policy; original Pay and native reactions remain.', evidence: ['components/nexus-emperor-secret-ally.tsx', 'components/nexus-emperor-seller-purchase.tsx', 'components/nexus-cards.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Minimal profiles use projected legal revival and original personally funded purchase choices, or Pay, without inspecting rival private custody. Complete strategy/calibration remain unfinished.', evidence: ['game/nexus-emperor-secret-ally-options.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Printed alternatives, independent revival and original bank/Richese purchases distinguish provisional native prevention, uniform timing and seller-unpaid recommendation from publisher rulings. Other acquisition sources and complete combinations remain open.', evidence: ['docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Current seller10-file union108/108, types/lint/build and twenty original API profiles pass. Actual390px cache/Black/self-cache/diverted-cache Use proves unchanged wallets, one lot and native bonus/successor without overflow/errors. Prior bank/revival evidence remains; broader acceptance and assurance remain 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', 'tests/fixture-nexus-emperor-seller.ts', 'tests/nexus-emperor-seller-purchase.test.ts', 'tests/nexus-emperor-seller-runtime.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -4568,8 +4569,9 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'With BG absent, every fresh Nexus battle asks both combatants in attacker order before Prescience, Nexus inspection or sealed plans. Continue preserves the card. The actual unallied BG-card holder may spend it to require or forbid one canonical type against the opponent. Under the explicit provisional recommendation, original Karama can cancel this named Voice advantage; the Nexus stays spent, only Voice is removed and native preparation continues. Existing in-flight preparation is not retrofitted. Native compliance and special/mode restrictions remain; this does not complete other unfinished effects.',
|
||||
'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. Secret Ally revival and normal bank-auction purchase have connected controls; normal forced allied funding is the separate operation below. Seller-paid Secret Ally purchases 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. Secret Ally independent revival, classic bank and original Richese seller purchases have connected controls; compulsory allied funding remains the separate operation below. Other acquisition sources and full combinations remain open.',
|
||||
'Emperor Secret Ally revival selects exactly three real normal/starred counters for free, outside ordinary total/free and native allied-extra usage. Declaration spends one Nexus and enters original pending revival; no leader or absent native payer is granted. Under the visible provisional independent-source policy, native Tleilaxu may prevent it and actual free returns award income once; ordinary free-rate blocking and Recruits do not reprice or double this fixed grant. Denial keeps both spent cards without forces/income/Tech or refund. Native typed caps, reserve deposits, Axlotl and eligible newly returned Fedaykin placement continue. Tleilax ordinary Free Revival deployment is not extended to this extra source. Source receipts distinguish requested, returned and denied; old physical receipts never replay.',
|
||||
'Original Richese cache and Advanced Black Market sold lots offer every buyer Pay after original Richese Betrayal. An eligible unallied Emperor Nexus holder, with Emperor absent and full price personally affordable, may Use to keep that spice and leave seller/bank unpaid under the visible provisional recommendation. Actual source stock, hand limits, native Harkonnen bonus and successor continue once. Legal self-cache is included; self-Black remains prohibited. Original self-discard produces no acquisition or Emperor Nexus spend. Direct CHOAM/Ambassador purchases remain open.',
|
||||
'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.',
|
||||
'During an original positive normal-auction purchase by Emperor’s ally, possible unallied held-Nexus rivals acknowledge in storm order before winner payment, regardless of secret face. Only the current actual Emperor-card holder may Use; Pass keeps cards and original payment. Under the provisional conservative full-price interpretation, native Emperor covers the full original price, reusing prepaid aid and adding only missing own spice. The buyer keeps their own spice and acquires one original card. Native Emperor income remains separately cancelable without refund; original next-lot inspection continues. No excess gift, bank-created subsidy or future-income borrowing is granted. Underfunded/fully prepaid Use is unavailable; special seller lots and full combinations remain open.',
|
||||
'With Fremen absent, an actual unallied Fremen-card holder may enable a fixed ordinary free quota of three at a quiet Revival boundary, then use normal partial/typed revival controls. Activation moves no counters, leader or spice. Actual prior free use counts; paid returns keep their cost and total usage. Native total limits, higher free rates, typed caps, prevention, Homeworld deposits/deployment, Axlotl activity and once-only Tleilaxu income remain original. Recruits doubles its native rate, not this fixed quota. Useless or unknown-ledger grants are unavailable. This is the visibly provisional recommended interpretation; strict old physical receipts adopt metadata without replay. Independent sandworm protection remains the separate operation below.',
|
||||
@@ -4599,11 +4601,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Existing effects and compulsory funding remain. Emperor Secret Ally now declares a signed exact-three independent source into original pending revival, with requested/returned/denied physical receipts and no ordinary or native allied-extra usage. Native Tleilaxu prevention/income, typed reserves, once-only Tech and Homeworld handling continue. Cyborg caps and returned Fedaykin placement stay native; zero-cost extras are not Tleilax ordinary Free Revival. Legacy physical evidence never replays. Wider combinations, custody and seller purchases remain 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'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Extra-three retains its actual typed selector and labels the provisional independent-source policy. Original native Tleilaxu clearance shows the committed extra source and no-refund consequence. Actual390px Allow then ordinary3, native Prevent and one returned Fedaykin placement use production controls; receipt lifecycle follows actual settlement. Existing compulsory funding and other Nexus controls remain. Wider controls and complete-mode acceptance stay open.', evidence: ['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing minimal profiles retain shared legal grant actions and native responses. Extra-three now enters the original pending pipeline instead of directly moving forces, with actual current typed quote/caps. Genuine Easy classic activation commits the independent physical return; compulsory funding and Fremen ordinary quota remain legal. No strategic tuning or four-tier calibration is claimed; both remain deferred and unfinished.', evidence: ['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts'] },
|
||||
{area:'Documentation',status:'Partial',detail:'Common rules and twelve faces retain remaining custody, inquiry, nested Cunning and combinations. Contracts distinguish named borrowed Voice cancellation, independent worm protection, fresh/storm BG card exception, Fremen ordinary fixed quota and Emperor original full-price funding. Interpretations are visibly provisional, not publisher answers or separately answered numerical rulings. Special seller composition and full-mode gates remain.',evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md']},
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Extra-return affected16-file union266/266 and types/lint/build pass. Genuine classic/native-Tleilaxu/Ix/Fedaykin setup, casualties, worm/pact/deal and Revival exercise original Allow/Prevent/income, later ordinary returns, typed caps and actual Homeworld placement. Fourteen API profiles reach Movement, including native deposits and Basic/Advanced Fedaykin. Three real390px surfaces prove requested-before-return, extra3 then ordinary3 with unchanged separate ledgers, once-only income/Tech, denied no-return/no-refund, and one star to Tabr14. Original legal reinforcement geometry and Fremen zero-support plans are respected; fixture-only wrong assumptions/obsolete pins are removed. No overflow/errors. Prior funding155/155/quota329/329 evidence remains; wider combinations, assurance and deployed acceptance stay open.', evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md'] },
|
||||
{area:'Implementation',status:'Partial',detail:"Existing extra revival and compulsory funding remain. Emperor Secret Ally now retains personally affordable original Richese cache/Advanced Black Market price after original Betrayal, with strict signed source receipts. Seller/bank receive no invented reimbursement; actual stock, hand limits, Hark bonus and successor continue. Basic/Advanced cache and legal self-cache are included; self-Black stays blocked. Direct CHOAM/Ambassador purchases, wider combinations and assurance remain 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"]},
|
||||
{area:'Player controls',status:'Partial',detail:"Uniform winning-buyer Pay follows original Richese Betrayal regardless concealed face. Actual eligible holder sees Use with retained price and visible provisional seller-unpaid policy. Original Truthtrance discard and bonus controls continue. Actual390px cache, Black Market, self-cache and diverted cache prove usable controls without overflow/errors. Wider surfaces and complete-mode acceptance remain open.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx"]},
|
||||
{area:'AI',status:'Partial',detail:"Minimal profiles use the shared projected seller entitlement and original Pay otherwise, without inspecting rival private custody. Existing extra-return, compulsory funding and other Nexus adapters remain. Strategy and four-tier calibration remain deferred and unfinished.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
|
||||
{area:'Documentation',status:'Partial',detail:"Printed alternatives retain explicit partial gates. Original Richese seller composition now selects visibly provisional unpaid-seller recommendation under the decision priority, not publisher clarification or a numerical user ruling. Original Betrayal precedes buyer confirmation; direct CHOAM/Ambassador sources and full combinations remain 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/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md"]},
|
||||
{area:'Verification',status:'Partial',detail:"Seller affected10-file union108/108 and types/lint/build pass. Genuine original setup/worm/pact/draw and positive cache/Black Market sales exercise Pay/Use, source stock, actual Hark bonus, self-cache, Truthtrance and original Betrayal pass/diversion/self-discard. Actual390px four surfaces prove unchanged wallets, one original card and native source successor without overflow/errors. Prior extra266/266, funding155/155 and quota329/329 evidence remains. Full combinations, assurance and deployed acceptance remain open.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-controls.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts"]},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -91,6 +91,7 @@ const DECISIONS = {
|
||||
nexusFremenCunningOffer: true,
|
||||
nexusFremenProtection: true,
|
||||
nexusEmperorPayment: true,
|
||||
nexusEmperorSellerPurchase: true,
|
||||
nexusFremenCunningRide: true,
|
||||
moritaniRetention: true,
|
||||
moritaniSpecialKarama: true,
|
||||
|
||||
Reference in new issue
Block a user