Connect Fremen Nexus protection before original worm devouring

This commit is contained in:
Quintine committed 2026-10-15 01:58:23 +11:00
1 parent 8ab827b902
commit 82f1aae51b
17 files changed
+1425 -12

No files matched your search

+1
View File
@@ -108,6 +108,7 @@ const decisions = {
greatMakerRide: true,
wormRide: true,
nexusFremenCunningOffer: true,
nexusFremenProtection: true,
nexusFremenCunningRide: true,
moritaniRetention: true,
moritaniSpecialKarama: true,
+6
View File
@@ -2684,6 +2684,12 @@ function policyActions(g: GameView): Action[] {
if (d.kind === 'jacurutuDial')
return g.jacurutuDial?.choices.map((_,allocation) => ({type:'decision',event:d.event,allocation})) ?? [];
if (d.kind === 'greatMakerVote' || d.kind === 'greatMakerRide') return greatMakerBotActions(g);
if (d.kind === 'nexusFremenProtection') {
const offer = g.nexusFremenProtection;
return offer && !offer.blocked
? [{ type: 'nexusFremenProtection', event: offer.event, use: offer.canUse }]
: [];
}
if (d.kind === 'nexusFremenCunningOffer')
return [{ type: 'decision', event: d.event,
accept: g.nexusCards?.card === 'fremen' && me.faction === 'fremen' &&
+115
View File
@@ -48,6 +48,7 @@ import { EMPEROR_NEXUS_REVIVALS, emperorNexusEvent, emperorNexusSignature, emper
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
import { FREMEN_NEXUS_FREE_FORCES, fremenNexusRevivalOffer, fremenNexusRevivalPools, fremenNexusRevivalSignature, validateFremenNexusRevival, type FremenNexusRevival } from './nexus-fremen-revival';
import { captureFremenCunningOccurrence, quoteFremenCunning, quoteFremenCunningRide, validateFremenCunningSelection, type FremenCunningOccurrence, type FremenCunningRideAuthorization, type FremenCunningSelection } from './nexus-fremen-cunning';
import { createNexusFremenProtection, currentNexusFremenProtectionOwner, answerNexusFremenProtection, validateNexusFremenProtection, type NexusFremenProtectionFrame, type NexusFremenProtectionInput } from './nexus-fremen-protection';
import { truthKnowledgeOf } from './truthtrance-knowledge';
import { createMoritaniAssassinateOpportunity, moritaniAssassinateTrigger, moritaniAssassinateChoices, quoteMoritaniAssassinate, moritaniAssassinateSignature, validateMoritaniAssassinate, type MoritaniAssassinateState, type MoritaniAssassinateReceipt } from './moritani-assassinate';
import { createNexusGuildSecretAlly, validateNexusGuildSecretAlly, quoteNexusGuildSecretShipment, type NexusGuildSecretAllyReceipt } from './nexus-guild-secret-ally';
@@ -958,6 +959,7 @@ export type Decision =
}
| { kind: 'wormPlacement'; player: string }
| { kind: 'wormProtection'; player: string; territory: string; ally: string }
| { kind: 'nexusFremenProtection'; player: string; event: string }
| { kind: 'discoveryDiscard'; player: string; event: string }
| { kind: 'discoveryEntry'; player: string; event: string }
| { kind: 'ecologicalStorm'; player: string; event: string }
@@ -1899,6 +1901,8 @@ export type Game = {
/** Optional independent module; complete effect coverage is still release-gated. */
nexusCards?: { cards: NexusState | null; phase: NexusCardPhase | null } | null;
nexusTraitorExchanges?: NexusTraitorExchange[];
/** Original public threatened-seat queue; a spent card protects only its owner. */
nexusFremenProtection?: NexusFremenProtectionFrame;
/** An initially empty actual worm occurrence; the optional card is not disclosed by this offer. */
nexusFremenCunningOffer?: { occurrence: FremenCunningOccurrence; owner: string } | null;
/** Accepted appearances wait for post-Nexus riding, then retain completion custody. */
@@ -10675,6 +10679,26 @@ function devour(
}
function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summoned' = 'natural') {
const fremen = byFaction(g, 'fremen');
if (!fremen && g.nexusCards?.cards && g.players.some(p =>
!p.ally && g.nexusCards!.cards!.hands[p.id] && at(p, t) > 0)) {
const frame = nexusRule(() => createNexusFremenProtection({
event: crypto.randomUUID(), turn: g.turn, territory: t, origin,
...(origin === 'summoned' ? { parent: g.summonedWorm?.event }
: { pile: g.spiceSequence?.pile,
index: g.spiceSequence ? g.spiceDiscard[g.spiceSequence.pile].length - 1 : undefined }),
order: g.order,
players: g.players.map(p => ({ id: p.id, faction: p.faction, ally: p.ally,
forces: p.forces, noField: p.noField, hasNexus: !!g.nexusCards!.cards!.hands[p.id] })),
}));
if (frame) {
requireRule(g.nexusFremenProtection?.stage !== 'choose',
'Finish the original worm protection before another appearance.');
g.nexusFremenProtection = frame;
g.decision = { kind: 'nexusFremenProtection',
player: currentNexusFremenProtectionOwner(frame)!, event: frame.event };
return;
}
}
// An empty appearance offers the same decision with or without the private
// Fremen card; the offer itself must not disclose its custody.
if ((origin !== 'summoned' || (g.summonedWorm?.event &&
@@ -10716,6 +10740,87 @@ function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summo
};
} else wormSurvival(g, t);
}
function nexusFremenProtectionInput(g: Game, frame: NexusFremenProtectionFrame): NexusFremenProtectionInput {
return {
event: frame.event, turn: g.turn, territory: frame.territory, origin: frame.origin,
...(frame.origin === 'summoned' ? { parent: g.summonedWorm?.event }
: { pile: g.spiceSequence?.pile,
index: g.spiceSequence ? g.spiceDiscard[g.spiceSequence.pile].length - 1 : undefined }),
order: g.order,
players: g.players.map(p => ({ id: p.id, faction: p.faction, ally: p.ally,
forces: p.forces, noField: p.noField,
// An anytime Harkonnen exchange may spend an irrelevant card after this
// public membership was captured; it cannot erase the original answer.
hasNexus: frame.order.includes(p.id) })),
};
}
function nexusFremenProtectionIntegrity(g: Game) {
const frame = g.nexusFremenProtection;
const controls = [g, g.pendingExchange, g.pendingNullentropy?.resume,
g.pendingRicheseGift?.resume, g.pendingRichesePurchaseIncome?.resume,
g.summonedWorm?.resume];
const decisions = controls.flatMap(control =>
control?.decision?.kind === 'nexusFremenProtection' ? [control.decision] : []);
if (!frame || frame.stage === 'complete') {
requireRule(!decisions.length, 'Worm protection has lost its original active occurrence.');
return;
}
requireRule(g.status === 'playing' && g.phase === 1 && g.nexusCards?.cards &&
!byFaction(g, 'fremen'), 'Worm protection requires its original absent-Fremen Nexus module.');
nexusRule(() => validateNexusFremenProtection(nexusFremenProtectionInput(g, frame), frame));
requireRule(decisions.length === 1 && decisions[0].event === frame.event &&
decisions[0].player === currentNexusFremenProtectionOwner(frame),
'The original worm protection has lost its current threatened seat.');
if (frame.origin === 'summoned')
requireRule(g.summonedWorm?.territory === frame.territory,
'Worm protection has lost its summoned territory.');
else {
const card = frame.pile === undefined || frame.index === undefined ? undefined :
g.spiceDiscard[frame.pile][frame.index];
requireRule(card && 'worm' in card && !card.suppressed,
'Worm protection has lost its actual unconsumed appearance.');
}
if (frame.protected) requireRule(g.nexusCards.cards.discard.includes('fremen') &&
!getPlayer(g, frame.protected).ally,
'The pending worm protection has lost its spent card or unallied owner.');
}
function nexusFremenProtectionOffer(g: Game, id: string) {
const frame = g.nexusFremenProtection;
if (!frame || currentNexusFremenProtectionOwner(frame) !== id ||
g.decision?.kind !== 'nexusFremenProtection' || g.decision.player !== id ||
g.decision.event !== frame.event) return null;
const p = getPlayer(g, id);
const blocked = g.truthtrance || pendingNexusTraitors(g) || g.response ||
g.pendingKarama || g.pendingTreacheryDiscard || g.pendingNullentropy
? 'Finish the current private response before worm protection.' : null;
return { event: frame.event, territory: frame.territory, owner: id,
canUse: !frame.protected && !p.ally &&
g.nexusCards?.cards?.hands[id] === 'fremen' && at(p, frame.territory) > 0,
blocked };
}
function playNexusFremenProtection(g: Game, p: Player, action: Action) {
const frame = g.nexusFremenProtection, offer = nexusFremenProtectionOffer(g, p.id);
requireRule(frame && offer && !offer.blocked && action.event === offer.event &&
typeof action.use === 'boolean' &&
Object.keys(action).every(key => ['type', 'event', 'use'].includes(key)),
'Choose only your current worm protection event with Use or Pass.');
requireRule(!action.use || offer.canUse,
'Only the actual unallied Fremen Nexus holder may protect its threatened forces.');
const next = nexusRule(() => answerNexusFremenProtection(frame, p.id, action.use as boolean));
if (action.use) {
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
log(g, `${p.name} spent Fremen Nexus Secret Ally to protect only their forces from this Shai-Hulud. Provisional policy: this independent protection has no additional Karama response.`,
{ faction: p.faction, name: 'Fremen Nexus protection' });
}
g.nexusFremenProtection = next;
const owner = currentNexusFremenProtectionOwner(next);
if (owner) g.decision = { kind: 'nexusFremenProtection', player: owner, event: next.event };
else {
g.decision = null;
devour(g, next.territory, next.protected ?? undefined);
afterWorm(g);
}
}
function currentFremenCunningResponse(g: Game, response: ResponseWindow) {
const ride = g.nexusFremenCunningRides?.find(row =>
row.stage === 'pending' && row.occurrence.event === response.intent);
@@ -11099,6 +11204,11 @@ function greatMakerIntegrity(g: Game) {
});
if (frame.stage === 'worm') requireRule(
controls.some(control => control?.decision?.kind === 'wormProtection' && control.decision.territory === frame.territory && control.decision.player === byFaction(g,'fremen')?.id && control.decision.ally === byFaction(g,'fremen')?.ally) ||
controls.some(control => control?.decision?.kind === 'nexusFremenProtection' &&
g.nexusFremenProtection?.stage === 'choose' &&
g.nexusFremenProtection.territory === frame.territory &&
g.nexusFremenProtection.pile === frame.pile &&
g.nexusFremenProtection.index === frame.index) ||
responses.some(response => (response?.kind === 'wormSurvival' || response?.kind === 'wormAllyProtection') && response.location === frame.territory && response.owner === byFaction(g,'fremen')?.id &&
(!response.recipient || response.recipient === byFaction(g,'fremen')?.ally)),
'The Great Maker has lost its pending worm survival or protection interaction.');
@@ -20716,6 +20826,7 @@ function marketGholaIntegrity(g: Game) {
nexusBorrowedVoiceIntegrity(g);
nexusFremenRevivalIntegrity(g);
nexusFremenCunningIntegrity(g);
nexusFremenProtectionIntegrity(g);
nexusEcazBetrayalIntegrity(g);
nexusTraitorIntegrity(g);
nexusFaceDancerIntegrity(g);
@@ -28944,6 +29055,7 @@ function applyActionInner(
if (t === 'nexusFaceDancers') { playNexusFaceDancers(g, p, action); return g; }
if (t === 'nexusTleilaxuRevival') { playNexusTleilaxuRevival(g,p,action); return g; }
if (t === 'nexusBorrowedVoice') { playNexusBorrowedVoice(g, p, action); return g; }
if (t === 'nexusFremenProtection') { playNexusFremenProtection(g, p, action); return g; }
if (t === 'nexusSuboids') { playNexusSuboids(g, p, action); return g; }
if (t === 'nexusAdvisors') { playNexusAdvisors(g, p, action); return g; }
if (t === 'nexusSardaukar') { playNexusSardaukar(g,p,action); return g; }
@@ -29333,6 +29445,8 @@ function applyActionInner(
}
if (g.decision) {
const decision = g.decision;
requireRule(decision.kind !== 'nexusFremenProtection',
'Use only the original worm protection event with Use or Pass.');
if (decision.kind === 'nexusIxianReplacement') {
requireRule(false, 'Use only the current purchased-card event with Use or Pass.');
}
@@ -33213,6 +33327,7 @@ export function viewGame(state: Game, id: string) {
nexusBgBetrayal: nexusBgBetrayalOffer(g, id),
nexusBorrowedVoice: nexusBorrowedVoiceOffer(g, id),
nexusFremenRevival: fremenNexusRevivalOffer(g,id),
nexusFremenProtection: nexusFremenProtectionOffer(g, id),
nexusMoritani: nexusMoritaniOffer(g,id),
nexusMoritaniBetrayal: moritaniBetrayalOffer(g,id,
g.grummanCollection?.stage === 'waiting' && grummanCollectionAutomatic(g)),
+241
View File
@@ -0,0 +1,241 @@
import { TERRITORIES, splitLocation, validLocation } from './board';
import { SPICE_CARDS } from './cards';
import { presenceAt, type ForcePresence } from './force-presence';
/** Public identity of one accepted worm, captured before its original devouring. */
export type NexusFremenProtectionContext = Readonly<{
event: string;
turn: number;
territory: string;
origin: 'natural' | 'additional' | 'summoned';
pile?: 0 | 1;
index?: number;
parent?: string;
}>;
/** Deliberately excludes card faces, No-Field values and token inventory. */
export type NexusFremenProtectionPlayer = ForcePresence & Readonly<{
id: string;
faction: string;
ally?: string | null;
hasNexus: boolean;
}>;
export type NexusFremenProtectionInput = NexusFremenProtectionContext & Readonly<{
order: readonly string[];
players: readonly NexusFremenProtectionPlayer[];
}>;
export type NexusFremenProtectionFrame = NexusFremenProtectionContext & Readonly<{
stage: 'choose' | 'complete';
order: readonly string[];
passed: readonly string[];
protected: string | null;
}>;
function requireProtection(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
const identity = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0;
const object = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
function validateContext(context: NexusFremenProtectionContext): void {
requireProtection(
object(context) && identity(context.event) &&
Number.isSafeInteger(context.turn) && context.turn > 0 &&
TERRITORIES.some((territory) => territory.id === context.territory &&
(territory.type === 'sand' || SPICE_CARDS.some(([id]) => id === territory.id))) &&
['natural', 'additional', 'summoned'].includes(context.origin),
'Invalid original Fremen protection occurrence.',
);
requireProtection(
context.origin === 'summoned'
? identity(context.parent) && context.pile === undefined && context.index === undefined
: context.parent === undefined &&
(context.pile === undefined || context.pile === 0 || context.pile === 1) &&
(context.index === undefined ||
(Number.isSafeInteger(context.index) && context.index >= 0)) &&
(context.origin !== 'natural' ||
(context.pile !== undefined && context.index !== undefined)),
'Invalid original Fremen protection source binding.',
);
}
function validateIds(ids: readonly string[]): void {
requireProtection(Array.isArray(ids), 'Invalid Fremen protection decision order.');
const seen = new Set<string>();
for (const id of ids) {
requireProtection(
identity(id) && !seen.has(id),
'Invalid Fremen protection decision order.',
);
seen.add(id);
}
}
function validateInput(input: NexusFremenProtectionInput): void {
validateContext(input);
validateIds(input.order);
requireProtection(Array.isArray(input.players), 'Invalid public protection seats.');
const ids = new Set<string>();
for (const player of input.players) {
requireProtection(
object(player) && identity(player.id) && !ids.has(player.id) &&
identity(player.faction) && typeof player.hasNexus === 'boolean' &&
object(player.forces),
'Invalid public protection seat.',
);
ids.add(player.id);
for (const [key, count] of Object.entries(player.forces)) {
const at = splitLocation(key);
if (at.territory !== input.territory) continue;
requireProtection(
key === `${at.territory}:${at.sector}` &&
validLocation(at.territory, at.sector) &&
typeof count === 'number' && Number.isSafeInteger(count) && count >= 0,
'Invalid public protection force presence.',
);
}
if (player.noField !== undefined && player.noField !== null) {
requireProtection(object(player.noField), 'Invalid public No-Field presence.');
const deployed = player.noField.deployed;
requireProtection(
deployed === null ||
(object(deployed) && object(deployed.location) &&
identity(deployed.location.territory) &&
typeof deployed.location.sector === 'number' &&
Number.isSafeInteger(deployed.location.sector) &&
(deployed.location.territory !== input.territory ||
validLocation(deployed.location.territory, deployed.location.sector))),
'Invalid public No-Field location.',
);
}
}
requireProtection(
input.order.length === ids.size && input.order.every((id) => ids.has(id)),
'Fremen protection needs the original storm order of every seat.',
);
for (const player of input.players)
requireProtection(
player.ally === undefined || player.ally === null ||
(identity(player.ally) && player.ally !== player.id && ids.has(player.ally)),
'Invalid public protection alliance.',
);
}
function candidates(input: NexusFremenProtectionInput): string[] {
if (input.players.some((player) => player.faction === 'fremen')) return [];
const eligible = new Set<string>();
for (const player of input.players)
if (!player.ally && player.hasNexus && presenceAt(player, input.territory) > 0)
eligible.add(player.id);
return input.order.filter((id) => eligible.has(id));
}
function validateFrame(frame: NexusFremenProtectionFrame): void {
validateContext(frame);
const keys = ['event', 'turn', 'territory', 'origin', 'stage', 'order', 'passed', 'protected'];
if (frame.pile !== undefined) keys.push('pile');
if (frame.index !== undefined) keys.push('index');
if (frame.parent !== undefined) keys.push('parent');
requireProtection(
Object.keys(frame).sort().join(',') === keys.sort().join(',') &&
(frame.stage === 'choose' || frame.stage === 'complete'),
'Invalid saved Fremen protection frame.',
);
validateIds(frame.order);
validateIds(frame.passed);
requireProtection(
frame.order.length > 0 && frame.passed.length <= frame.order.length &&
frame.passed.every((id, index) => id === frame.order[index]),
'Fremen protection acknowledgements must be an original ordered prefix.',
);
requireProtection(
frame.protected === null ||
(identity(frame.protected) && frame.passed.includes(frame.protected)),
'Fremen protection must belong to one acknowledged original owner.',
);
requireProtection(
(frame.stage === 'complete') === (frame.passed.length === frame.order.length),
'Fremen protection must close the entire original queue.',
);
}
/** Public candidates only; a wrong private card face still receives the same queue. */
export function createNexusFremenProtection(
input: NexusFremenProtectionInput,
): NexusFremenProtectionFrame | null {
validateInput(input);
const order = candidates(input);
if (!order.length) return null;
return {
event: input.event,
turn: input.turn,
territory: input.territory,
origin: input.origin,
...(input.pile === undefined ? {} : { pile: input.pile }),
...(input.index === undefined ? {} : { index: input.index }),
...(input.parent === undefined ? {} : { parent: input.parent }),
stage: 'choose',
order,
passed: [],
protected: null,
};
}
export function currentNexusFremenProtectionOwner(
frame: NexusFremenProtectionFrame,
): string | null {
validateFrame(frame);
return frame.stage === 'complete' ? null : frame.order[frame.passed.length];
}
/** The parent must authenticate and spend the actual Fremen card before committing
* this result. `passed` is the acknowledgement prefix, including a Use answer.
* Protection does not skip any remaining acknowledgement or grant worm riding. */
export function answerNexusFremenProtection(
frame: NexusFremenProtectionFrame,
actor: string,
use: boolean,
): NexusFremenProtectionFrame {
validateFrame(frame);
requireProtection(typeof use === 'boolean', 'Choose Use or Pass for Fremen protection.');
requireProtection(
frame.stage === 'choose' && actor === frame.order[frame.passed.length],
'Only the current original owner may answer Fremen protection.',
);
requireProtection(!use || frame.protected === null, 'This worm already has its protected owner.');
const passed = [...frame.passed, actor];
return {
...frame,
passed,
protected: use ? actor : frame.protected,
stage: passed.length === frame.order.length ? 'complete' : 'choose',
};
}
/** Validate against the original public pre-devour snapshot, not live hands after
* expenditure or board presence after devouring. This is not card entitlement,
* an event-history receipt, or authority to retrofit an already-devoured worm. */
export function validateNexusFremenProtection(
input: NexusFremenProtectionInput,
frame: NexusFremenProtectionFrame,
): void {
validateInput(input);
validateFrame(frame);
requireProtection(
frame.event === input.event && frame.turn === input.turn &&
frame.territory === input.territory && frame.origin === input.origin &&
frame.pile === input.pile && frame.index === input.index && frame.parent === input.parent,
'Fremen protection has lost its original occurrence identity.',
);
const order = candidates(input);
requireProtection(
order.length === frame.order.length &&
order.every((id, index) => id === frame.order[index]),
'Fremen protection has lost its original public candidates and storm order.',
);
}
+7 -6
View File
@@ -4570,7 +4570,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'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.',
'Emperor Betrayal has a battle alternative in Advanced play: an unallied opponent holding the Emperor Nexus card can spend it during the ordinary pre-plan Sardaukar response to suppress actual Sardaukar strength for this battle. Every seat receives the same response opportunity even with no card, keeping hidden custody private. The starred pieces remain physical Sardaukar; no card grants a new force or cancels their separate support benefit.',
'With Fremen absent, an unallied holder may spend Fremen Secret Ally during Revival to return exactly three eligible forces from Tanks to reserves at no spice cost. Unlike Emperor Secret Ally, these three consume the ordinary force and free-force allowances, including the Advanced one-elite cap. This bounded base-faction path keeps prior-return and competing-revival guards; supported classic Tech/Stronghold composition invokes actual Axlotl activity after the batch, independently of allowance accounting. Both free returns collect Tech only at phase end, never as spendable advance spice. Partial returns, prior returns, higher free rates and separate sandworm protection remain unavailable.',
'With Fremen absent, an unallied holder may spend Fremen Secret Ally during Revival to return exactly three eligible forces from Tanks to reserves at no spice cost. Unlike Emperor Secret Ally, these three consume the ordinary force and free-force allowances, including the Advanced one-elite cap. This bounded base-faction path keeps prior-return and competing-revival guards; supported classic Tech/Stronghold composition invokes actual Axlotl activity after the batch, independently of allowance accounting. Both free returns collect Tech only at phase end, never as spendable advance spice. Partial returns, prior returns and higher free rates remain guarded; independent sandworm protection is the separate connected 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.',
'CHOAM Secret Ally has a working Collection trade: when CHOAM is absent, an unallied holder may discard one Worthless card and the Nexus card for two bank spice. After a battle victory, every winner in an eligible Nexus game receives the same use-or-continue window; an unallied CHOAM-card holder may instead spend that card to inspect one random unused card in the opposing combatant’s current hand. Played battle cards remain excluded even if retained. The inspected face is private, while use is public. The universal window protects concealed card custody; this is an interface interpretation, not a publisher-prescribed prompt. An ordinary Karama response to this Secret Ally inspection remains unestablished.',
'With CHOAM seated, an unallied holder may spend CHOAM Betrayal at a clean play boundary to discard one uniformly random held CHOAM Treachery Card without spice compensation. The target name is public, but its hand count is offered only during Bidding; the holder cannot pick or inspect the card before the ordinary discard reveal.',
@@ -4596,11 +4597,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 connected. Tleilaxu Secret Ally retains its provisional shared revival policy. Absent-BG borrowed Voice now uses a neutral attacker-first pre-inspection queue, actual unallied holder and one physical card declaration. Original Karama can cancel only Voice while preserving spent Nexus, receipt and Prescience. Native command checks are shared; old preparations are not retrofitted. Exceptional custody, worm protection and wider combinations 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'] },
{ area: 'Player controls', status: 'Partial', detail: 'Existing private lifecycle and Tleilaxu free-leader/paid-counter controls remain. Borrowed Voice adds neutral Continue and current-holder canonical require/forbid selection before Prescience, with a visible provisional cancellation policy. Original Karama and native preparation controls continue. Remaining reactive combinations and complete modes 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'] },
{ area: 'AI', status: 'Partial', detail: 'All four minimal legal profiles retain existing effect policies, actual Tleilaxu combined returns, and now answer both borrowed Voice opportunities through their own projected offer. Cardless Continue does not spend; eligible Use opens the original response. Complete strategy and 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'] },
{ area: 'Documentation', status: 'Partial', detail: 'Common rules and twelve faces retain source-bound remaining custody, inquiry, nested worm, Great Maker, fresh advisor/storm and combination questions. Borrowed Voice explicitly adopts recommended cancellable named-advantage semantics and a neutral pre-Prescience queue; this is not publisher clarification. Existing Richese and Guild payment/privacy contracts retain their own limits. Full mode and response-priority acceptance remain open.', evidence: ['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/NEXUS_GUILD_RULES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Borrowed Voice related4-file union50/50, types/lint and build pass. Genuine Basic/Advanced setup, paid Karama, worm/alliance/Nexus draw and reserve shipment produce the original battle; cases exercise order, spend, cancellation, compliance, impossible compliance, all-pass and four legal profiles. Actual390px Continue, projectile command, Karama, native Prescience decline and Seal battle plan preserve one spent Nexus and canceled receipt without overflow/errors after QA configuration repair. Earlier Tleilaxu and mixed-family evidence remains bounded; full modes, 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'] },
{ area: 'Implementation', status: 'Partial', detail: 'Existing physical effects, Tleilaxu shared revival and borrowed Voice remain connected. Absent-Fremen protection now binds one original pre-devour occurrence and public threatened held-Nexus storm order; only the actual unallied holder spends. It preserves that owner only and resumes native other casualties, spice removal, Great Maker vote and replacement once. No extra Karama provider, ride or native identity is invented under the visible provisional policy. Wider custody and combinations 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'] },
{ area: 'Player controls', status: 'Partial', detail: 'Existing lifecycle, Tleilaxu and borrowed Voice controls remain. Worm protection adds owner-only neutral Continue or actual-holder Use before devouring, with a visible independent-operation policy. The original Accept spice blow and Great Maker vote remain available after settlement. Nexus table guidance now lists connected effects and supply-only Moritani Cunning honestly. Remaining combinations and complete modes 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'] },
{ area: 'AI', status: 'Partial', detail: 'All four minimal legal profiles retain existing effects, Tleilaxu combined returns and borrowed Voice, and now answer only their own worm protection offer: actual holder Use or irrelevant-face Pass. Original later worm/Nexus and vote policies continue. This is legal participation, not strategy or difficulty calibration; both remain deferred and unfinished.', evidence: ['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Common rules and twelve faces retain remaining custody, inquiry, native nested Cunning, fresh advisor/storm and combination questions. Source documents explicitly distinguish cancellable named borrowed Voice from independent absent-Fremen protection with neutral pre-destruction timing. Neither is publisher clarification or universal reaction priority. 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: 'Protection related9-file union86/86 and actual390px GameTable pass. Genuine setup, worm/alliance/closing draw, paid safe Hagga shipment and free Advanced advisor produce the next real worm; Use/Pass, source rejection, four policies and original Great Maker vote/replacement are exercised. Neutral Continue leaves groups intact; Use preserves Emperor1/reserves19/Tanks0/spice10, devours BG advisor1, clears four spice, spends one Nexus and accepts the resumed Sihaya blow. No overflow/errors. Earlier Voice/Tleilaxu evidence remains bounded; wider modules, 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'] },
],
},
{
+1
View File
@@ -89,6 +89,7 @@ const DECISIONS = {
greatMakerRide: true,
wormRide: true,
nexusFremenCunningOffer: true,
nexusFremenProtection: true,
nexusFremenCunningRide: true,
moritaniRetention: true,
moritaniSpecialKarama: true,