Prototype Emperor Nexus shortage handling
This commit is contained in:
1 parent
b620cea873
commit
e5c7e338e8
31 files changed
+1057
-95
No files matched your search
+24
-13
@@ -118,7 +118,7 @@ import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySig
|
||||
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
|
||||
import { quoteEcazBattleLead } from './ecaz-homeworld-lead';
|
||||
import { sourceForRicheseCunning, richeseCunningRateMatches, validRicheseCunningRateSource, RicheseCunningRateError, type RicheseCunningRateSource } from './richese-cunning-rate';
|
||||
import { EMPEROR_NEXUS_REVIVALS, emperorNexusLegacyRevivalModeSupported, quoteEmperorNexusRevival, createEmperorNexusRevival, settleEmperorNexusRevival, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { emperorNexusRevivalCount, emperorNexusLegacyRevivalModeSupported, quoteEmperorNexusRevival, createEmperorNexusRevival, settleEmperorNexusRevival, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, emperorNexusBankPurchaseModeSupported, emperorNexusSellerPurchaseModeSupported, quoteEmperorNexusSellerPurchase, createEmperorNexusSellerPurchase, emperorNexusAmbassadorPurchaseModeSupported, quoteEmperorNexusAmbassadorPurchase, createEmperorNexusAmbassadorPurchase, quoteEmperorNexusSpecialPurchase, createEmperorNexusSpecialPurchase, 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';
|
||||
@@ -3333,15 +3333,16 @@ function playNexusEmperorRevive(g:Game,p:Player,action:Action) {
|
||||
const offer = nexusEmperorSecretAllyOffer(g,p.id);
|
||||
requireRule(offer && !offer.revival.blocked && action.event === offer.event &&
|
||||
Object.keys(action).sort().join(',') === 'elite,event,type' && offer.revival.eliteOptions.includes(action.elite as number),
|
||||
offer?.revival.blocked ?? 'Choose the current Emperor Nexus revival and an eligible exact three-force group.');
|
||||
offer?.revival.blocked ?? 'Choose the current Emperor Nexus revival and its eligible physical force group.');
|
||||
const record = nexusRule(() => createEmperorNexusRevival(g, p.id, action.elite as number));
|
||||
const amount = emperorNexusRevivalCount(record);
|
||||
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!,p.id,g.players));
|
||||
(g.nexusEmperorSecretHistory ??= []).push(record);
|
||||
(g.nexusEmperorSecretEvents ??= []).push(record.event);
|
||||
log(g, `${p.name} spent Emperor Nexus Secret Ally to request exactly three additional forces for free through the original revival continuation. Ordinary force/free allowances are unchanged; native Tleilaxu prevention applies under the provisional independent-extra-return policy.`,
|
||||
log(g, `${p.name} spent Emperor Nexus Secret Ally to request ${amount === 3 ? 'exactly three' : amount} additional force${amount === 1 ? '' : 's'} for free through the original revival continuation.${amount < 3 ? ' Provisional shortage policy uses every currently eligible counter.' : ''} Ordinary force/free allowances are unchanged; native Tleilaxu prevention applies under the provisional independent-extra-return policy.`,
|
||||
{faction:p.faction,name:'Emperor Nexus revival'});
|
||||
beginRevival(g, { player: p.id, kind: 'forces', amount: EMPEROR_NEXUS_REVIVALS,
|
||||
elite: record.elite, free: EMPEROR_NEXUS_REVIVALS, normalCost: 0, cost: 0,
|
||||
beginRevival(g, { player: p.id, kind: 'forces', amount,
|
||||
elite: record.elite, free: amount, normalCost: 0, cost: 0,
|
||||
checks: [], nexusEmperorEvent: record.event });
|
||||
}
|
||||
function finishNexusEmperorRevival(g: Game, revival: PendingRevival, outcome: 'returned' | 'denied') {
|
||||
@@ -3351,7 +3352,7 @@ function finishNexusEmperorRevival(g: Game, revival: PendingRevival, outcome: 'r
|
||||
const record = history?.[index];
|
||||
requireRule(record && 'version' in record && record.version === 2 &&
|
||||
record.stage === 'requested' && record.owner === revival.player,
|
||||
'The extra-three revival lost its original consumed Nexus source.');
|
||||
'The additional revival lost its original consumed Nexus source.');
|
||||
history![index] = nexusRule(() => settleEmperorNexusRevival(g, record, outcome));
|
||||
if (outcome === 'denied')
|
||||
log(g, `${getPlayer(g, revival.player).name}'s Emperor Nexus extra return was prevented. The Nexus remains spent; no counters, payment, revival income or Tech activity was granted.`);
|
||||
@@ -4265,7 +4266,9 @@ function nexusSardaukarIntegrity(g: Game) {
|
||||
requireRule(record.stage === 'active' && ['pending','complete'].includes(losses.outcome) &&
|
||||
Object.keys(losses).sort().join(',') ===
|
||||
['dial','forces','options','outcome','support',...(fee ? ['reserveFee'] : []),...Object.keys(constraints)].sort().join(',') &&
|
||||
validCombatForces(losses.forces) && (fee ? Number(losses.forces.temporaryElite) >= 2 : losses.forces.temporaryElite === 5) &&
|
||||
validCombatForces(losses.forces) && (fee
|
||||
? Number(losses.forces.temporaryElite) >= 0 && Number(losses.forces.temporaryElite) < record.receipt.count
|
||||
: losses.forces.temporaryElite === record.receipt.count) &&
|
||||
Array.isArray(losses.options) && losses.options.length > 0 &&
|
||||
JSON.stringify(losses.options) === JSON.stringify(committedCasualtyOptions(losses.forces,losses.dial,losses.support,losses)),
|
||||
'The Nexus Sardaukar casualties have lost their committed force allocation.');
|
||||
@@ -4315,7 +4318,8 @@ function nexusSardaukarIntegrity(g: Game) {
|
||||
if (record.stage !== 'canceled') {
|
||||
const p = getPlayer(g, record.receipt.owner), other = getPlayer(g, b!.attacker === p.id ? b!.defender : b!.attacker);
|
||||
const forces = combatForces(g, p, b!.territory, other);
|
||||
requireRule(forces.normal >= 5 && forces.elite === 0, 'The Nexus battle no longer has five eligible ordinary forces.');
|
||||
requireRule(forces.normal >= record.receipt.count && forces.elite === 0,
|
||||
'The Nexus battle no longer has its original eligible ordinary role group.');
|
||||
}
|
||||
}
|
||||
for (const response of responses) validateNexusSardaukarResponse(g, response!);
|
||||
@@ -4335,7 +4339,7 @@ function nexusSardaukarEligibility(g: Game, p: Player): string | null {
|
||||
const other = getPlayer(g, b.attacker === p.id ? b.defender : b.attacker);
|
||||
const forces = combatForces(g, p, b.territory, other);
|
||||
if (forces.elite) return 'Cunning requires a battle containing none of your actual Sardaukar.';
|
||||
if (forces.normal < 5) return 'Using Emperor Cunning with fewer than five eligible forces awaits a ruling.';
|
||||
if (forces.normal < 1) return 'Cunning needs at least one eligible ordinary force in this battle.';
|
||||
return null;
|
||||
}
|
||||
function projectedNexusSardaukar(g: Game, id: string) {
|
||||
@@ -4343,9 +4347,15 @@ function projectedNexusSardaukar(g: Game, id: string) {
|
||||
const record = currentNexusSardaukar(g);
|
||||
const active = record?.stage === 'active';
|
||||
const pending = record?.stage === 'pending' ? {event: record.receipt.event, owner: record.receipt.owner} : null;
|
||||
const short: { shortCount?: number } = record?.receipt.shortGroup ? { shortCount: record.receipt.count } : {};
|
||||
const p = getPlayer(g,id);
|
||||
if (p.faction !== 'emperor' || g.nexusCards.cards.hands[id] !== 'emperor') return {offer:null,active,pending};
|
||||
if (p.faction !== 'emperor' || g.nexusCards.cards.hands[id] !== 'emperor') return {offer:null,active,pending,...short};
|
||||
let blocked = nexusSardaukarEligibility(g,p);
|
||||
if (!blocked) {
|
||||
const b = g.battle!, other = getPlayer(g,b.attacker === p.id ? b.defender : b.attacker);
|
||||
const normal = combatForces(g,p,b.territory,other).normal;
|
||||
if (normal < 5) short.shortCount = normal;
|
||||
}
|
||||
if (!blocked && (g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingKarama ||
|
||||
g.pendingTreacheryDiscard || g.pendingNullentropy || pendingNexusTraitors(g) || g.nexusCards.phase?.stage === 'drawing'))
|
||||
blocked = 'Finish the current interaction before declaring Nexus Sardaukar.';
|
||||
@@ -4356,7 +4366,7 @@ function projectedNexusSardaukar(g: Game, id: string) {
|
||||
if (!findReachableBattlePlan(trial,getPlayer(trial,id)))
|
||||
blocked = 'Sardaukar strength would prevent you from honoring an existing Battle Plan commitment.';
|
||||
}
|
||||
return {offer:{event,blocked},active,pending};
|
||||
return {offer:{event,blocked},active,pending,...short};
|
||||
}
|
||||
function commitNexusSardaukar(g: Game, p: Player, stage: 'pending' | 'active') {
|
||||
const blocked = nexusSardaukarEligibility(g,p);
|
||||
@@ -4375,7 +4385,8 @@ function playNexusSardaukar(g: Game,p: Player,action: Action) {
|
||||
requireRule(Object.keys(action).every(key => ['type','event'].includes(key)) && offer && !offer.blocked && action.event === offer.event,
|
||||
offer?.blocked ?? 'Choose the current Emperor Cunning opportunity.');
|
||||
commitNexusSardaukar(g,p,'pending');
|
||||
log(g,`${p.name} spent Emperor Nexus Cunning to count five ordinary forces as Sardaukar for this battle. Karama may prevent this use. The physical counters remain ordinary forces.`,
|
||||
const count = currentNexusSardaukar(g)!.receipt.count;
|
||||
log(g,`${p.name} spent Emperor Nexus Cunning to count ${count === 5 ? 'five' : count} ordinary force${count === 1 ? '' : 's'} as Sardaukar for this battle.${count < 5 ? ' Provisional shortage policy uses the available group.' : ''} Karama may prevent this use. The physical counters remain ordinary forces.`,
|
||||
{faction:p.faction,name:'Nexus Sardaukar declared'});
|
||||
}
|
||||
function finishNexusSardaukar(g: Game,canceled: boolean) {
|
||||
@@ -4384,7 +4395,7 @@ function finishNexusSardaukar(g: Game,canceled: boolean) {
|
||||
g.nexusSardaukarLast = {event:record.receipt.event,stage:record.stage};
|
||||
if (canceled) reconcileChangedBattleInspections(g, 'the canceled Sardaukar enhancement');
|
||||
log(g,canceled ? 'Karama prevented Emperor Cunning. These forces remain ordinary in both strength and physical custody; the Nexus card stays spent.'
|
||||
: 'Five ordinary Emperor forces count as Sardaukar in this battle only. They use Sardaukar strength and support, including the Fremen exception, but every casualty remains an ordinary physical counter.',
|
||||
: `${record.receipt.count === 5 ? 'Five' : record.receipt.count} ordinary Emperor force${record.receipt.count === 1 ? '' : 's'} count${record.receipt.count === 1 ? 's' : ''} as Sardaukar in this battle only. They use Sardaukar strength and support, including the Fremen exception, but every casualty remains an ordinary physical counter.`,
|
||||
{faction:'emperor',name:canceled ? 'Nexus Sardaukar prevented' : 'Nexus Sardaukar active'});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Action, GameView } from './engine';
|
||||
import { EMPEROR_NEXUS_REVIVALS } from './nexus-emperor-secret-ally';
|
||||
|
||||
/** Bind the current private offer without inspecting another player's cards. */
|
||||
export function nexusEmperorRevivalAction(
|
||||
@@ -6,6 +7,7 @@ export function nexusEmperorRevivalAction(
|
||||
elite: number,
|
||||
): Action | null {
|
||||
const offer = game.nexusEmperorSecretAlly;
|
||||
const amount = offer?.revival.amount ?? EMPEROR_NEXUS_REVIVALS;
|
||||
const owner = game.players.find((player) => player.id === game.me);
|
||||
if (
|
||||
!offer ||
|
||||
@@ -23,7 +25,10 @@ export function nexusEmperorRevivalAction(
|
||||
game.nexusCards.waiting.length ||
|
||||
game.nexusTraitors?.pending ||
|
||||
offer.revival.blocked ||
|
||||
offer.event !== offer.revival.event ||
|
||||
!Number.isSafeInteger(amount) || amount < 1 || amount > EMPEROR_NEXUS_REVIVALS ||
|
||||
!Number.isSafeInteger(elite) ||
|
||||
elite < 0 || elite > amount ||
|
||||
!offer.revival.eliteOptions.includes(elite)
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -52,7 +52,7 @@ export type NexusEmperorRevival =
|
||||
stage?: never;
|
||||
after: EmperorNexusPools;
|
||||
})
|
||||
| (EmperorNexusRevivalIdentity & { version: 2 } & (
|
||||
| (EmperorNexusRevivalIdentity & { version: 2; amount?: 1 | 2 } & (
|
||||
| { stage: 'requested'; after: null }
|
||||
| { stage: 'returned' | 'denied'; after: EmperorNexusPools }
|
||||
));
|
||||
@@ -60,6 +60,11 @@ export type NexusEmperorRevivalRequest = Extract<
|
||||
NexusEmperorRevival,
|
||||
{ version: 2; stage: 'requested' }
|
||||
>;
|
||||
/** Existing receipts omit the amount and retain their original three-force
|
||||
* signature. Only a new short requested return records one or two. */
|
||||
export function emperorNexusRevivalCount(receipt: NexusEmperorRevival): number {
|
||||
return receipt.version === 2 ? receipt.amount ?? EMPEROR_NEXUS_REVIVALS : EMPEROR_NEXUS_REVIVALS;
|
||||
}
|
||||
export function emperorNexusPools(p: Player): EmperorNexusPools {
|
||||
return {
|
||||
reserves: p.reserves,
|
||||
@@ -108,17 +113,29 @@ export function emperorNexusLegacyRevivalModeSupported(g: Game): boolean {
|
||||
(!!g.leaderSkills && classicNexusLeaderSkillsProfile(g)))))
|
||||
);
|
||||
}
|
||||
/** A fixed three-counter grant preserves the separate Advanced elite cap. */
|
||||
/** Provisional short-group completion: take three whenever eligible; otherwise
|
||||
* take every eligible physical counter, respecting the existing typed cap. */
|
||||
export function emperorNexusRevivalAmount(
|
||||
p: Pick<Player, 'tanks' | 'faction' | 'elites'>,
|
||||
advanced: boolean,
|
||||
): number {
|
||||
return Math.min(EMPEROR_NEXUS_REVIVALS,
|
||||
p.tanks - (p.elites?.tanks ?? 0) + eliteRevivalRemaining(p, advanced));
|
||||
}
|
||||
|
||||
/** Choose only the typed composition of the current maximal return. */
|
||||
export function emperorNexusRevivalElites(
|
||||
p: Player,
|
||||
advanced: boolean,
|
||||
): number[] {
|
||||
const amount = emperorNexusRevivalAmount(p, advanced);
|
||||
if (amount <= 0) return [];
|
||||
const choices: number[] = [];
|
||||
for (let elite = 0; elite <= 3; elite++)
|
||||
for (let elite = 0; elite <= amount; elite++)
|
||||
if (
|
||||
elite <= eliteRevivalRemaining(p, advanced) &&
|
||||
elite <= (p.elites?.tanks ?? 0) &&
|
||||
3 - elite <= p.tanks - (p.elites?.tanks ?? 0)
|
||||
amount - elite <= p.tanks - (p.elites?.tanks ?? 0)
|
||||
)
|
||||
choices.push(elite);
|
||||
return choices;
|
||||
@@ -129,6 +146,8 @@ const receiptKeys =
|
||||
'advanced,after,before,elite,event,faction,kind,owner,phase,sequence,signature,turn';
|
||||
const receiptV2Keys =
|
||||
'advanced,after,before,elite,event,faction,kind,owner,phase,sequence,signature,stage,turn,version';
|
||||
const receiptV2ShortKeys =
|
||||
'advanced,after,amount,before,elite,event,faction,kind,owner,phase,sequence,signature,stage,turn,version';
|
||||
const whole = (n: unknown): n is number =>
|
||||
Number.isSafeInteger(n) && (n as number) >= 0;
|
||||
function shape(value: unknown, keys: string): boolean {
|
||||
@@ -149,10 +168,11 @@ function samePools(a: EmperorNexusPools, b: EmperorNexusPools): boolean {
|
||||
a[key as keyof EmperorNexusPools] === b[key as keyof EmperorNexusPools]);
|
||||
}
|
||||
function returnedPools(r: NexusEmperorRevival): EmperorNexusPools {
|
||||
const amount = emperorNexusRevivalCount(r);
|
||||
return {
|
||||
...r.before,
|
||||
reserves: r.before.reserves + EMPEROR_NEXUS_REVIVALS,
|
||||
tanks: r.before.tanks - EMPEROR_NEXUS_REVIVALS,
|
||||
reserves: r.before.reserves + amount,
|
||||
tanks: r.before.tanks - amount,
|
||||
eliteReserves: r.before.eliteReserves + r.elite,
|
||||
eliteTanks: r.before.eliteTanks - r.elite,
|
||||
eliteRevived: r.before.eliteRevived + r.elite,
|
||||
@@ -167,17 +187,19 @@ export function validateEmperorNexusRevival(
|
||||
): void {
|
||||
const p = r && g.players.find(p => p.id === r.owner);
|
||||
const legacy = shape(r, receiptKeys);
|
||||
if (!p || !(legacy || shape(r, receiptV2Keys)) ||
|
||||
const short = shape(r, receiptV2ShortKeys);
|
||||
const amount = r ? emperorNexusRevivalCount(r) : EMPEROR_NEXUS_REVIVALS;
|
||||
if (!p || !(legacy || shape(r, receiptV2Keys) || short) ||
|
||||
r.kind !== 'revival' || r.phase !== 4 ||
|
||||
typeof r.owner !== 'string' || !r.owner.trim() ||
|
||||
!whole(r.turn) || r.turn < 1 || r.turn > g.turn ||
|
||||
r.faction !== p.faction || r.advanced !== g.advanced ||
|
||||
!whole(r.sequence) || !whole(r.elite) || r.elite > EMPEROR_NEXUS_REVIVALS ||
|
||||
!whole(r.sequence) || !whole(r.elite) || r.elite > amount ||
|
||||
r.event !== emperorNexusEvent(r.turn, 4, r.owner, r.sequence) ||
|
||||
r.signature !== emperorNexusSignature(r) || !validPools(r.before) ||
|
||||
r.before.tanks < EMPEROR_NEXUS_REVIVALS ||
|
||||
r.before.tanks < amount ||
|
||||
r.elite > r.before.eliteTanks ||
|
||||
EMPEROR_NEXUS_REVIVALS - r.elite > r.before.tanks - r.before.eliteTanks)
|
||||
amount - r.elite > r.before.tanks - r.before.eliteTanks)
|
||||
throw new Error('Emperor Nexus revival has lost its exact original physical return or allowance.');
|
||||
if (legacy) {
|
||||
if (g.expansions.length ||
|
||||
@@ -192,6 +214,10 @@ export function validateEmperorNexusRevival(
|
||||
}
|
||||
const typed = p.faction === 'ixians' || p.faction === 'fremen';
|
||||
if (r.version !== 2 ||
|
||||
(short && r.amount !== 1 && r.amount !== 2) ||
|
||||
amount !== emperorNexusRevivalAmount({ faction: r.faction, tanks: r.before.tanks,
|
||||
elites: { reserves: r.before.eliteReserves, tanks: r.before.eliteTanks,
|
||||
revived: r.before.eliteRevived, forces: {} } }, r.advanced) ||
|
||||
g.players.some(seat => seat.faction === 'emperor') ||
|
||||
(!typed && (r.elite || r.before.eliteTanks || r.before.eliteReserves || r.before.eliteRevived)) ||
|
||||
(typed && r.before.eliteReserves + r.before.eliteTanks > (p.faction === 'ixians' ? 7 : 3)) ||
|
||||
@@ -236,16 +262,18 @@ function revivalModeSupported(g: Game): boolean {
|
||||
|
||||
/** Provisional composition, not a publisher clarification or numerical user ruling. */
|
||||
const revivalPolicy =
|
||||
'Provisional composed policy: exactly three additional free physical forces from Tanks, beyond ordinary force and free-revival allowances. Native typed caps and Tleilaxu prevention still apply; this is not ordinary free-rate revival, so ordinary free blocking (including LaLaLa) and higher or expanded rates do not reprice it. Recruits does not double this fixed grant. Spend the card on declaration; denial returns no forces and grants no revival income or Tech benefit. No leader, missing-native-Emperor payment, extra Karama response or refund is granted. Actual returns continue through native Tleilaxu, Tech and Homeworld handling.';
|
||||
'Provisional composed policy: return exactly three additional free physical forces from Tanks whenever three are eligible; otherwise return all one or two eligible forces. Zero cannot spend the card. This short-group policy is not publisher clarification. The return is beyond ordinary force and free-revival allowances. Native typed caps and Tleilaxu prevention still apply; this is not ordinary free-rate revival, so ordinary free blocking (including LaLaLa) and higher or expanded rates do not reprice it. Recruits does not double the grant. Spend the card on declaration; denial returns no forces and grants no revival income or Tech benefit. No leader, missing-native-Emperor payment, extra Karama response or refund is granted. Actual returns continue through native Tleilaxu, Tech and Homeworld handling.';
|
||||
|
||||
export function quoteEmperorNexusRevival(g: Game, owner: string): {
|
||||
event: string;
|
||||
amount: number;
|
||||
blocked: string | null;
|
||||
eliteOptions: number[];
|
||||
policy: string;
|
||||
} {
|
||||
const p = g.players.find(p => p.id === owner);
|
||||
const event = emperorNexusEvent(g.turn, g.phase, owner, g.nexusEmperorSecretHistory?.length ?? 0);
|
||||
const amount = p ? emperorNexusRevivalAmount(p, g.advanced) : 0;
|
||||
const eliteOptions = p ? emperorNexusRevivalElites(p, g.advanced) : [];
|
||||
let blocked: string | null = null;
|
||||
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor')
|
||||
@@ -256,7 +284,7 @@ export function quoteEmperorNexusRevival(g: Game, owner: string): {
|
||||
else if (!revivalModeSupported(g))
|
||||
blocked = 'Emperor Nexus revival requires an original supported Nexus roster and module profile.';
|
||||
else if (g.status !== 'playing' || g.phase !== 4)
|
||||
blocked = 'Use the three-force Emperor Nexus return during Revival.';
|
||||
blocked = 'Use the additional Emperor Nexus return during Revival.';
|
||||
else if (g.response || g.decision || g.truthtrance || g.phaseOpening ||
|
||||
g.pendingRevival || g.pendingKarama || g.pendingTreacheryDiscard ||
|
||||
g.pendingNullentropy || g.pendingExchange || g.pendingRicheseGift ||
|
||||
@@ -268,8 +296,8 @@ export function quoteEmperorNexusRevival(g: Game, owner: string): {
|
||||
else if (revivalPrevented(g, owner))
|
||||
blocked = 'Native Tleilaxu prevention blocks this revival.';
|
||||
else if (!eliteOptions.length)
|
||||
blocked = 'Emperor Nexus needs exactly three eligible forces in Tanks.';
|
||||
return { event, blocked, eliteOptions, policy: revivalPolicy };
|
||||
blocked = 'Emperor Nexus needs at least one eligible force in Tanks.';
|
||||
return { event, amount, blocked, eliteOptions, policy: revivalPolicy };
|
||||
}
|
||||
|
||||
/** Pure declaration receipt: the engine commits card custody and the original pending revival. */
|
||||
@@ -280,12 +308,13 @@ export function createEmperorNexusRevival(
|
||||
): NexusEmperorRevivalRequest {
|
||||
const quote = quoteEmperorNexusRevival(g, owner);
|
||||
if (quote.blocked || !quote.eliteOptions.includes(elite))
|
||||
throw new Error(quote.blocked ?? 'Choose an eligible exact three-force typed group.');
|
||||
throw new Error(quote.blocked ?? 'Choose an eligible typed group for the current additional return.');
|
||||
const p = g.players.find(p => p.id === owner)!;
|
||||
const r: NexusEmperorRevivalRequest = {
|
||||
kind: 'revival', version: 2, event: quote.event, owner, turn: g.turn, phase: 4,
|
||||
sequence: g.nexusEmperorSecretHistory?.length ?? 0,
|
||||
faction: p.faction, advanced: g.advanced, elite,
|
||||
...(quote.amount === 1 || quote.amount === 2 ? { amount: quote.amount } : {}),
|
||||
before: emperorNexusPools(p), after: null, stage: 'requested', signature: '',
|
||||
};
|
||||
r.signature = emperorNexusSignature(r);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Action, GameView } from './engine';
|
||||
|
||||
/** The owner-only server offer verifies the five-counter army and the
|
||||
/** The owner-only server offer verifies the available ordinary army and the
|
||||
* feasibility of existing commitments. Preserve its exact battle event. */
|
||||
export function nexusSardaukarAction(
|
||||
game: GameView,
|
||||
|
||||
+14
-6
@@ -13,7 +13,9 @@ export type NexusSardaukarReceipt = {
|
||||
territory: string;
|
||||
normal: number;
|
||||
elite: 0;
|
||||
count: 5;
|
||||
count: number;
|
||||
/** Explicit provisional shortage composition; absent on original five-role receipts. */
|
||||
shortGroup?: true;
|
||||
roster: { id: string; faction: FactionId }[];
|
||||
signature: string;
|
||||
};
|
||||
@@ -58,6 +60,7 @@ function signature(receipt: NexusSardaukarReceipt): string {
|
||||
receipt.elite,
|
||||
receipt.count,
|
||||
receipt.roster.map((p) => [p.id, p.faction]),
|
||||
...(receipt.shortGroup ? ['available-short-group'] : []),
|
||||
]);
|
||||
}
|
||||
/** Historical evidence does not depend on forces surviving this battle. */
|
||||
@@ -72,7 +75,9 @@ export function validateNexusSardaukar(
|
||||
!Array.isArray(receipt) &&
|
||||
[Object.prototype, null].includes(Object.getPrototypeOf(receipt)) &&
|
||||
Object.keys(receipt).sort().join(',') ===
|
||||
'battle,count,elite,event,normal,owner,roster,signature,territory,turn,version' &&
|
||||
['battle','count','elite','event','normal','owner','roster',
|
||||
...(receipt.shortGroup === true ? ['shortGroup'] : []),
|
||||
'signature','territory','turn','version'].sort().join(',') &&
|
||||
receipt.version === 1 &&
|
||||
identifier(receipt.battle) &&
|
||||
identifier(receipt.territory) &&
|
||||
@@ -80,10 +85,12 @@ export function validateNexusSardaukar(
|
||||
receipt.turn > 0 &&
|
||||
receipt.turn <= context.turn &&
|
||||
Number.isSafeInteger(receipt.normal) &&
|
||||
receipt.normal >= 5 &&
|
||||
receipt.normal >= 1 &&
|
||||
receipt.normal <= 20 &&
|
||||
receipt.elite === 0 &&
|
||||
receipt.count === 5 &&
|
||||
(receipt.normal < 5
|
||||
? receipt.shortGroup === true && receipt.count === receipt.normal
|
||||
: receipt.shortGroup === undefined && receipt.count === 5) &&
|
||||
seats.some((p) => p.id === receipt.owner && p.faction === 'emperor') &&
|
||||
receipt.event ===
|
||||
JSON.stringify([
|
||||
@@ -94,7 +101,7 @@ export function validateNexusSardaukar(
|
||||
]) &&
|
||||
JSON.stringify(receipt.roster) === JSON.stringify(seats) &&
|
||||
receipt.signature === signature(receipt),
|
||||
'The Sardaukar Nexus has lost its original battle, five-counter group or owner.',
|
||||
'The Sardaukar Nexus has lost its original battle, physical role group or owner.',
|
||||
);
|
||||
}
|
||||
export function createNexusSardaukar(
|
||||
@@ -113,7 +120,8 @@ export function createNexusSardaukar(
|
||||
territory,
|
||||
normal,
|
||||
elite: 0,
|
||||
count: 5,
|
||||
count: Math.min(5, normal),
|
||||
...(normal < 5 ? { shortGroup: true as const } : {}),
|
||||
roster: roster(context),
|
||||
signature: '',
|
||||
};
|
||||
|
||||
+9
-9
@@ -5877,11 +5877,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'With Emperor absent, an unallied holder can choose exactly three extra free returns or retain a personally affordable original purchase price. Original classic/native bank, Richese cache/Advanced Black Market, Ambassador and joint special Karama have distinct connected paths. Full combinations and assurance retain separate gates.',
|
||||
summary: 'With Emperor absent, an unallied holder can take the current eligible extra free group or retain a personally affordable original purchase price. The provisional shortage policy takes three whenever possible, otherwise all one or two eligible counters. Original classic/native bank, Richese cache/Advanced Black Market, Ambassador and joint special Karama have distinct connected paths. Full combinations and assurance retain separate gates.',
|
||||
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.',
|
||||
'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 three eligible forces whenever possible, otherwise all one or two eligible counters under the visible provisional shortage policy. It is discarded once; zero forces cannot spend it.',
|
||||
'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 Advanced one-per-turn Fedaykin limit still applies across ordinary revivals and this card. The extra total allowance does not permit another elite revival. The visible provisional shortage policy uses the remaining positive eligible group without exceeding that cap.',
|
||||
'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 original classic/native Nexus 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 privacy recommendation remains. Canonical native module envelopes preserve original card/capacity, bonus and next-lot inspection; ordinary spice or legal Karama remains available. No bank subsidy or Richese cache/seller route is invented. Strict old physical revival admission is separate and never broadens.',
|
||||
'Original Richese cache and other-buyer 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. Already-held Black Market paid retention supplies no acquisition/free-seller source, so it does not invent this confirmation or spend a Nexus. Original self-purchase forced discard cannot consume this Nexus or acquire a card.',
|
||||
@@ -5890,11 +5890,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
related: ['nexus-cards', 'revival'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Independent extra-three and seller/Ambassador/joint sources remain. Normal bank purchase now uses canonical native Nexus admission plus exact old classic parity, preserving its original price, card/capacity/bonus/successor and receipt format. Strict legacy physical revival stays classic. No subsidy or extra seller source; full combinations and assurance are open.', evidence: ['game/nexus-emperor-secret-ally.ts', 'game/nexus-emperor-purchase.ts', 'game/auction-continuation-quote.ts', 'game/engine.ts', 'game/ambassador-phase.ts', 'game/ambassador-resume.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Independent extra returns retain the full three-counter source and explicitly record positive short groups under the provisional policy. Seller/Ambassador/joint sources remain. Normal bank purchase now uses canonical native Nexus admission plus exact old classic parity, preserving its original price, card/capacity/bonus/successor and receipt format. Strict legacy physical revival stays classic. No subsidy or extra seller source; full combinations and assurance are open.', evidence: ['game/nexus-emperor-secret-ally.ts', 'game/nexus-emperor-purchase.ts', 'game/auction-continuation-quote.ts', 'game/engine.ts', 'game/ambassador-phase.ts', 'game/ambassador-resume.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Existing uniform bank payment, private eligible Use and original next-lot controls now work for native buyers. Actual390px Richese/Ecaz Homeworld-Tech and Basic Tleilaxu retain price; wrong-face CHOAM Pay and Atreides inspection remain. Existing typed revival and seller/Ambassador/joint controls stay connected; complete-mode acceptance remains open.', evidence: ['components/nexus-emperor-secret-ally.tsx', 'components/nexus-emperor-seller-purchase.tsx', 'components/nexus-emperor-ambassador-purchase.tsx', 'components/nexus-cards.tsx', 'components/game-table.tsx', 'components/richese-special-karama.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 purchase panel composes with original bank/Richese seller/Ambassador/special purchase classifications under visible provisional recommendations. No fake money, auction, beneficiary veto or broader Kull admission. Full combination/assurance/publication gates remain; CHOAM closing sales/trades are not direct purchase producers.', evidence: ['docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', 'docs/RULE_DECISIONS.md', 'docs/RICHESE_AMBASSADOR_RULES.md', 'docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md', 'docs/RICHESE_ACQUISITION_RULES.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Native bank16-file union217/217, types/lint/build and thirty-six real API profiles pass: all six natives Basic/Advanced Pay/Use and Advanced Homeworld/Tech Pay/Use. Four actual390px surfaces prove Richese5 to5/Ecaz12 to12 with modules unchanged, Basic Tleilaxu5 to5 and Guild-card CHOAM Pay14 to12, one card/index0 to1 and actual next Atreides inspection, no overflow/errors. Prior joint112/112, Ambassador111/111, seller108/108 and other evidence remains; broad assurance and deployed acceptance are 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', 'tests/fixture-nexus-emperor-ambassador.ts', 'tests/nexus-emperor-ambassador-purchase.test.ts', 'tests/nexus-emperor-ambassador-runtime.test.ts', 'tests/fixture-nexus-emperor-special.ts', 'tests/nexus-emperor-special-purchase.test.ts', 'tests/nexus-emperor-special-runtime.test.ts', 'tests/fixture-nexus-emperor-bank.ts', 'tests/nexus-emperor-bank-profile.test.ts', 'tests/nexus-emperor-bank-runtime.test.ts'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Native bank16-file union217/217, types/lint/build and thirty-six real API profiles pass: all six natives Basic/Advanced Pay/Use and Advanced Homeworld/Tech Pay/Use. Four actual390px surfaces prove Richese5 to5/Ecaz12 to12 with modules unchanged, Basic Tleilaxu5 to5 and Guild-card CHOAM Pay14 to12, one card/index0 to1 and actual next Atreides inspection, no overflow/errors. Prior joint112/112, Ambassador111/111, seller108/108 and other evidence remains; broad assurance and deployed acceptance are open.', evidence: ['tests/nexus-emperor-short-revival.test.ts', 'tests/fixture-nexus-short-revival.ts', 'tests/nexus-short-revival-runtime.test.ts', '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', 'tests/fixture-nexus-emperor-ambassador.ts', 'tests/nexus-emperor-ambassador-purchase.test.ts', 'tests/nexus-emperor-ambassador-runtime.test.ts', 'tests/fixture-nexus-emperor-special.ts', 'tests/nexus-emperor-special-purchase.test.ts', 'tests/nexus-emperor-special-runtime.test.ts', 'tests/fixture-nexus-emperor-bank.ts', 'tests/nexus-emperor-bank-profile.test.ts', 'tests/nexus-emperor-bank-runtime.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -6133,10 +6133,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'One Karama response covers the entire declared territory set. Cancellation preserves every original advisor group and lock while leaving the Nexus spent. Under the explicit recommended in-place card exception, Cunning may include freshly accompanied or storm-bound/separated territories. This is provisional, not publisher clarification. Ordinary flipping/arrival, allied capacity, storm movement/casualties and battle geometry remain unchanged; broader module acceptance remains open.',
|
||||
'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.',
|
||||
'Emperor Cunning requires a positive own ordinary battle pool and no actual Sardaukar in Advanced, before submitting your plan. The provisional shortage policy uses five when available, otherwise every eligible counter; zero and chosen underfill are unavailable. The played Nexus card opens one Karama response. Allowing it makes the quoted ordinary group count as Sardaukar for this battle; cancellation prevents the entire enhancement and leaves the Nexus card spent.',
|
||||
'Temporary Sardaukar use applicable strength/support rules, including the Fremen exception, but remain physical ordinary counters. Losses go to Tanks as ordinary, without changing starred pools. Existing inspected/promised plans bind and the role ends after this battle; fewer-than-five remains unavailable. Secret Ally independent revival and original bank/seller/Ambassador/joint special purchases have connected controls; compulsory funding stays separate. Full combinations remain open.',
|
||||
'Original normal bank-auction purchase now uses canonical native Nexus roster/module admission and exact prior classic parity. Uniform winner payment is private-face independent. Actual unallied holder proves a positive full own price, spends one Nexus and acquires one original lot without bank payment. Native card capacity, bonus and next Atreides inspection remain; Richese ordinary pool is not a seller/cache route. Bank receipt format is unchanged, and strict legacy physical revival remains separately classic without replay. Unsupported native aliases and full-mode gates stay closed.',
|
||||
'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.',
|
||||
'Emperor Secret Ally revival selects three real counters when eligible, otherwise the full positive typed-eligible group under the visible provisional shortage policy. It is free and 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 retain it and leave seller/bank unpaid under the visible provisional recommendation. Actual stock, hand limits, native Harkonnen bonus and successor run once. Legal self-cache is included; self-Black remains blocked. Original self-discard gives no acquisition or Emperor Nexus spend.',
|
||||
'Original Richese Ambassador trigger offers the beneficiary uniform Pay or eligible Emperor Nexus Use, independently of hidden face or private availability. Under provisional purchase composition, an actual unallied holder proves/retains three own spice and acquires one original deck card, with no bank payment, reimbursement or fake auction. Native source phase/resume authority, private inability, draw/recycling, bonus and movement/worm suffix remain. An ally cannot Use; forming alliance disposes held Nexus originally. CHOAM closing sales/trades are not direct purchases.',
|
||||
'Original Advanced Richese special Karama may jointly spend its held activation plus actual Emperor Nexus, after proving three own uncommitted spice. The provisional purchase composition retains that price and acquires one exact cache card at native commit, preserving original once-use, capacity, reserved-card and enclosing controls. Full hand exchanges within its actual limit; final depletion continues to ordinary Bidding. No bank payment, reimbursement, ally bonus, fake auction or public automatic pause. Ordinary paid use remains; broader Kull admission is not invented.',
|
||||
@@ -6176,7 +6176,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{area:"Player controls",status:'Partial',detail:"Richese pair controls retain the purchased combined tariff and persistent bank/rate/cancellation explanation. Fremen table SSR retains original Cunning spend/ride, Betrayal and Great Maker controls; an actual pending Sandmaster quote also renders its shared checkbox. Native Betrayal/Atomics table SSR retains typed/concealed return, revelation, ordered arrival, Aftermath and desert controls; CSS stubs mean no visual/browser acceptance. Earlier selected E3 lead/plan, assassination/revival and Duke controls retain their dated proof. Four earlier original-action390px journeys prove native Junction5/full2, poor1/full2 release plus ordinary one-force replacement, still-held source sand2 disabled then five-force stronghold1, and independent purchased1. The response explains the spent-card consequence. No overflow, browser error or application exception; two tool-only stale-node errors were resolved by fresh evidence without replay. Earlier Junction/loan/refund surfaces remain scoped evidence.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx", "components/junction-transport.tsx"]},
|
||||
{area:"AI",status:'Partial',detail:"The Richese minimal pair candidate and shared payment guard use the original beneficiary purchased rate; strategy remains deferred. All four policies retain original Fremen Cunning and both Betrayal declarations with shared destination legality. All four policies retain minimal legal Ecaz Betrayal and original Aftermath/Sneak participation. All four policies execute original selected E3 Duke Cunning and unused expiry, and legal Occupy plans with real World sources. All four original policies also execute a legal still-held Richese full-rate one-counter alternative after native cancellation, with actual physical delivery and card cost. Shared human/AI quotes no longer promise a canceled discount. Native permission alternatives, recipient-specific lender prices and immune Junction source choice remain. Optional non-reoffer is not forced. No strategic first-candidate pin, tuning or calibration.",evidence:['game/richese-cunning-rate.ts', 'game/homeworld-payment-options.ts', '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:"The native Richese pair/purchased rate composition and retained provisional exact-card refund are explicit in its canonical source contract. Selected E3 scope, printed two-seat no-alliance draw limit and remaining individual-effect envelopes are explicit in the canonical Nexus contract. Richese printed one-counter pricing and native Guild eligibility stay distinct from absent-faction cancellation, physical force count and independent purchased price. Unfunded spent-card/no-refund release is selected existing no-debt composition, not a new publisher worked example. Immune Junction and earlier permission/refund/loan interpretations remain explicit. Full rules and public gates stay open.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md", "docs/JUNCTION_TRANSPORT_RULES.md", "docs/JUNCTION_TRANSPORT_RUNTIME.md"]},
|
||||
{area:"Verification",status:'Partial',detail:"Independent Discovery adds frozen15-file118/118, types/lint/build and six original-action/rendered-control programmes; broader modes, visual/deployed acceptance and final assurance remain open. Native Fremen modules pass frozen14-file86/86, types/lint/build and nine original-action/rendered-control programmes. HMS has genuine runtime proof; Discovery destination arrival remains quote-only evidence. Earlier native Betrayal/Atomics composition passes frozen16-file115/115, types/lint/build and eight finite original-action/table-SSR programmes, including original Cunning desert Aftermath; visual/browser and deployed acceptance remain open. Earlier selected E3 entry, Occupy, Assassinate/Recruits, Duke and arrival proof remains dated in the canonical contract. Earlier selected E1/E2 native card/cancellation proof remains dated in the canonical Nexus contract. Classic core retains original World/Tech/Stronghold modules with base33/selected36 and optional14. Independent World cases use native setup, population-sensitive Recruits, typed foreign Harass and actual own-Kaitain Reinforcements/Cunning. The local fee reduces only its paid temporary roles; original count5 and real stars remain. Earlier phase/card proofs are dated in the canonical contract. Native-family/Discovery/Semuta envelopes, full-mode and comprehensive assurance remain open.",evidence:['tests/independent-discovery-nexus-entry.test.ts', 'tests/independent-discovery-cards.test.ts', 'tests/independent-discovery-battles.test.ts', 'tests/fremen-cunning-modules.test.ts','tests/fixture-fremen-cunning-modules.ts','tests/fremen-betrayal-modules.test.ts','tests/fixture-fremen-betrayal-modules.ts','tests/nexus-fremen-cunning-modules-quotes.test.ts','tests/ecaz-betrayal-modules.test.ts','tests/fixture-ecaz-betrayal-modules.ts','tests/richese-no-field-hidden-return.test.ts','tests/nexus-ecaz-betrayal-quotes.test.ts','tests/moritani-atomics-modules.test.ts','tests/nexus-moritani-atomics-runtime.test.ts','tests/nexus-world-independent-cards.test.ts','tests/nexus-world-independent-battles.test.ts','tests/reinforcements-temporary-role.test.ts','tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-neLine truncated
|
||||
{area:"Verification",status:'Partial',detail:"Independent Discovery adds frozen15-file118/118, types/lint/build and six original-action/rendered-control programmes; broader modes, visual/deployed acceptance and final assurance remain open. Native Fremen modules pass frozen14-file86/86, types/lint/build and nine original-action/rendered-control programmes. HMS has genuine runtime proof; Discovery destination arrival remains quote-only evidence. Earlier native Betrayal/Atomics composition passes frozen16-file115/115, types/lint/build and eight finite original-action/table-SSR programmes, including original Cunning desert Aftermath; visual/browser and deployed acceptance remain open. Earlier selected E3 entry, Occupy, Assassinate/Recruits, Duke and arrival proof remains dated in the canonical contract. Earlier selected E1/E2 native card/cancellation proof remains dated in the canonical Nexus contract. Classic core retains original World/Tech/Stronghold modules with base33/selected36 and optional14. Independent World cases use native setup, population-sensitive Recruits, typed foreign Harass and actual own-Kaitain Reinforcements/Cunning. The local fee reduces only its paid temporary roles; original count5 and real stars remain. Earlier phase/card proofs are dated in the canonical contract. Native-family/Discovery/Semuta envelopes, full-mode and comprehensive assurance remain open.",evidence:['tests/nexus-short-sardaukar.test.ts', 'tests/fixture-nexus-short-cunning.ts', 'tests/nexus-short-cunning.test.ts', 'tests/independent-discovery-nexus-entry.test.ts', 'tests/independent-discovery-cards.test.ts', 'tests/independent-discovery-battles.test.ts', 'tests/fremen-cunning-modules.test.ts','tests/fixture-fremen-cunning-modules.ts','tests/fremen-betrayal-modules.test.ts','tests/fixture-fremen-betrayal-modules.ts','tests/nexus-fremen-cunning-modules-quotes.test.ts','tests/ecaz-betrayal-modules.test.ts','tests/fixture-ecaz-betrayal-modules.ts','tests/richese-no-field-hidden-return.test.ts','tests/nexus-ecaz-betrayal-quotes.test.ts','tests/moritani-atomics-modules.test.ts','tests/nexus-moritani-atomics-runtime.test.ts','tests/nexus-world-independent-cards.test.ts','tests/nexus-world-independent-battles.test.ts','tests/reinforcements-temporary-role.test.ts','tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/pLine truncated
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -6385,7 +6385,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When reviving an own leader while you have no Skill Card, decide whether to draw before seeing two private cards. Once drawn, keep one and assign it to that revived leader. Under the 13 October 2026 ruling the original faction keeps its assignment while its trained leader is captured, so reviving a different own leader is legal but draws no replacement while that card exists.',
|
||||
'Planetologist chooses either one extra movement range (maximum three) or forces from two territories to one destination at normal range. These alternatives do not stack. Provisional mixed BG advisor/fighter gather into an enemy-only territory declares one common fighter arrival before either group moves. Native Karama cancellation keeps both sources and the movement allowance unchanged, and blocks another advisor flip for that movement opportunity; fresh-advisor locks remain. Empty or already BG-occupied destinations keep their original common stance. The actual skilled leader can use an eligible green Special in the weapon slot for +2 if it survives; discard that Special without its ordinary effect. Other combined modules remain unfinished.',
|
||||
'Suk Graduate returns one selected casualty to reserves while its leader stays face up, or lets its surviving battle leader save up to three: one stays in its original sector and the rest return to reserves. The effects do not stack. Rescued counters still count toward Advanced Atreides’ seven battle losses for Kwisatz Haderach; combined modules remain unfinished.',
|
||||
'In the supported classic Nexus/Skills composition, Emperor Cunning counts five actual ordinary counters as temporary Sardaukar. Normal or trained Suk rescue still saves physical ordinary casualties. The original casualty allocation remains pending while the rescue choice is open; physical counters move only when rescue settles. Original support is already paid and is not refunded or charged twice. A normal save returns one to reserves; a trained save keeps one and returns the rest. Actual starred reserves and Tanks remain unchanged.',
|
||||
'In the supported classic Nexus/Skills composition, Emperor Cunning counts five actual ordinary counters as temporary Sardaukar, or the full positive eligible short group under the provisional shortage policy. Normal or trained Suk rescue still saves physical ordinary casualties. The original casualty allocation remains pending while the rescue choice is open; physical counters move only when rescue settles. Original support is already paid and is not refunded or charged twice. A normal save returns one to reserves; a trained save keeps one and returns the rest. Actual starred reserves and Tanks remain unchanged.',
|
||||
'A surviving selected Mentat adds two battle strength. A surviving selected Bureaucrat subtracts one from the opponent’s total per distinct stronghold occupied by that opponent; advisors do not count. Printed disc values and bounties stay unchanged.',
|
||||
'A living native Sandmaster may collect one spice per entered territory along a declared legal ground route. With multiple positive piles, provisionally choose one debit sector per territory or decline; two piles never grant two spice from the same territory. Canceled moves earn nothing. The initial source and a sector-only reposition earn nothing. Separate movements have separate opportunities. Native worm rides and physical Fremen Ambassador relocation offer one selected destination pile before arrival reactions, without an invented route or normal movement use. Native Ixian HMS passengers choose one pile in each entered territory before faction collection, including supplied Advanced special Karama; its route preview reflects both transfers. These allocations are provisional. Other special relocation remains unfinished.',
|
||||
'A surviving skilled Sandmaster winner adds three board spice to an existing pile in the battle territory, even with no surviving forces. No pile adds nothing; a single pile resolves automatically. With several remaining positive piles, the winner selects one sector after any Smuggler allocation. Owner selection is a visibly provisional placement policy, not a publisher ruling.',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DUKE_VIDAL_ID } from './duke-vidal';
|
||||
import { resolveEcazDukeRevival } from './ecaz-duke-revival';
|
||||
import {
|
||||
emperorNexusPools,
|
||||
emperorNexusRevivalCount,
|
||||
validateEmperorNexusRevival,
|
||||
} from './nexus-emperor-secret-ally';
|
||||
import { validateNexusCards } from './nexus-cards';
|
||||
@@ -87,14 +88,15 @@ function validateNexusEmperorSource(
|
||||
requireResume(
|
||||
typeof event === 'string' && event.length > 0 &&
|
||||
Object.keys(pending).every((key) => nexusEmperorPendingKeys[key]) &&
|
||||
pending.kind === 'forces' && pending.amount === 3 &&
|
||||
pending.kind === 'forces' && Number.isSafeInteger(pending.amount) &&
|
||||
pending.amount! >= 1 && pending.amount! <= 3 &&
|
||||
Number.isSafeInteger(pending.elite) && pending.elite! >= 0 &&
|
||||
pending.elite! <= 3 && pending.free === 3 &&
|
||||
pending.elite! <= pending.amount! && pending.free === pending.amount &&
|
||||
pending.cost === 0 && pending.normalCost === 0 &&
|
||||
pending.payer === undefined && pending.emperorExtra === undefined &&
|
||||
pending.leader === undefined && Array.isArray(pending.checks) &&
|
||||
pending.checks.length === 0,
|
||||
'The Emperor Nexus source needs its original exact-three free request.',
|
||||
'The Emperor Nexus source needs its original physical free-force request.',
|
||||
);
|
||||
const owner = g.players.find((p) => p.id === pending.player);
|
||||
const history = g.nexusEmperorSecretHistory;
|
||||
@@ -117,6 +119,7 @@ function validateNexusEmperorSource(
|
||||
receipt.version === 2 && receipt.stage === 'requested' &&
|
||||
receipt.owner === owner.id && receipt.turn === g.turn &&
|
||||
receipt.phase === g.phase && receipt.elite === pending.elite &&
|
||||
emperorNexusRevivalCount(receipt) === pending.amount &&
|
||||
receipt.after === null &&
|
||||
receipt.before && Object.entries(currentPools).every(([key, value]) =>
|
||||
receipt.before[key as keyof typeof currentPools] === value) &&
|
||||
|
||||
Reference in new issue
Block a user