Connect Emperor Nexus compulsory funding to native auction settlement
This commit is contained in:
1 parent
25996ec8a7
commit
022040972c
18 files changed
+1291
-16
No files matched your search
@@ -109,6 +109,7 @@ const decisions = {
|
||||
wormRide: true,
|
||||
nexusFremenCunningOffer: true,
|
||||
nexusFremenProtection: true,
|
||||
nexusEmperorPayment: true,
|
||||
nexusFremenCunningRide: true,
|
||||
moritaniRetention: true,
|
||||
moritaniSpecialKarama: true,
|
||||
|
||||
@@ -2690,6 +2690,9 @@ function policyActions(g: GameView): Action[] {
|
||||
? [{ type: 'nexusFremenProtection', event: offer.event, use: offer.canUse }]
|
||||
: [];
|
||||
}
|
||||
if (d.kind === 'nexusEmperorPayment')
|
||||
return [{ type: 'nexusEmperorPayment', event: d.event,
|
||||
use: !!g.nexusEmperorPayment?.canUse && !g.nexusEmperorPayment.blocked }];
|
||||
if (d.kind === 'nexusFremenCunningOffer')
|
||||
return [{ type: 'decision', event: d.event,
|
||||
accept: g.nexusCards?.card === 'fremen' && me.faction === 'fremen' &&
|
||||
|
||||
+150
@@ -46,6 +46,7 @@ import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySig
|
||||
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 { 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';
|
||||
import { createNexusFremenProtection, currentNexusFremenProtectionOwner, answerNexusFremenProtection, validateNexusFremenProtection, type NexusFremenProtectionFrame, type NexusFremenProtectionInput } from './nexus-fremen-protection';
|
||||
@@ -931,6 +932,7 @@ export type Decision =
|
||||
}
|
||||
| { kind: 'guildTiming'; player: string; next: string; following: string }
|
||||
| { kind: 'auctionPayment'; player: string }
|
||||
| { kind: 'nexusEmperorPayment'; player: string; event: string }
|
||||
| {
|
||||
kind: 'advisor';
|
||||
player: string;
|
||||
@@ -1195,6 +1197,9 @@ export type Game = {
|
||||
quote: Extract<RicheseSettlementQuote, {kind:'sold'}>;
|
||||
signature: string;
|
||||
} | null;
|
||||
/** Original normal-auction obligation; neutral acknowledgements precede funding. */
|
||||
nexusEmperorPayment?: { sequence: number; current: string | null; completed: EmperorPaymentReceipt[] };
|
||||
pendingNexusEmperorPayment?: { receipt: EmperorPaymentReceipt } | null;
|
||||
/** Explicit development profile; never enabled by a public start or player action. */
|
||||
kullPreview?: boolean;
|
||||
nexusKullPreview?: boolean;
|
||||
@@ -12632,6 +12637,112 @@ function stampAuctionBureaucrat(g:Game,p:Player,amount:number,allyPayment:number
|
||||
const {payee,shares} = auctionBureaucratParties(g,p,amount,allyPayment,seller);
|
||||
return payee ? stampBureaucratContributors(g,'auction',payee,shares,allyPayment > 0) : {};
|
||||
}
|
||||
const EMPEROR_PAYMENT_POLICY = 'Provisional full-price funding: Emperor covers this original allied purchase, reusing prepaid aid and adding only the missing spice. No extra gift or bank subsidy is created; original acquisition and native income continue.';
|
||||
function nexusEmperorPaymentIntegrity(g: Game): void {
|
||||
const cursor = g.nexusEmperorPayment, frame = g.pendingNexusEmperorPayment;
|
||||
if (!cursor) {
|
||||
requireRule(!frame && g.decision?.kind !== 'nexusEmperorPayment',
|
||||
'Emperor compulsory funding lost its original obligation.');
|
||||
return;
|
||||
}
|
||||
requireRule(g.nexusCards?.cards && Object.keys(cursor).sort().join(',') === 'completed,current,sequence' &&
|
||||
Number.isSafeInteger(cursor.sequence) && cursor.sequence >= 1 && Array.isArray(cursor.completed) &&
|
||||
cursor.sequence === cursor.completed.length + Number(!!frame),
|
||||
'Emperor compulsory funding lost its original occurrence history.');
|
||||
const events = new Set<string>();
|
||||
for (const receipt of cursor.completed) {
|
||||
nexusRule(() => validateEmperorPaymentReceipt(g, receipt));
|
||||
requireRule(receipt.stage !== 'pending' && !events.has(receipt.event),
|
||||
'Emperor compulsory funding repeated or reopened a completed obligation.');
|
||||
events.add(receipt.event);
|
||||
}
|
||||
if (!frame) {
|
||||
requireRule(cursor.current === null && g.decision?.kind !== 'nexusEmperorPayment',
|
||||
'Emperor compulsory funding lost its pending source.');
|
||||
return;
|
||||
}
|
||||
requireRule(Object.keys(frame).join(',') === 'receipt' &&
|
||||
frame.receipt.stage === 'pending' && cursor.current === frame.receipt.event &&
|
||||
!events.has(frame.receipt.event), 'Emperor compulsory funding lost its pending receipt.');
|
||||
nexusRule(() => validateEmperorPaymentReceipt(g, frame.receipt));
|
||||
const owner = frame.receipt.required[frame.receipt.passed.length];
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const decision = g.decision?.kind === 'nexusEmperorPayment' ? g.decision :
|
||||
continuation?.kind === 'truthtranceDiscard' ? continuation.resume.decision : null;
|
||||
requireRule(decision?.kind === 'nexusEmperorPayment' && decision.event === frame.receipt.event &&
|
||||
decision.player === owner && g.active === owner,
|
||||
'Emperor compulsory funding lost its original acknowledgement owner.');
|
||||
}
|
||||
function nexusEmperorPaymentOffer(g: Game, id: string) {
|
||||
const receipt = g.pendingNexusEmperorPayment?.receipt;
|
||||
if (!receipt || g.decision?.kind !== 'nexusEmperorPayment' || g.decision.player !== id ||
|
||||
g.nexusCards?.cards?.hands[id] !== 'emperor') return null;
|
||||
const blocked = nexusRule(() => emperorPaymentUseBlock(g, receipt, id));
|
||||
return { event: receipt.event, canUse: !blocked, blocked, policy: EMPEROR_PAYMENT_POLICY,
|
||||
price: receipt.price, emperor: receipt.emperor, winner: receipt.winner };
|
||||
}
|
||||
function beginNexusEmperorPayment(g: Game): boolean {
|
||||
if (!g.nexusCards?.cards) return false;
|
||||
const source = nexusRule(() => emperorPaymentSourceQuote(g));
|
||||
if (!source) return false;
|
||||
const last = g.nexusEmperorPayment?.completed.at(-1);
|
||||
if (last?.turn === g.turn && last.auctionIndex === source.auctionIndex &&
|
||||
last.card === source.card && last.winner === source.winner && last.price === source.price) return false;
|
||||
const required = nexusRule(() => requiredEmperorPaymentResponders(g));
|
||||
if (!required.length) return false;
|
||||
const receipt = nexusRule(() => createEmperorPaymentReceipt(g, crypto.randomUUID(), required));
|
||||
const cursor = g.nexusEmperorPayment ??= { sequence: 0, current: null, completed: [] };
|
||||
cursor.sequence++;
|
||||
cursor.current = receipt.event;
|
||||
g.pendingNexusEmperorPayment = { receipt };
|
||||
g.active = required[0];
|
||||
g.decision = { kind: 'nexusEmperorPayment', player: required[0], event: receipt.event };
|
||||
return true;
|
||||
}
|
||||
function decideNexusEmperorPayment(g: Game, p: Player, action: Action): void {
|
||||
const receipt = g.pendingNexusEmperorPayment!.receipt;
|
||||
requireRule(action.type === 'nexusEmperorPayment' && typeof action.use === 'boolean' &&
|
||||
Object.keys(action).sort().join(',') === 'event,type,use' && action.event === receipt.event &&
|
||||
receipt.required[receipt.passed.length] === p.id,
|
||||
'Acknowledge only your current original allied purchase.');
|
||||
requireRule(!g.truthtrance && !pendingNexusTraitors(g),
|
||||
'Finish Truthtrance and private Traitor returns before the auction reaction.');
|
||||
if (action.use) {
|
||||
const blocked = nexusRule(() => emperorPaymentUseBlock(g, receipt, p.id));
|
||||
requireRule(!blocked, blocked ?? 'This card cannot fund the original purchase.');
|
||||
}
|
||||
const passed = action.use ? receipt.passed : [...receipt.passed, p.id];
|
||||
const stage = action.use ? 'used' : passed.length === receipt.required.length ? 'passed' : 'pending';
|
||||
const next: EmperorPaymentReceipt = { ...receipt, passed, stage,
|
||||
owner: action.use ? p.id : null, signature: '' };
|
||||
next.signature = emperorPaymentReceiptSignature(next);
|
||||
nexusRule(() => validateEmperorPaymentReceipt(g, next));
|
||||
if (stage === 'pending') {
|
||||
g.pendingNexusEmperorPayment!.receipt = next;
|
||||
const owner = next.required[next.passed.length];
|
||||
g.active = owner;
|
||||
g.decision = { kind: 'nexusEmperorPayment', player: owner, event: next.event };
|
||||
return;
|
||||
}
|
||||
const source = nexusRule(() => emperorPaymentSourceQuote(g));
|
||||
requireRule(source, 'The original auction funding obligation is no longer present.');
|
||||
if (action.use) {
|
||||
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
|
||||
getPlayer(g, source.emperor).spice -= source.missing;
|
||||
const credit = g.aid[source.emperor];
|
||||
g.aid[source.emperor] = { recipient: source.winner,
|
||||
amount: (credit?.recipient === source.winner ? credit.amount : 0) + source.missing };
|
||||
g.auction!.allyPayment = source.price;
|
||||
log(g, `${p.name} spent Emperor Nexus Betrayal to compel the original ${source.price}-spice allied purchase. ${source.prepaid} prepaid spice is reused and Emperor supplies ${source.missing}; no extra gift or bank subsidy is created. Provisional full-price interpretation.`);
|
||||
}
|
||||
g.nexusEmperorPayment!.completed.push(next);
|
||||
g.nexusEmperorPayment!.current = null;
|
||||
g.pendingNexusEmperorPayment = null;
|
||||
g.active = source.winner;
|
||||
g.decision = null;
|
||||
if (action.use) settleAuction(g, false);
|
||||
else recoverAuctionPayment(g, getPlayer(g, source.winner));
|
||||
}
|
||||
function auctionNext(g: Game) {
|
||||
const a = g.auction!;
|
||||
const eligible = g.order.filter((id) => {
|
||||
@@ -12653,6 +12764,7 @@ function auctionNext(g: Game) {
|
||||
finishNormalBidding(g);
|
||||
return;
|
||||
}
|
||||
if (beginNexusEmperorPayment(g)) return;
|
||||
g.decision = { kind: 'auctionPayment', player: next.player };
|
||||
g.active = next.player;
|
||||
recoverAuctionPayment(g, getPlayer(g, next.player));
|
||||
@@ -28476,6 +28588,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
guildBetrayalIntegrity(state);
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -28566,6 +28679,11 @@ 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) {
|
||||
finishActionContinuations(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
if (g.pendingKull) {
|
||||
settleAutomaticContinuations(g);
|
||||
if (g.pendingKull) { kullIntegrity(g); marketGholaIntegrity(g); return g; }
|
||||
@@ -28682,6 +28800,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
nexusEcazDukeIntegrity(g);
|
||||
richesePairIntegrity(g);
|
||||
richeseBetrayalIntegrity(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
marketGholaIntegrity(g);
|
||||
homeworldRule(() => homeworldGameIntegrity(g));
|
||||
homeworldBattleLossIntegrity(g);
|
||||
@@ -28711,6 +28830,7 @@ function finishActionContinuations(g: Game) {
|
||||
finishTreacheryDiscard(g);
|
||||
if (g.pendingTreacheryDiscard?.reaction) return;
|
||||
requireRule(!g.pendingTreacheryDiscard, 'The automatic discard chain did not finish.');
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
observeOccupation(g);
|
||||
resumeHomeworldRevivalReturn(g);
|
||||
@@ -28770,6 +28890,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingNexusIxianReplacement) return;
|
||||
if (g.pendingGuildBetrayal) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingKull?.stage === 'offer') return;
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
if (g.pendingNullentropy) return;
|
||||
@@ -28779,6 +28900,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingGuildBetrayal) return;
|
||||
if (g.pendingNexusIxianReplacement) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
syncTupileSlots(g);
|
||||
prepareTupileCleanup(g);
|
||||
if (g.pendingHomeworldTupileCleanup || tupileSourceBlock(g)) return;
|
||||
@@ -28837,6 +28959,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingNexusIxianBetrayal) return;
|
||||
if (g.pendingGuildBetrayal) return;
|
||||
if (g.pendingRicheseBetrayal) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
// Opposing/automatic consequences can release a now-impossible promise;
|
||||
// they are not the original actor voluntarily spending a promised resource.
|
||||
reconcileBattlePromises(g);
|
||||
@@ -28860,6 +28983,7 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
guildBetrayalIntegrity(state);
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -28900,6 +29024,11 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
if (g.pendingNexusIxianReplacement) return g;
|
||||
if (g.pendingGuildBetrayal) return g;
|
||||
if (g.pendingRicheseBetrayal) return g;
|
||||
if (g.pendingNexusEmperorPayment) {
|
||||
finishActionContinuations(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
if (g.pendingKull?.stage === 'offer') return g;
|
||||
ornithopterIntegrity(g);
|
||||
lateDefenseIntegrity(g);
|
||||
@@ -28930,6 +29059,7 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
nexusEcazDukeIntegrity(g);
|
||||
richesePairIntegrity(g);
|
||||
richeseBetrayalIntegrity(g);
|
||||
nexusEmperorPaymentIntegrity(g);
|
||||
marketGholaIntegrity(g);
|
||||
prepareTupileCleanup(g);
|
||||
homeworldRule(() => homeworldGameIntegrity(g));
|
||||
@@ -28965,6 +29095,17 @@ 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)) {
|
||||
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);
|
||||
return g;
|
||||
}
|
||||
}
|
||||
if (g.pendingNexusHarkonnenBetrayal) {
|
||||
getPlayer(g,id);
|
||||
if (action?.type === 'advanceBots') return g;
|
||||
@@ -29139,6 +29280,11 @@ function applyActionInner(
|
||||
throw error;
|
||||
}
|
||||
requireRule(!pendingNexusTraitors(g), 'Finish the private Nexus card return before continuing play.');
|
||||
if (g.pendingNexusEmperorPayment) {
|
||||
if (action.type === 'advanceBots') return g;
|
||||
decideNexusEmperorPayment(g, p, action);
|
||||
return g;
|
||||
}
|
||||
if (t === 'card' && action.card === RECRUITS_CARD_ID) {
|
||||
requireRule(
|
||||
Object.keys(action).sort().join(',') === 'card,type',
|
||||
@@ -29480,6 +29626,8 @@ function applyActionInner(
|
||||
const decision = g.decision;
|
||||
requireRule(decision.kind !== 'nexusFremenProtection',
|
||||
'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.');
|
||||
if (decision.kind === 'nexusIxianReplacement') {
|
||||
requireRule(false, 'Use only the current purchased-card event with Use or Pass.');
|
||||
}
|
||||
@@ -32916,6 +33064,7 @@ export function viewGame(state: Game, id: string) {
|
||||
guildBetrayalIntegrity(state);
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
nexusEmperorPaymentIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
@@ -33358,6 +33507,7 @@ export function viewGame(state: Game, id: string) {
|
||||
nexusEcazDuke: nexusRule(() => quoteNexusEcazDuke(g, id)),
|
||||
nexusEmperorSecretAlly: nexusEmperorSecretAllyOffer(g,id),
|
||||
nexusEmperorBetrayal: nexusEmperorBetrayalOffer(g, id),
|
||||
nexusEmperorPayment: nexusEmperorPaymentOffer(g, id),
|
||||
nexusBgBetrayal: nexusBgBetrayalOffer(g, id),
|
||||
nexusBorrowedVoice: nexusBorrowedVoiceOffer(g, id),
|
||||
nexusFremenRevival: fremenNexusRevivalOffer(g,id),
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { Game } from './engine';
|
||||
|
||||
export type EmperorPaymentSource = {
|
||||
auctionIndex: number;
|
||||
card: string;
|
||||
winner: string;
|
||||
price: number;
|
||||
emperor: string;
|
||||
ally: string;
|
||||
allyPayment: number;
|
||||
aid: { recipient: string | null; amount: number };
|
||||
emperorSpice: number;
|
||||
};
|
||||
export type EmperorPaymentQuote = EmperorPaymentSource & {
|
||||
prepaid: number;
|
||||
missing: number;
|
||||
blocked: string | null;
|
||||
};
|
||||
/** Original obligation and public acknowledgement snapshot, not a new bid or sale. */
|
||||
export type EmperorPaymentReceipt = EmperorPaymentSource & {
|
||||
event: string;
|
||||
turn: number;
|
||||
phase: 3;
|
||||
order: string[];
|
||||
holders: string[];
|
||||
required: string[];
|
||||
/** Pass answers only; a Use owner is the next member of the original queue. */
|
||||
passed: string[];
|
||||
stage: 'pending' | 'passed' | 'used';
|
||||
owner: string | null;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
const object = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
const text = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0;
|
||||
const count = (value: unknown): value is number =>
|
||||
Number.isSafeInteger(value) && (value as number) >= 0;
|
||||
const ids = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every(text) && new Set(value).size === value.length;
|
||||
const sameIds = (a: readonly string[], b: readonly string[]): boolean =>
|
||||
a.length === b.length && a.every((id, index) => id === b[index]);
|
||||
function requirePayment(value: unknown, message: string): asserts value {
|
||||
if (!value) throw new Error(message);
|
||||
}
|
||||
|
||||
/** A visible conservative full-obligation policy, not a publisher timing ruling.
|
||||
* Prepaid matching Emperor aid is already out of their wallet. Reuse it before
|
||||
* taking only the missing credit from their own spice; never count future income.
|
||||
* The engine alone establishes that ordinary bidding has reached its winner. */
|
||||
export function emperorPaymentSourceQuote(g: Game): EmperorPaymentQuote | null {
|
||||
const auction = g.auction;
|
||||
if (g.status !== 'playing' || g.phase !== 3 || !count(g.turn) || g.turn === 0 ||
|
||||
!auction || g.currentAuctionSale || g.richeseAuction ||
|
||||
!Array.isArray(auction.cards) || !count(auction.index) ||
|
||||
auction.index >= auction.cards.length || !count(auction.bid) || auction.bid === 0 ||
|
||||
!text(auction.cards[auction.index]?.id) || !text(auction.bidder)) return null;
|
||||
const winner = g.players.find(player => player.id === auction.bidder);
|
||||
const emperors = g.players.filter(player => player.faction === 'emperor');
|
||||
const emperor = emperors[0];
|
||||
if (!winner || emperors.length !== 1 || !emperor || winner.id === emperor.id ||
|
||||
winner.ally !== emperor.id || emperor.ally !== winner.id ||
|
||||
!count(emperor.spice)) return null;
|
||||
const allyPayment = auction.allyPayment ?? 0;
|
||||
if (!count(allyPayment) || allyPayment > auction.bid) return null;
|
||||
const credit = g.aid?.[emperor.id];
|
||||
if (credit && (!object(credit) || !text(credit.recipient) || !count(credit.amount))) return null;
|
||||
const card = auction.cards[auction.index].id;
|
||||
// Sold earlier entries are aliases, not physical custody. A still-unsold
|
||||
// duplicate or a card already acquired by a seat cannot be this original lot.
|
||||
for (let index = auction.index + 1; index < auction.cards.length; index++)
|
||||
if (auction.cards[index]?.id === card) return null;
|
||||
if (g.players.some(player => player.hand?.some(held => held.id === card)) ||
|
||||
g.deck?.some(held => held.id === card) || g.discard?.some(held => held.id === card)) return null;
|
||||
const prepaid = credit?.recipient === winner.id ? Math.min(credit.amount, auction.bid) : 0;
|
||||
const missing = auction.bid - prepaid;
|
||||
let blocked: string | null = null;
|
||||
if (credit && credit.amount > 0 && credit.recipient !== winner.id)
|
||||
blocked = 'The Emperor’s prepaid aid belongs to another recipient.';
|
||||
else if (allyPayment > prepaid)
|
||||
blocked = 'The original allied bid has lost its committed prepaid aid.';
|
||||
else if (missing === 0)
|
||||
blocked = 'The Emperor has already prepaid the full original price.';
|
||||
else if (emperor.spice < missing)
|
||||
blocked = 'The Emperor cannot cover the missing full-price funding from their own spice.';
|
||||
return {
|
||||
auctionIndex: auction.index, card, winner: winner.id, price: auction.bid,
|
||||
emperor: emperor.id, ally: winner.id, allyPayment,
|
||||
aid: { recipient: credit?.recipient ?? null, amount: credit?.amount ?? 0 },
|
||||
emperorSpice: emperor.spice, prepaid, missing, blocked,
|
||||
};
|
||||
}
|
||||
|
||||
/** Public roster only: card identity, bidder hand and native wallet do not
|
||||
* determine who must acknowledge the original obligation. */
|
||||
export function requiredEmperorPaymentResponders(g: Game): string[] {
|
||||
if (!g.nexusCards?.cards || !g.auction?.bidder ||
|
||||
!ids(g.order) || g.order.length !== g.players.length ||
|
||||
!ids(g.players.map(player => player.id)) ||
|
||||
!g.order.every(id => g.players.some(player => player.id === id))) return [];
|
||||
const emperors = g.players.filter(player => player.faction === 'emperor');
|
||||
if (emperors.length !== 1) return [];
|
||||
const emperor = emperors[0];
|
||||
const winner = g.players.find(player => player.id === g.auction!.bidder);
|
||||
if (!winner || winner.id === emperor.id || winner.ally !== emperor.id ||
|
||||
emperor.ally !== winner.id) return [];
|
||||
return g.order.filter(id => {
|
||||
const player = g.players.find(seat => seat.id === id)!;
|
||||
return id !== emperor.id && id !== winner.id && !player.ally &&
|
||||
!!g.nexusCards!.cards!.hands[id];
|
||||
});
|
||||
}
|
||||
|
||||
export function emperorPaymentReceiptSignature(receipt: EmperorPaymentReceipt): string {
|
||||
return JSON.stringify(['nexusEmperorPaymentReceipt', receipt.event, receipt.turn,
|
||||
receipt.phase, receipt.auctionIndex, receipt.card, receipt.winner, receipt.price,
|
||||
receipt.emperor, receipt.ally, receipt.allyPayment, receipt.aid.recipient,
|
||||
receipt.aid.amount, receipt.emperorSpice, receipt.order, receipt.holders,
|
||||
receipt.required, receipt.passed, receipt.stage, receipt.owner]);
|
||||
}
|
||||
|
||||
export function createEmperorPaymentReceipt(
|
||||
g: Game, event: string, required: readonly string[],
|
||||
): EmperorPaymentReceipt {
|
||||
const quote = emperorPaymentSourceQuote(g);
|
||||
requirePayment(quote && text(event) && ids(required) && required.length > 0 &&
|
||||
sameIds(required, requiredEmperorPaymentResponders(g)),
|
||||
'Emperor payment needs the original ordinary winning obligation and public responders.');
|
||||
const receipt: EmperorPaymentReceipt = {
|
||||
event, turn: g.turn, phase: 3, auctionIndex: quote.auctionIndex,
|
||||
card: quote.card, winner: quote.winner, price: quote.price,
|
||||
emperor: quote.emperor, ally: quote.ally, allyPayment: quote.allyPayment,
|
||||
aid: { ...quote.aid }, emperorSpice: quote.emperorSpice,
|
||||
order: [...g.order],
|
||||
holders: g.order.filter(id => !!g.nexusCards!.cards!.hands[id]),
|
||||
required: [...required], passed: [], stage: 'pending', owner: null, signature: '',
|
||||
};
|
||||
receipt.signature = emperorPaymentReceiptSignature(receipt);
|
||||
return receipt;
|
||||
}
|
||||
|
||||
function sameSource(receipt: EmperorPaymentReceipt, quote: EmperorPaymentQuote): boolean {
|
||||
return receipt.auctionIndex === quote.auctionIndex && receipt.card === quote.card &&
|
||||
receipt.winner === quote.winner && receipt.price === quote.price &&
|
||||
receipt.emperor === quote.emperor && receipt.ally === quote.ally &&
|
||||
receipt.allyPayment === quote.allyPayment &&
|
||||
receipt.aid.recipient === quote.aid.recipient && receipt.aid.amount === quote.aid.amount &&
|
||||
receipt.emperorSpice === quote.emperorSpice;
|
||||
}
|
||||
|
||||
/** Pending receipts bind the actual original source. Completed history retains
|
||||
* its own original facts, not future wallets, alliances, or physical hand zones. */
|
||||
export function validateEmperorPaymentReceipt(g: Game, receipt: EmperorPaymentReceipt): void {
|
||||
requirePayment(object(receipt) && Object.keys(receipt).sort().join(',') ===
|
||||
'aid,ally,allyPayment,auctionIndex,card,emperor,emperorSpice,event,holders,order,owner,passed,phase,price,required,signature,stage,turn,winner' &&
|
||||
text(receipt.event) && count(receipt.turn) && receipt.turn > 0 && receipt.turn <= g.turn &&
|
||||
receipt.phase === 3 && (receipt.turn < g.turn || g.phase >= 3) &&
|
||||
count(receipt.auctionIndex) && text(receipt.card) && text(receipt.winner) &&
|
||||
count(receipt.price) && receipt.price > 0 && text(receipt.emperor) &&
|
||||
receipt.emperor !== receipt.winner && receipt.ally === receipt.winner &&
|
||||
count(receipt.allyPayment) && receipt.allyPayment <= receipt.price &&
|
||||
object(receipt.aid) && Object.keys(receipt.aid).sort().join(',') === 'amount,recipient' &&
|
||||
count(receipt.aid.amount) && (receipt.aid.recipient === null
|
||||
? receipt.aid.amount === 0 : text(receipt.aid.recipient) &&
|
||||
g.players.some(player => player.id === receipt.aid.recipient)) &&
|
||||
count(receipt.emperorSpice) && ids(receipt.order) && ids(receipt.holders) &&
|
||||
ids(receipt.required) && ids(receipt.passed) && receipt.required.length > 0 &&
|
||||
receipt.order.length === g.players.length &&
|
||||
receipt.order.every(id => g.players.some(player => player.id === id)) &&
|
||||
g.players.some(player => player.id === receipt.emperor && player.faction === 'emperor') &&
|
||||
receipt.order.includes(receipt.winner) &&
|
||||
sameIds(receipt.holders, receipt.order.filter(id => receipt.holders.includes(id))) &&
|
||||
sameIds(receipt.required, receipt.order.filter(id => receipt.required.includes(id))) &&
|
||||
receipt.required.every(id => receipt.holders.includes(id) &&
|
||||
id !== receipt.emperor && id !== receipt.winner) &&
|
||||
receipt.passed.length <= receipt.required.length &&
|
||||
receipt.passed.every((id, index) => id === receipt.required[index]) &&
|
||||
((receipt.stage === 'pending' && receipt.owner === null && receipt.passed.length < receipt.required.length) ||
|
||||
(receipt.stage === 'passed' && receipt.owner === null && receipt.passed.length === receipt.required.length) ||
|
||||
(receipt.stage === 'used' && text(receipt.owner) &&
|
||||
receipt.owner === receipt.required[receipt.passed.length])) &&
|
||||
text(receipt.signature) && receipt.signature === emperorPaymentReceiptSignature(receipt),
|
||||
'Emperor payment lost its strict original obligation or ordered acknowledgements.');
|
||||
if (receipt.stage === 'pending') {
|
||||
const quote = emperorPaymentSourceQuote(g);
|
||||
requirePayment(receipt.turn === g.turn && quote && sameSource(receipt, quote) &&
|
||||
sameIds(receipt.order, g.order),
|
||||
'Emperor payment lost its original current auction, alliance, aid or native spice.');
|
||||
}
|
||||
if (receipt.stage === 'used') {
|
||||
const prepaid = receipt.aid.recipient === receipt.winner
|
||||
? Math.min(receipt.aid.amount, receipt.price) : 0;
|
||||
requirePayment((receipt.aid.recipient === null || receipt.aid.recipient === receipt.winner ||
|
||||
receipt.aid.amount === 0) && receipt.allyPayment <= prepaid &&
|
||||
prepaid < receipt.price && receipt.emperorSpice >= receipt.price - prepaid,
|
||||
'Emperor payment Use lacks its original full-price funding.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Pure entitlement check. The engine spends the one physical card and commits
|
||||
* the additional credit only after this succeeds and its native checks pass. */
|
||||
export function emperorPaymentUseBlock(
|
||||
g: Game, receipt: EmperorPaymentReceipt, owner: string,
|
||||
): string | null {
|
||||
try { validateEmperorPaymentReceipt(g, receipt); }
|
||||
catch (error) { return error instanceof Error ? error.message : 'Invalid Emperor payment obligation.'; }
|
||||
const pending = g.pendingNexusEmperorPayment?.receipt;
|
||||
try {
|
||||
if (pending && pending !== receipt) validateEmperorPaymentReceipt(g, pending);
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Invalid current Emperor payment obligation.';
|
||||
}
|
||||
if (receipt.stage !== 'pending' || g.nexusEmperorPayment?.current !== receipt.event ||
|
||||
!pending || pending.event !== receipt.event || pending.signature !== receipt.signature ||
|
||||
g.nexusEmperorPayment?.completed.some(completed => completed.event === receipt.event) ||
|
||||
receipt.required[receipt.passed.length] !== owner || g.active !== owner ||
|
||||
g.decision?.kind !== 'nexusEmperorPayment' || g.decision.player !== owner ||
|
||||
g.decision.event !== receipt.event)
|
||||
return 'Only the current original Emperor payment responder may use this obligation.';
|
||||
if (g.truthtrance || g.nexusTraitorExchanges?.some(exchange => exchange.stage === 'return') ||
|
||||
g.nexusCards?.phase?.stage === 'drawing' || g.response || g.pendingKarama)
|
||||
return 'Finish the current question, card return or response first.';
|
||||
const player = g.players.find(seat => seat.id === owner);
|
||||
if (!player || player.ally || owner === receipt.emperor || owner === receipt.winner ||
|
||||
g.nexusCards?.cards?.hands[owner] !== 'emperor')
|
||||
return 'Use requires your actual unallied Emperor Nexus Betrayal card.';
|
||||
const quote = emperorPaymentSourceQuote(g);
|
||||
if (!quote || !sameSource(receipt, quote)) return 'The original Emperor payment source changed.';
|
||||
return quote.blocked;
|
||||
}
|
||||
+7
-6
@@ -4568,8 +4568,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. The separate Secret Ally revival and normal bank-auction purchase have connected controls; seller-paid and forced Emperor payments 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 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.',
|
||||
'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.',
|
||||
'Before one accepted worm devours forces with Fremen absent, publicly possible unallied threatened held-Nexus seats acknowledge in original storm order regardless of secret face. Only the actual Fremen holder may spend to save their own groups. Pass keeps the card; all acknowledgements precede casualties. Under the explicit provisional independent-operation policy, no extra Karama provider or riding is invented. Original other casualties, spice removal, Great Maker vote and next blow/Nexus remain. Protected No-Field uses native protected-victim skipping; wider marker/module acceptance remains open.',
|
||||
'Native Fremen Cunning has a bounded classic Nexus path, optionally retaining original Tech Tokens and Advanced Stronghold Cards: 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 or changing original component custody/income. 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, dynamic destinations and other optional-module combinations await integration.',
|
||||
@@ -4597,11 +4598,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 physical effects remain. Fremen Secret Ally now enables a fixed ordinary free quota3 without moving forces, then original partial/typed revival preserves prior actual free/paid usage, native limits/rates/Recruits, prevention, income, Tech and Homeworld continuation. Strict old physical receipts adopt source metadata without replay. Removed early-only direct-transfer action/selector. Borrowed Voice, independent protection and fresh/storm BG Cunning retain their separate provisional policies. Wider combinations and custody 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'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Fremen activation visibly explains the provisional total-free-quota policy, returns no counters and hands over to original partial force/elite controls and native Tleilaxu Allow decisions. Real390px one-plus-two returns exhaust the quota and disable further returns. Existing Voice/protection/Cunning controls and shared native movement route guidance remain. Complete modes and wider module controls are 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'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing minimal legal profiles retain original responses and continuation. Fremen holders use the shared event-only quota activation before original ordinary revival selection rather than a Nexus-only elite action. Genuine Basic/Advanced classic/native-Tleilaxu Easy paths return eligible free counters; 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'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Common rules and twelve faces retain remaining custody, inquiry, native nested Cunning and combination questions. Source documents distinguish cancellable named Voice, independent protection and the recommended in-place fresh/storm Cunning exceptions without claiming publisher answers. Actual Advanced card-distance chronology and native ordinary restrictions are explicit. Existing Richese/Guild contracts and complete-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: 'Quota affected20-file union329/329 passes with genuine Basic/Advanced classic/native-Tleilaxu one-plus-two, late free/paid returns, expiry, original income, typed module consumers and legacy metadata adoption. Types/lint pass. Four actual API profiles reach Movement after late activation. Actual390px activation then one/two returns with original Tleilaxu Allow decisions preserves spice13, Tanks8→5/reserves10→13/free3 and native income1 once; one discard, no overflow/browser errors. Prior BG Cunning65/65 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'] },
|
||||
{area:'Implementation',status:'Partial',detail:'Existing physical effects and Fremen fixed ordinary quota remain. Emperor Betrayal now binds original positive normal-auction allied obligation, neutral possible-holder acknowledgements, exact original lot/price/alliance/escrow/wallet and one physical card. Provisional full-price funding reuses prepaid aid, adds only missing own spice and calls native acquisition/income/next-lot continuation. Truthtrance discard restoration is retained; no refund, new bid, excessive gift or bank subsidy. Special seller composition, wider combinations and custody 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']},
|
||||
{area:'Player controls',status:'Partial',detail:'Emperor payment uses neutral original-event Continue and a current-holder-only Use with visible full-price interpretation and blocking reason. Actual390px neutral/pass/use followed by original income Allow or printed Karama cancellation preserves native acquisition and next-lot inspection. Existing Fremen normal partial controls, Voice/protection/Cunning and movement guidance 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']},
|
||||
{area:'AI',status:'Partial',detail:'Existing minimal legal profiles retain original responses and continuation. Fremen holders enable ordinary quota before native revival selection. Emperor compulsory-funding responders pass from the public event unless their current private offer permits Use. Genuine Easy neutral/pass/use and original paid acquisition are exercised; 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']},
|
||||
{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:'Compulsory-funding affected10-file union155/155 passes. Genuine Basic/Advanced setup, real worm/pact/closing draw and Guild price2 auction exercise prepaid0/1, allpass0/1/2, original income allow/cancel, Truthtrance restore and immutable event/actor/key refusals. Four API profiles and two real390px allow/cancel surfaces retain buyer6, reuse prepaid1/add missing1, acquire one card/spend one Nexus and restore lot1 inspection. Native Emperor10 returns on allowance or stays8 after cancellation; no refund/overflow/browser errors. Types/lint/build pass. Prior quota329/329 and BG Cunning65/65 evidence remains; special seller, 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']},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -90,6 +90,7 @@ const DECISIONS = {
|
||||
wormRide: true,
|
||||
nexusFremenCunningOffer: true,
|
||||
nexusFremenProtection: true,
|
||||
nexusEmperorPayment: true,
|
||||
nexusFremenCunningRide: true,
|
||||
moritaniRetention: true,
|
||||
moritaniSpecialKarama: true,
|
||||
|
||||
Reference in new issue
Block a user