Integrate CHOAM Nexus Cunning Kull with source-aware costs
This commit is contained in:
1 parent
fc5cbcd720
commit
0d9d0a72d6
32 files changed
+2502
-193
No files matched your search
@@ -7,11 +7,14 @@ export function botChoamKullActions(g: GameView): Action[] {
|
||||
!offer.canDecline) return [];
|
||||
const own = g.players.find((player) => player.id === g.me);
|
||||
if (!(own?.bot ?? own?.autopilot)) return [];
|
||||
const card = !offer.blocked && offer.target !== own.ally
|
||||
? offer.cards[0]
|
||||
const play = !offer.blocked && offer.target !== own.ally
|
||||
? offer.plays.find((candidate) => !candidate.blocked)
|
||||
: undefined;
|
||||
return card
|
||||
? [{ type: 'kullDecision', event: offer.event, card: card.id }]
|
||||
return play
|
||||
? [{
|
||||
type: 'kullDecision', event: offer.event,
|
||||
source: play.source, card: play.card.id,
|
||||
}]
|
||||
: [{ type: 'kullDecision', event: offer.event, decline: true }];
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,17 @@ export function kullNativeCostCards(
|
||||
!reservedIds.includes(card.id) && uniquelyHeld(card, held, physicalCards));
|
||||
}
|
||||
|
||||
/** Any canonical held Treachery card can pay Nexus Kull.
|
||||
* Nexus authority, timing and live promise eligibility belong to the caller. */
|
||||
export function kullNexusCostCards(
|
||||
held: readonly Card[],
|
||||
physicalCards: readonly Card[],
|
||||
reservedIds: readonly string[] = [],
|
||||
): Card[] {
|
||||
return held.filter(card => card && !reservedIds.includes(card.id) &&
|
||||
uniquelyHeld(card, held, physicalCards));
|
||||
}
|
||||
|
||||
/** The caller supplies effective eligibility (BG, Shrine, phase bans and live reservations).
|
||||
* This helper never reads a Game or infers activatability from a printed card alone. */
|
||||
export function kullCounterCards(
|
||||
|
||||
+200
-72
@@ -465,7 +465,7 @@ import {
|
||||
import { reserveExtortion, collectExtortion, answerExtortion, validateExtortionState, type ExtortionState } from './moritani-extortion';
|
||||
import { STORM_START_SECTOR, PLAYER_CIRCLE_SECTORS } from './player-positions';
|
||||
import { cashInCards } from './choam-karama';
|
||||
import { kullBlocksKarama, activeKullRestrictions, validateKullPhaseRestrictions, validateKullAttemptStamp, kullNativeCostCards, kullCounterCards, type KullPhaseRestriction } from './choam-kull';
|
||||
import { kullBlocksKarama, activeKullRestrictions, validateKullPhaseRestrictions, validateKullAttemptStamp, kullNativeCostCards, kullNexusCostCards, kullCounterCards, type KullPhaseRestriction } from './choam-kull';
|
||||
import { quoteRicheseBetrayalInvoice, requiredRicheseBetrayalResponders, richeseBetrayalEligibility, richeseBetrayalEvent, createRicheseBetrayalReceipt, validateRicheseBetrayalCursor, type RicheseBetrayalCursor, type RicheseBetrayalReceipt } from './nexus-richese-betrayal';
|
||||
import { saleOptions, quoteSale, type ChoamMarket } from './choam-market';
|
||||
import { highKaitainDiscardsAvailable, quoteKaitainDiscards, homeworldWorthlessSaleBlock } from './homeworld-card-economy';
|
||||
@@ -1092,6 +1092,7 @@ export type Game = {
|
||||
} | null;
|
||||
/** Explicit development profile; never enabled by a public start or player action. */
|
||||
kullPreview?: boolean;
|
||||
nexusKullPreview?: boolean;
|
||||
kullSequence?: number;
|
||||
kullRestrictions?: KullPhaseRestriction[];
|
||||
pendingKull?: {
|
||||
@@ -1103,6 +1104,7 @@ export type Game = {
|
||||
phase: number;
|
||||
form: 'printed' | 'substitution';
|
||||
stage: 'offer' | 'counter';
|
||||
selection?: { source: 'printed' | 'nexus'; card: string; nexusEvent?: string };
|
||||
intent: { kind: 'ordinary'; use: KaramaUse } | { kind: 'special'; use: SpecialKaramaIntent };
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
worthless: Game['pendingChoamWorthless'];
|
||||
@@ -2639,21 +2641,17 @@ function nexusFremenCunningIntegrity(g: Game) {
|
||||
'Fremen Cunning has lost its current rider choice.');
|
||||
}
|
||||
if (stage === 'pending') {
|
||||
const live = g.response;
|
||||
const prior = suspended?.response;
|
||||
const liveKarama = g.pendingKarama;
|
||||
const priorKarama = suspended?.pendingKarama;
|
||||
requireRule(
|
||||
(live?.kind === 'nexusFremenCunning' &&
|
||||
live.owner === authorization.owner && live.intent === occurrence.event) ||
|
||||
(prior?.kind === 'nexusFremenCunning' &&
|
||||
prior.owner === authorization.owner && prior.intent === occurrence.event) ||
|
||||
(liveKarama?.use.kind === 'cancel' &&
|
||||
liveKarama.use.response.kind === 'nexusFremenCunning' &&
|
||||
liveKarama.use.response.intent === occurrence.event) ||
|
||||
(priorKarama?.use.kind === 'cancel' &&
|
||||
priorKarama.use.response.kind === 'nexusFremenCunning' &&
|
||||
priorKarama.use.response.intent === occurrence.event),
|
||||
const controls = [g, suspended, g.pendingKull?.resume];
|
||||
requireRule(controls.some(control => {
|
||||
const response = control?.response;
|
||||
const karama = control?.pendingKarama;
|
||||
return (response?.kind === 'nexusFremenCunning' &&
|
||||
response.owner === authorization.owner && response.intent === occurrence.event) ||
|
||||
(karama?.use.kind === 'cancel' &&
|
||||
karama.use.response.kind === 'nexusFremenCunning' &&
|
||||
karama.use.response.owner === authorization.owner &&
|
||||
karama.use.response.intent === occurrence.event);
|
||||
}),
|
||||
'Fremen Cunning has lost its pending cancellation window.');
|
||||
}
|
||||
}
|
||||
@@ -3179,7 +3177,7 @@ function validateNexusAdvisorResponse(g: Game, response: ResponseWindow) {
|
||||
function nexusAdvisorIntegrity(g: Game) {
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,g.bureaucratPayments?.pending?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,g.bureaucratPayments?.pending?.resume, g.pendingKull?.resume,
|
||||
continuation && 'resume' in continuation ? continuation.resume : null];
|
||||
const responses = contexts.flatMap(context => {
|
||||
const pending = context && 'pendingKarama' in context ? context.pendingKarama as Game['pendingKarama'] : null;
|
||||
@@ -3296,7 +3294,7 @@ function validateNexusSardaukarResponse(g: Game, response: ResponseWindow) {
|
||||
function nexusSardaukarIntegrity(g: Game) {
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume, g.pendingKull?.resume,
|
||||
continuation && 'resume' in continuation ? continuation.resume : null];
|
||||
const responses = contexts.flatMap(context => {
|
||||
const pending = context && 'pendingKarama' in context ? context.pendingKarama as Game['pendingKarama'] : null;
|
||||
@@ -3464,7 +3462,7 @@ function nexusInspectionIntegrity(g: Game) {
|
||||
nexusRule(() => validateNexusInspection(battleInspectionContext(g), record));
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume, g.pendingKull?.resume,
|
||||
continuation && 'resume' in continuation ? continuation.resume : null];
|
||||
const responses = contexts.flatMap(context => {
|
||||
const pending = context && 'pendingKarama' in context ? context.pendingKarama as Game['pendingKarama'] : null;
|
||||
@@ -8189,7 +8187,7 @@ export function initializeSemutaGameForAudit(state: Game): Game {
|
||||
}
|
||||
/** Opt-in printed Kull gameplay with the physical Ix deck, not a public release gate. */
|
||||
export function initializeKullGameForAudit(state: Game): Game {
|
||||
requireRule(!state.kullPreview && !state.pendingKull && !state.kullRestrictions &&
|
||||
requireRule(!state.kullPreview && !state.nexusKullPreview && !state.pendingKull && !state.kullRestrictions &&
|
||||
state.expansions.length === 2 && state.expansions.includes('choam') &&
|
||||
state.expansions.includes('ix') && state.players.some(p => p.faction === 'choam') &&
|
||||
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base'),
|
||||
@@ -8198,6 +8196,28 @@ export function initializeKullGameForAudit(state: Game): Game {
|
||||
g.kullPreview = true;
|
||||
return g;
|
||||
}
|
||||
/** Fresh native CHOAM Kull with the physical Ix deck and only Nexus Cards. */
|
||||
export function initializeNexusKullGameForAudit(state: Game): Game {
|
||||
requireRule(!state.kullPreview && !state.nexusKullPreview && !state.pendingKull &&
|
||||
state.kullRestrictions === undefined && state.kullSequence === undefined &&
|
||||
!state.semutaPreview && !state.richeseBetrayalPreview && !state.richeseBetrayal &&
|
||||
!state.pendingRicheseBetrayal && !state.homeworlds && !state.leaderSkills &&
|
||||
!state.discoveryEnabled && !state.discoveries && !state.discoveryStash && !state.greatMaker &&
|
||||
!state.techTokens && !state.strongholdCards && !state.ecazTreachery &&
|
||||
state.nexusCards?.cards === null && state.nexusCards.phase === null &&
|
||||
typeof state.advanced === 'boolean' && state.expansions.length === 2 &&
|
||||
state.expansions.includes('choam') && state.expansions.includes('ix') &&
|
||||
state.players.some(p => p.faction === 'choam') &&
|
||||
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base'),
|
||||
'Nexus Kull requires a fresh native CHOAM/classic lobby, CHOAM and Ix decks, and only Nexus Cards.');
|
||||
requireFreshBaseRuntime(state);
|
||||
const g = initializeSetupGameForAudit(state, false, true, false, false, false, false, true);
|
||||
g.kullPreview = true;
|
||||
g.nexusKullPreview = true;
|
||||
// Ix supplies Treachery cards here, not the optional Sandtrout Spice card.
|
||||
g.spiceDeck = shuffle(spiceDeck());
|
||||
return g;
|
||||
}
|
||||
/** Prototype-only Discovery setup. Public starts stay gated while remaining effects are connected. */
|
||||
export function initializeDiscoveryGameForAudit(state: Game): Game {
|
||||
requireRule(state.discoveryEnabled === true && !state.discoveries && !state.discoveryStash && !state.greatMaker,
|
||||
@@ -10573,7 +10593,8 @@ function karamaSpendingBlock(g: Game, p: Player, card: Card): string | null {
|
||||
if (g.kullPreview && kullBlocksKarama(g.kullRestrictions, g.turn, g.phase, p.id))
|
||||
return 'Kull Wahad prevents your Karama activations for this phase.';
|
||||
if (g.pendingKull?.stage === 'counter' &&
|
||||
(g.pendingKull.card === card.id || g.pendingChoamWorthless?.card === card.id))
|
||||
(g.pendingKull.card === card.id || g.pendingChoamWorthless?.card === card.id ||
|
||||
g.pendingKull.worthless?.card === card.id))
|
||||
return 'This physical card is reserved for the interrupted Kull transaction.';
|
||||
if (g.pendingIxRicheseTechnology?.card.id === card.id)
|
||||
return 'This card is reserved for the pending Richese lot and Ixian choice.';
|
||||
@@ -11369,31 +11390,76 @@ function assertKaramaPromiseFeasibility(
|
||||
return true;
|
||||
}
|
||||
type KullFrame = NonNullable<Game['pendingKull']>;
|
||||
function kullCostPromisesAllow(g: Game, p: Player, card: Card): boolean {
|
||||
const battlePromises = g.battle && !g.battle.revealed && !g.battle.plans[p.id]
|
||||
? (g.battle.truthPromises ?? []).filter(promise => promise.player === p.id && !promise.released)
|
||||
: [];
|
||||
const shipmentPromises = liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn);
|
||||
if (!battlePromises.length && !shipmentPromises.length) return true;
|
||||
const projected = structuredClone(g.pendingKull ? kullSource(g, g.pendingKull) : g);
|
||||
const owner = getPlayer(projected, p.id);
|
||||
owner.hand = owner.hand.filter(held => held.id !== card.id);
|
||||
if (g.pendingKull)
|
||||
projected.kullRestrictions = [
|
||||
...activeKullRestrictions(projected.kullRestrictions, projected.turn, projected.phase),
|
||||
{ player: g.pendingKull.owner, turn: projected.turn, phase: projected.phase },
|
||||
];
|
||||
projected.response = null;
|
||||
projected.decision = null;
|
||||
projected.pendingKarama = null;
|
||||
return (!battlePromises.length ||
|
||||
!!findReachableBattlePlan(projected, owner, { promises: battlePromises })) &&
|
||||
(!shipmentPromises.length || !!findShipmentCompletion(projected, owner, shipmentPromises));
|
||||
// Independent printed descriptors, assembled once rather than trusting a saved
|
||||
// hand to define its own identity or rebuilding the physical deck per quote.
|
||||
const kullCanonicalCards: Readonly<Record<string, Card>> =
|
||||
Object.fromEntries(treacheryDeck(['choam', 'ix']).map(card => [card.id, card]));
|
||||
function canonicalKullCard(card: Card): boolean {
|
||||
const expected = Object.hasOwn(kullCanonicalCards, card.id) ? kullCanonicalCards[card.id] : undefined;
|
||||
return !!expected && card.name === expected.name && card.kind === expected.kind &&
|
||||
card.effect === expected.effect;
|
||||
}
|
||||
function kullCostCards(g: Game, p: Player): Card[] {
|
||||
return kullNativeCostCards(cashInCards(g, p), physicalTreacheryCards(g))
|
||||
.filter(card => !choamWorthlessBlocked(g, card.id) && !giftReserved(g, p.id, card.id) &&
|
||||
!transferCardBlock(g, p, card) && kullCostPromisesAllow(g, p, card));
|
||||
function kullCostRetainsAuctionPayment(g: Game, p: Player, card: Card, physical: readonly Card[]): boolean {
|
||||
return !g.auction || g.auction.bidder !== p.id || auctionSpicePaymentFunded(g, p) ||
|
||||
p.hand.some(held => held.id !== card.id && canonicalKullCard(held) &&
|
||||
physical.reduce((count, candidate) => count + Number(candidate.id === held.id), 0) === 1 &&
|
||||
!karamaSpendingBlock(g, p, held));
|
||||
}
|
||||
function kullCostPromisesAllow(g: Game, p: Player, card: Card, fuelSource: 'printed' | 'nexus'): boolean {
|
||||
const source = g.pendingKull ? kullSource(g, g.pendingKull) : g;
|
||||
const battlePromises = source.battle && !source.battle.revealed && !source.battle.plans[p.id]
|
||||
? (source.battle.truthPromises ?? []).filter(promise => promise.player === p.id && !promise.released)
|
||||
: [];
|
||||
const shipmentPromises = liveShipmentPromises(source.shipmentPromises ?? [], p.id, source.turn);
|
||||
if (!battlePromises.length && !shipmentPromises.length) return true;
|
||||
for (const success of [true, false]) {
|
||||
const projected = structuredClone(source);
|
||||
const owner = getPlayer(projected, p.id);
|
||||
// Acceptance spends Cunning in either outcome. Do not let feasibility
|
||||
// reuse its physical Nexus as a future preparation or shipment resource.
|
||||
if (fuelSource === 'nexus' && projected.nexusCards?.cards?.hands[p.id] === 'choam')
|
||||
projected.nexusCards.cards = nexusRule(() => discardNexusCard(projected.nexusCards!.cards!, p.id, projected.players));
|
||||
if (success) {
|
||||
owner.hand = owner.hand.filter(held => held.id !== card.id);
|
||||
if (g.pendingKull)
|
||||
projected.kullRestrictions = [
|
||||
...activeKullRestrictions(projected.kullRestrictions, projected.turn, projected.phase),
|
||||
{ player: g.pendingKull.owner, turn: projected.turn, phase: projected.phase },
|
||||
];
|
||||
}
|
||||
projected.response = null;
|
||||
projected.decision = null;
|
||||
projected.pendingKarama = null;
|
||||
if ((battlePromises.length && !findReachableBattlePlan(projected, owner, { promises: battlePromises })) ||
|
||||
(shipmentPromises.length && !findShipmentCompletion(projected, owner, shipmentPromises)))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function kullCostCards(g: Game, p: Player, source: 'printed' | 'nexus' = 'printed'): Card[] {
|
||||
const original = g.pendingKull ? kullSource(g, g.pendingKull) : g;
|
||||
if (source === 'nexus' && (!g.nexusKullPreview || p.faction !== 'choam' || p.ally ||
|
||||
(g.pendingKull?.selection?.source !== 'nexus' && g.nexusCards?.cards?.hands[p.id] !== 'choam')))
|
||||
return [];
|
||||
const reserved = [
|
||||
g.pendingKull?.card, g.pendingKull?.worthless?.card,
|
||||
].filter((id): id is string => typeof id === 'string');
|
||||
const physical = physicalTreacheryCards(g);
|
||||
const candidates = source === 'nexus'
|
||||
? kullNexusCostCards(p.hand, physical, reserved)
|
||||
: kullNativeCostCards(cashInCards(g, p), physical).filter(card => !reserved.includes(card.id));
|
||||
return candidates.filter(card => canonicalKullCard(card) &&
|
||||
kullCostRetainsAuctionPayment(original, p, card, physical) && !choamWorthlessBlocked(original, card.id) &&
|
||||
!giftReserved(original, p.id, card.id) && !transferCardBlock(original, p, card) &&
|
||||
!retentionReservesCard(original, p.id, card.id) && original.battle?.lateDefense?.[p.id] !== card.id &&
|
||||
![original.battle?.plans[p.id]?.weapon, original.battle?.plans[p.id]?.defense,
|
||||
original.battle?.plans[p.id]?.leader].includes(card.id) &&
|
||||
!(original.battle && committedPlanElements(original.battle, p.id).some(element => element.value === card.id)) &&
|
||||
kullCostPromisesAllow(g, p, card, source));
|
||||
}
|
||||
function kullPlays(g: Game, p: Player) {
|
||||
return (['printed', 'nexus'] as const).flatMap(source => kullCostCards(g, p, source).map(card =>
|
||||
({ source, effect: 'kull' as const, card, event: g.pendingKull!.event, blocked: null as string | null })));
|
||||
}
|
||||
function kullSource(g: Game, frame: KullFrame): Game {
|
||||
return { ...g, ...frame.resume, pendingKull: null, pendingChoamWorthless: frame.worthless };
|
||||
@@ -11403,6 +11469,7 @@ function kullSignature(g: Game, frame: KullFrame): string {
|
||||
return JSON.stringify({
|
||||
event: frame.event, player: frame.player, owner: frame.owner, card: frame.card,
|
||||
turn: frame.turn, phase: frame.phase, form: frame.form, intent: frame.intent,
|
||||
selection: frame.selection,
|
||||
controls: nullentropyParentSignature(source, frame.resume, frame.event),
|
||||
status: source.status, advanced: source.advanced, active: source.active,
|
||||
order: source.order, ready: source.ready, hajr: source.hajr, choamMarket: source.choamMarket,
|
||||
@@ -11412,8 +11479,35 @@ function kullSignature(g: Game, frame: KullFrame): string {
|
||||
stormResolution: source.stormResolution, worthless: frame.worthless,
|
||||
});
|
||||
}
|
||||
/** Only the verified pre-source printed profile may bind its old held cost. */
|
||||
function migratePrintedKullCounter(state: Game): Game {
|
||||
const frame = state.pendingKull;
|
||||
if (!frame || frame.stage !== 'counter' || frame.selection !== undefined ||
|
||||
state.nexusKullPreview || state.nexusCards) return state;
|
||||
const declaration = state.pendingChoamWorthless;
|
||||
requireRule(state.kullPreview === true && declaration?.effect === 'kull' &&
|
||||
!declaration.nexusEvent && declaration.owner === frame.player &&
|
||||
frame.signature === kullSignature(state, frame),
|
||||
'The legacy printed Kull counter has lost its original declaration or signature.');
|
||||
const migrated = structuredClone(state);
|
||||
migrated.pendingKull!.selection = { source: 'printed', card: declaration.card };
|
||||
migrated.pendingKull!.signature = kullSignature(migrated, migrated.pendingKull!);
|
||||
// All original stamp, intent, custody, response and quote checks still apply.
|
||||
kullIntegrity(migrated);
|
||||
return migrated;
|
||||
}
|
||||
function kullIntegrity(g: Game): void {
|
||||
try {
|
||||
requireRule(g.nexusKullPreview === undefined || g.nexusKullPreview === true &&
|
||||
g.kullPreview === true && !!g.nexusCards?.cards &&
|
||||
g.expansions.length === 2 && g.expansions.includes('choam') && g.expansions.includes('ix') &&
|
||||
g.players.some(p => p.faction === 'choam') &&
|
||||
g.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base') &&
|
||||
!g.homeworlds && !g.leaderSkills && !g.discoveryEnabled && !g.discoveries &&
|
||||
!g.discoveryStash && !g.greatMaker && !g.sandtrout &&
|
||||
!g.techTokens && !g.strongholdCards && !g.ecazTreachery && !g.semutaPreview &&
|
||||
!g.richeseBetrayalPreview && !g.richeseBetrayal && !g.pendingRicheseBetrayal,
|
||||
'The Nexus Kull profile has lost its supported native inventory and modules.');
|
||||
if (g.kullRestrictions !== undefined) validateKullPhaseRestrictions(g, g.kullRestrictions);
|
||||
const frame = g.pendingKull;
|
||||
if (!frame) {
|
||||
@@ -11434,6 +11528,12 @@ function kullIntegrity(g: Game): void {
|
||||
requireRule(frame.signature === kullSignature(g, frame),
|
||||
'The saved Kull attempt no longer matches its original controls and opportunity.');
|
||||
const source = kullSource(g, frame), owner = getPlayer(source, frame.owner);
|
||||
if (frame.worthless) {
|
||||
const parentOwner = getPlayer(source, frame.worthless.owner);
|
||||
requireRule(kullNexusCostCards(parentOwner.hand, physicalTreacheryCards(g))
|
||||
.some(card => card.id === frame.worthless!.card && canonicalKullCard(card)),
|
||||
'The interrupted CHOAM declaration must retain its exact unique physical cost.');
|
||||
}
|
||||
const card = owner.hand.find(card => card.id === frame.card);
|
||||
requireRule(card, 'The original attempted Karama must remain in its owner’s hand.');
|
||||
requireRule(frame.form === (card.kind === 'worthless' ? 'substitution' : 'printed'),
|
||||
@@ -11449,21 +11549,25 @@ function kullIntegrity(g: Game): void {
|
||||
requireRule(JSON.stringify(prepared) === JSON.stringify(frame.intent.use),
|
||||
'The saved special Karama selection has changed.');
|
||||
}
|
||||
if (frame.stage === 'offer')
|
||||
if (frame.stage === 'offer') {
|
||||
requireRule(frame.selection === undefined, 'An undeclared Kull offer cannot reserve a selected source.');
|
||||
requireRule(JSON.stringify(g.response) === JSON.stringify(frame.resume.response) &&
|
||||
JSON.stringify(g.decision) === JSON.stringify(frame.resume.decision) &&
|
||||
JSON.stringify(g.pendingKarama ?? null) === JSON.stringify(frame.resume.pendingKarama ?? null) &&
|
||||
JSON.stringify(g.pendingChoamWorthless ?? null) === JSON.stringify(frame.worthless ?? null),
|
||||
'The Kull offer no longer owns its original controls.');
|
||||
else {
|
||||
} else {
|
||||
const declaration = g.pendingChoamWorthless;
|
||||
const response = g.pendingKarama?.use.kind === 'cancel'
|
||||
? g.pendingKarama.use.response : g.response;
|
||||
requireRule(declaration?.effect === 'kull' && declaration.owner === frame.player &&
|
||||
requireRule(frame.selection && ['printed','nexus'].includes(frame.selection.source) &&
|
||||
declaration?.effect === 'kull' && declaration.owner === frame.player &&
|
||||
declaration.card === frame.selection.card && declaration.nexusEvent === frame.selection.nexusEvent &&
|
||||
(frame.selection.source === 'nexus' ? !!declaration.nexusEvent : !declaration.nexusEvent) &&
|
||||
declaration.target === frame.owner && response?.kind === 'choamWorthless' &&
|
||||
response.owner === frame.player && response.intent === 'Kull Wahad' &&
|
||||
response.recipient === frame.owner &&
|
||||
kullCostCards(g, getPlayer(g, frame.player)).some(card => card.id === declaration.card),
|
||||
kullCostCards(g, getPlayer(g, frame.player), frame.selection.source).some(card => card.id === declaration.card),
|
||||
'The Kull counter no longer matches its held cost and original attempted player.');
|
||||
currentChoamWorthlessCancellationQuote(g, response);
|
||||
}
|
||||
@@ -11522,25 +11626,33 @@ function resumeKullAttempt(g: Game, prevented: boolean): void {
|
||||
function decideKull(g: Game, p: Player, action: Action): void {
|
||||
const frame = g.pendingKull!;
|
||||
requireRule(action.type === 'kullDecision' && action.event === frame.event &&
|
||||
p.id === frame.player && Object.keys(action).every(key => ['type','event','decline','card'].includes(key)),
|
||||
p.id === frame.player && Object.keys(action).every(key => ['type','event','decline','source','card'].includes(key)),
|
||||
'Choose the current Kull opportunity as CHOAM.');
|
||||
if (action.decline === true) {
|
||||
requireRule(action.card === undefined, 'Decline or choose one Kull card, not both.');
|
||||
requireRule(action.card === undefined && action.source === undefined, 'Decline or choose one Kull source and card, not both.');
|
||||
resumeKullAttempt(g, false);
|
||||
return;
|
||||
}
|
||||
requireRule(action.decline === undefined && typeof action.card === 'string' &&
|
||||
kullCostCards(g, p).some(card => card.id === action.card),
|
||||
'Choose your exact eligible held Kull Wahad card.');
|
||||
requireRule(action.decline === undefined && (action.source === 'printed' || action.source === 'nexus') &&
|
||||
typeof action.card === 'string' && kullCostCards(g, p, action.source).some(card => card.id === action.card),
|
||||
'Choose your exact eligible held Kull source and card.');
|
||||
const source = action.source;
|
||||
const card = p.hand.find(card => card.id === action.card)!;
|
||||
const nexusEvent = source === 'nexus'
|
||||
? JSON.stringify(['nexusChoam', g.turn, g.phase, p.id, card.id, 'kull']) : undefined;
|
||||
frame.selection = { source, card: card.id, ...(nexusEvent ? { nexusEvent } : {}) };
|
||||
frame.signature = kullSignature(g, frame);
|
||||
frame.stage = 'counter';
|
||||
g.decision = null;
|
||||
g.pendingKarama = null;
|
||||
g.phaseOpening = null;
|
||||
g.pendingChoamWorthless = {
|
||||
owner: p.id, card: action.card, effect: 'kull', target: frame.owner, revival: false,
|
||||
...(nexusEvent ? { nexusEvent } : {}),
|
||||
};
|
||||
g.response = { kind: 'choamWorthless', owner: p.id, recipient: frame.owner,
|
||||
intent: 'Kull Wahad', passed: [] };
|
||||
if (source === 'nexus') recordNexusChoam(g, p, card, 'kull');
|
||||
log(g, `${p.name} declared Kull Wahad. Only a different Karama can prevent it.`);
|
||||
}
|
||||
function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered = false) {
|
||||
@@ -23228,7 +23340,7 @@ function finishGuildCunningShipment(g: Game, owner: string, kind: 'reserve'|'gui
|
||||
}
|
||||
function nexusGuildCunningIntegrity(g: Game) {
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g,g.pendingExchange,g.pendingNullentropy?.resume,g.pendingRicheseGift?.resume,g.pendingRichesePurchaseIncome?.resume,g.summonedWorm?.resume,
|
||||
const contexts = [g,g.pendingExchange,g.pendingNullentropy?.resume,g.pendingRicheseGift?.resume,g.pendingRichesePurchaseIncome?.resume,g.summonedWorm?.resume,g.pendingKull?.resume,
|
||||
continuation && 'resume' in continuation ? continuation.resume : null];
|
||||
const responses = contexts.flatMap(c => {
|
||||
if (!c) return [];
|
||||
@@ -23690,21 +23802,30 @@ function nexusChoamParent(g: Game, pending: NonNullable<Game['pendingChoamWorthl
|
||||
revival: pending.revival ? g.pendingRevival : undefined,
|
||||
storm: pending.storm ? g.stormResolution : undefined,
|
||||
mentat: pending.mentat ? g.choamMentatPending : undefined,
|
||||
kull: pending.effect === 'kull' ? {
|
||||
event: g.pendingKull?.event, signature: g.pendingKull?.signature,
|
||||
selection: g.pendingKull?.selection,
|
||||
} : undefined,
|
||||
});
|
||||
}
|
||||
function nexusChoamIntegrity(g: Game) {
|
||||
const pending = g.pendingChoamWorthless;
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
|
||||
const controls = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
|
||||
g.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,
|
||||
continuation && 'resume' in continuation ? continuation.resume : null];
|
||||
const responses = contexts.flatMap(context => {
|
||||
const responseList = (contexts: typeof controls) => contexts.flatMap(context => {
|
||||
const karama = context && 'pendingKarama' in context ? context.pendingKarama as Game['pendingKarama'] : null;
|
||||
return [context?.response, karama?.use.kind === 'cancel' ? karama.use.response : null];
|
||||
}).filter(response => response?.kind === 'choamWorthless');
|
||||
}).filter((response): response is ResponseWindow => response?.kind === 'choamWorthless');
|
||||
const transactions = [
|
||||
{ state: g, pending: g.pendingChoamWorthless, responses: responseList(controls) },
|
||||
...(g.pendingKull ? [{ state: kullSource(g, g.pendingKull), pending: g.pendingKull.worthless,
|
||||
responses: responseList([g.pendingKull.resume]) }] : []),
|
||||
];
|
||||
const declared = transactions.filter(transaction => transaction.pending?.nexusEvent);
|
||||
const history = g.nexusChoamHistory;
|
||||
if (history === undefined) {
|
||||
requireRule(!g.nexusChoamLast && !pending?.nexusEvent, 'CHOAM Cunning has lost its original saved history.');
|
||||
requireRule(!g.nexusChoamLast && !declared.length, 'CHOAM Cunning has lost its original saved history.');
|
||||
return;
|
||||
}
|
||||
requireRule(g.nexusCards?.cards && Array.isArray(history) && history.length > 0,
|
||||
@@ -23723,19 +23844,22 @@ function nexusChoamIntegrity(g: Game) {
|
||||
const last = history.at(-1)!;
|
||||
requireRule(JSON.stringify(g.nexusChoamLast) === JSON.stringify({event:last.receipt.event,stage:last.stage}),
|
||||
'CHOAM Cunning has lost its latest completed or pending outcome.');
|
||||
requireRule((last.stage === 'pending') === !!pending?.nexusEvent,
|
||||
requireRule((last.stage === 'pending') === (declared.length > 0) &&
|
||||
declared.every(transaction => transaction.pending?.nexusEvent === last.receipt.event),
|
||||
'CHOAM Cunning has lost or reopened its declared effect.');
|
||||
if (last.stage === 'pending') {
|
||||
requireRule(pending && pending.nexusEvent === last.receipt.event &&
|
||||
pending.owner === last.receipt.owner && pending.card === last.receipt.card && pending.effect === last.receipt.effect &&
|
||||
g.turn === last.receipt.turn && g.phase === last.receipt.phase &&
|
||||
JSON.stringify(pending) === last.frame && nexusChoamParent(g,pending) === last.parent && responses.length > 0 && g.nexusCards.cards.discard.includes('choam'),
|
||||
for (const { state, pending, responses } of declared) {
|
||||
requireRule(pending && pending.owner === last.receipt.owner && pending.card === last.receipt.card &&
|
||||
pending.effect === last.receipt.effect && g.turn === last.receipt.turn && g.phase === last.receipt.phase &&
|
||||
JSON.stringify(pending) === last.frame && nexusChoamParent(state,pending) === last.parent &&
|
||||
responses.length > 0 && g.nexusCards.cards.discard.includes('choam'),
|
||||
'CHOAM Cunning no longer matches its original card, selected effect or response.');
|
||||
for (const response of responses) {
|
||||
try { quoteChoamWorthlessCancellation(g,response!); }
|
||||
catch(error) {
|
||||
if (error instanceof ChoamWorthlessCancellationError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
for (const response of responses) {
|
||||
try { quoteChoamWorthlessCancellation(state,response); }
|
||||
catch(error) {
|
||||
if (error instanceof ChoamWorthlessCancellationError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24693,6 +24817,7 @@ function normalizeCardNames(g: Game) {
|
||||
]);
|
||||
}
|
||||
export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
@@ -24780,7 +24905,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
if (g.pendingRicheseBetrayal) {richeseBetrayalIntegrity(g);return g;}
|
||||
if (g.pendingKull) {
|
||||
settleAutomaticContinuations(g);
|
||||
if (g.pendingKull) { kullIntegrity(g); return g; }
|
||||
if (g.pendingKull) { kullIntegrity(g); marketGholaIntegrity(g); return g; }
|
||||
}
|
||||
finishLeaderSkillCustody(g, state);
|
||||
observeOccupation(g);
|
||||
@@ -25014,6 +25139,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
}
|
||||
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
|
||||
export function normalizeAutomaticGame(state: Game): Game {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
@@ -28927,6 +29053,7 @@ function applyActionInner(
|
||||
throw new RuleError('That action is not available.');
|
||||
}
|
||||
export function viewGame(state: Game, id: string) {
|
||||
state = migratePrintedKullCounter(state);
|
||||
richeseBetrayalIntegrity(state);
|
||||
kullIntegrity(state);
|
||||
advancedPreviewIntegrity(state);
|
||||
@@ -29441,13 +29568,14 @@ export function viewGame(state: Game, id: string) {
|
||||
})()
|
||||
: null,
|
||||
kullPreview: !!g.kullPreview,
|
||||
nexusKullPreview: !!g.nexusKullPreview,
|
||||
kullCounterEvent: g.pendingKull?.stage === 'counter' ? g.pendingKull.event : null,
|
||||
karamaBlocked: g.kullPreview && kullBlocksKarama(g.kullRestrictions, g.turn, g.phase, id)
|
||||
? 'Kull Wahad prevents your Karama activations for this phase.' : null,
|
||||
kullReaction: g.pendingKull?.stage === 'offer'
|
||||
? { event: g.pendingKull.event, player: g.pendingKull.player, target: g.pendingKull.owner,
|
||||
intent: g.pendingKull.intent.kind === 'ordinary' ? g.pendingKull.intent.use.kind : 'special',
|
||||
cards: id === g.pendingKull.player ? kullCostCards(g, me) : [],
|
||||
plays: id === g.pendingKull.player ? kullPlays(g, me) : [],
|
||||
canDecline: id === g.pendingKull.player, blocked: null as string | null }
|
||||
: null,
|
||||
choamWorthless:
|
||||
|
||||
+1
-2
@@ -65,8 +65,7 @@ function phaseMatches(effect: NexusChoamEffect, phase: number): boolean {
|
||||
return phase === 8;
|
||||
case 'jubba':
|
||||
return phase === 0;
|
||||
// This describes printed timing only; the engine must separately authorize
|
||||
// a real Karama attempt and retains Kull's unresolved implementation gate.
|
||||
// Any phase can contain a Karama attempt; the caller authorizes that timing.
|
||||
case 'kull':
|
||||
return true;
|
||||
default:
|
||||
|
||||
+39
-9
@@ -2428,7 +2428,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Atreides full-plan inspection, Emperor revival, Fremen’s summoned worm, Harkonnen hand exchange the Guild shipment stop and Tleilaxu revival prevention are implemented as once-per-game uses. Other cancellations and special Karamas are still being implemented.',
|
||||
'In the opt-in Kull development preview, another player’s legal ordinary Karama or supported special activation pauses before its costs for CHOAM’s neutral reaction. A declared Kull is pending, not yet a phase ban. Only a different eligible physical Karama can counter it; the original stays reserved. Successful Kull retains the unplayed original, including an advanced Bene Gesserit Worthless card intercepted before conversion, and prevents further Karama activations for that phase. See Kull Wahad for the explicit preview policy and deferred winning-overbid warning.',
|
||||
],
|
||||
related: ['bidding', 'battle', 'advanced-combat', 'special-karama', 'choam-kull'],
|
||||
related: ['bidding', 'battle', 'advanced-combat', 'special-karama', 'choam-kull', 'nexus-choam-kull'],
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-forces',
|
||||
@@ -2747,7 +2747,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
`advanced-${f.id}`,
|
||||
'faction-sheets',
|
||||
...(f.id === 'richese' ? ['richese-cards', 'richese-no-field', 'richese-gift', 'richese-acquisition'] : []),
|
||||
...(f.id === 'choam' ? ['choam-auditor', 'choam-worthless', 'choam-kull', 'choam-combat'] : []),
|
||||
...(f.id === 'choam' ? ['choam-auditor', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'choam-combat'] : []),
|
||||
...(f.id === 'ecaz'
|
||||
? ['ecaz-loyalty', 'ecaz-ambassadors', 'duke-vidal', 'implementation-checklist']
|
||||
: []),
|
||||
@@ -3321,13 +3321,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Whenever another faction requests a normal revival with a free portion, CHOAM receives a response decision independently of its private hand. Allow the request, or declare La La La for that requester. If the effect succeeds, the whole pending request stops without payment, force movement or quota use; a faction permitted to purchase normal revivals can submit a paid request. If the effect is canceled, the original quote resumes through any Tleilaxu and other applicable responses.',
|
||||
'La La La does not prevent otherwise legal paid revivals or separate Ghola card effects. With Tleilaxu in the game, Fremen may submit a paid request within their current limit after free revival is prevented. Without Tleilaxu, Fremen’s base purchase restriction leaves no normal force return available. Emperor-funded extras remain separate. The prohibition resets when the next Revival phase begins. Card custody is checked at settlement: if CHOAM cashed in the declared card during the response, no Worthless effect occurs and the interrupted revival resumes.',
|
||||
'During Shipment and Movement, use Baliset to select another player and a territory CHOAM occupies. The current implementation prevents that player from entering that territory by movement for the phase while CHOAM remains there; shipment is allowed. A declared move into a CHOAM territory pauses for CHOAM regardless of its private hand. Allow it or declare Baliset. A successful effect leaves the moving forces in place and the movement unspent; a canceled or unavailable card resumes the original move after checking its legality again.',
|
||||
'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has a separate opt-in development preview; see Kull Wahad for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. This is not six-effect Nexus Cunning: its Kull choice remains unavailable. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Normal expansion starts remain disabled.',
|
||||
'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has a separate opt-in development preview; the explicit Nexus Kull profile adds Cunning fuel choices at that same reaction, not in every Nexus game. See both Kull guides for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Normal expansion starts remain disabled.',
|
||||
],
|
||||
related: [
|
||||
'faction-choam',
|
||||
'choam-gamont',
|
||||
'choam-jubba',
|
||||
'choam-kull',
|
||||
'nexus-choam-kull',
|
||||
'choam-market',
|
||||
'choam-revival',
|
||||
'movement',
|
||||
@@ -3344,7 +3345,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary: 'React before a Karama is played; a distinct counter can stop Kull before its phase restriction begins.',
|
||||
steps: [
|
||||
'This opt-in development preview supports CHOAM with classic factions in Basic or Advanced, using the physical CHOAM and Ix Treachery decks without optional modules. Basic supports printed ordinary Karama; Advanced also supports Bene Gesserit substitutions and the legal special powers of this roster. Existing games are not retrofitted. Normal expansion starts remain closed, and the visible preview warning is not rules-completion approval.',
|
||||
'Another player’s legal Karama cancellation, shipment-rate use, immediate auction purchase, winning-bid payment or supported special activation pauses before its card and other costs. CHOAM sees the attempted player and purpose and may decline or choose a legal held Kull Wahad. The same public opportunity appears without Kull in hand, so waiting does not disclose hidden ownership. Invalid attempts never open this window; CHOAM does not receive a self-targeted Kull offer.',
|
||||
'Another player’s legal Karama cancellation, shipment-rate use, immediate auction purchase, winning-bid payment or supported special activation pauses before its card and other costs. CHOAM sees the attempted player and purpose and may decline or choose a legal held Kull Wahad. Each Use names its printed source explicitly. The separate Nexus Kull guide covers additional Cunning costs in its new profile only. The same public opportunity appears without Kull in hand, so waiting does not disclose hidden ownership. Invalid attempts never open this window; CHOAM does not receive a self-targeted Kull offer.',
|
||||
'Decline resumes the original attempt once. Declaring Kull opens the ordinary CHOAM-effect response. Kull is still pending: its target may counter with a different eligible physical Karama before any phase ban applies. The interrupted card stays reserved and cannot serve as its own counter. Other eligible responders use their existing response controls.',
|
||||
'If everyone allows Kull, CHOAM discards the exact Kull card. The original attempted card remains in its owner’s hand; its effect and additional costs do not occur, and an attempted special once-per-game use remains unspent. The activating player cannot activate printed or substituted Karama again during this turn’s current phase. An allied shipment recipient is not automatically the restricted player.',
|
||||
'The successful restriction ends when the phase or turn changes, not during a temporary response, market or worm substep. It does not prohibit holding, acquiring, transferring or otherwise legally discarding a Karama without activating it.',
|
||||
@@ -3352,10 +3353,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Advanced Bene Gesserit is intercepted before Worthless conversion or discard: successful Kull keeps that original unplayed Worthless card held. If CHOAM declines or Kull is countered, the original proceeds to its separate conversion response. Ordinary cancellation of that conversion still leaves its Worthless card discarded. A different BG Worthless counter has its own conversion response; it is never the reserved original.',
|
||||
'Winning overbids whose Karama payment would become unplayable remain deferred. To avoid accepting a bid that the preview cannot settle, a non-CHOAM unfunded Karama-dependent bid is refused privately before commitment, independently of CHOAM’s hidden Kull ownership. CHOAM self-activations are outside interception; normal non-preview bidding is unchanged. An already unfunded winning activation remains guarded. Holding Karama alone is not a reaction trigger. This unfinished-composition fence does not restart bidding, award the lot free, supply new funding or change numeric UI bid limits.',
|
||||
'A live Truthtrance battle or shipment promise also constrains original attempts, Kull costs and counters. A cost that cannot be proved compatible is unavailable before disposal; CHOAM may decline instead. Proven supported cancellations remain available. Pending Kull cannot release a suspended promise, and its counter choices exclude unproven costs so legal AI can progress. Unsupported continuations remain an incomplete preview composition, not a publisher ban or permission to release the promise.',
|
||||
'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. Nexus Cunning Kull, other expansion rosters, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside this preview. Nexus cards do not receive blanket immunity to Karama.',
|
||||
'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. This printed-only profile still excludes Nexus. The separate Nexus Kull profile adds one bounded Cunning interaction without changing older games; other expansion rosters, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside both previews. Nexus cards do not receive blanket immunity to Karama.',
|
||||
],
|
||||
example: 'Bene Gesserit attempts to use Baliset as Karama. CHOAM declares Kull. Baliset cannot also counter Kull; a different eligible Karama may. If Kull succeeds, Baliset stays held and BG cannot activate any Worthless-as-Karama again this phase. If a distinct counter prevents Kull, the original Baliset attempt proceeds to its normal conversion response.',
|
||||
related: ['choam-worthless', 'faction-choam', 'card-karama', 'advanced-beneGesserit', 'special-karama', 'bidding', 'choam-karama', 'nexus-cards', 'implementation-checklist'],
|
||||
related: ['choam-worthless', 'nexus-choam-kull', 'faction-choam', 'card-karama', 'advanced-beneGesserit', 'special-karama', 'bidding', 'choam-karama', 'nexus-cards', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Opt-in printed Kull interception binds typed ordinary or prepared special intents, distinct physical counters and a stamped activation restriction. Deferred overbid recovery and combined-module support remain excluded.', evidence: ['game/choam-kull.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Owner-only legal held costs and decline use the Kull event; counters reuse ordinary response controls. Public viewers receive purpose/actors, not private intent payloads or hidden choices.', evidence: ['components/choam-kull.tsx', 'components/game-table.tsx'] },
|
||||
@@ -3982,6 +3983,35 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'nexus-choam-kull',
|
||||
title: 'CHOAM Nexus Cunning: choose Kull Wahad',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Verified',
|
||||
summary: 'In the explicit Nexus Kull preview, choose one actual Treachery fuel card at a Karama attempt; the CHOAM Nexus is spent when the declaration is accepted, even if Kull is prevented.',
|
||||
steps: [
|
||||
'This is the fresh local nexus-kull development profile, not a public expansion start or an upgrade to existing saves. It admits two to six ready seats with native CHOAM and otherwise classic factions in Basic or Advanced, physical CHOAM and Ix Treachery decks, and Nexus alone. Other optional modules, expansion factions, Semuta and Richese Betrayal stay outside its boundary. The older kull profile remains printed-only and has no Nexus cards.',
|
||||
'The CHOAM Nexus face says: Discard any Treachery Card to obtain a Worthless Card special effect of your choice. The CHOAM rulebook gives Kull its attempted-Karama timing and phase restriction. Cunning changes the fuel identity, not the effect, target or timing. Neither text supplies a nested counter priority: distinct counters and BG-before-conversion custody are the recorded user-selected policy, not a publisher clarification.',
|
||||
'Wait for another player’s validated Karama attempt. There is no proactive Kull choice in the generic Nexus panel. The public event, attempted player and purpose are the same whether CHOAM holds CHOAM Nexus, another Nexus, no eligible fuel or no Nexus at all. Only CHOAM receives its private source-aware plays and blocking reasons; opponents do not see the cost list or reserved original card.',
|
||||
'Choose a cost through the shared selector and inspect that actual card before Use. Printed source requires a held printed Kull Wahad. Nexus source requires unallied native CHOAM holding the actual CHOAM Nexus in this profile and one uniquely held canonical Treachery Card: a weapon, defense, special, Karama or Worthless card can be fuel. Reserved or promise-incompatible fuel is unavailable. The card does not become a manufactured Kull, and Karama used as fuel does not activate its own power.',
|
||||
'Use must identify the offered event, selected source (printed or nexus) and actual fuel card. Omitting the source, guessing another event or supplying someone else’s card is not a valid Use. Decline needs only the event and spends nothing; the exact original attempt resumes once. You do not choose a new target, price, effect or Nexus identity.',
|
||||
'An accepted Nexus declaration discards the physical CHOAM Nexus once immediately. The selected fuel stays held while the ordinary CHOAM-effect response is pending. Pending Kull is not yet a phase ban. The interrupted original stays reserved; it cannot counter its own interruption. A distinct eligible printed Karama or Advanced BG substitution uses the existing response controls before any restriction begins.',
|
||||
'If Kull succeeds, discard exactly the selected fuel once, retain the original unplayed Karama or BG Worthless card and leave the original special once-use and additional costs unspent. The activating player, not an allied shipment beneficiary, cannot activate printed or substituted Karama for the stamped current turn and phase. This does not ban holding, acquiring, trading or otherwise legally discarding Karama.',
|
||||
'If a distinct counter prevents Kull, the selected fuel remains held under the existing exact-cost phase prevention policy, but the accepted Nexus spend is not refunded. No Kull activation ban is established. Resume the original attempt once with its normal costs. A BG original resumes its separate ordinary conversion response; ordinary prevention of that conversion still leaves its Worthless card discarded. A BG counter likewise owns its separate conversion, never the reserved original.',
|
||||
'The selected deferred-overbid policy still privately refuses non-CHOAM unfunded Karama-dependent bids before commitment, independently of hidden Kull or Nexus ownership. No auction restart, free award, invented funding or numeric UI cap is added. Own Truthtrance promises and reservations constrain the original, fuel and counters; unsupported continuations are guarded before an offer rather than silently releasing a promise.',
|
||||
'Refresh restores the same event, selected source, physical costs, spent Nexus and original continuation, including a suspended BG response. Corrupt ownership rejects without changing the game. Verified older printed-only counter saves receive a bounded immutable binding migration, not Nexus choices or a source-omitting action shim. Frozen checks and actual Basic/Advanced CLI/runtime/phone success and prevention are verified locally; complete CHOAM/Nexus, wider combinations and deployed acceptance remain open.',
|
||||
],
|
||||
example: 'CHOAM selects a held weapon as Nexus fuel when BG attempts a Worthless-as-Karama activation. Accepted Use spends CHOAM Nexus but keeps the weapon pending. If everyone allows Kull, the weapon is discarded and BG keeps its original Worthless card unplayed. If a different Karama prevents Kull, CHOAM keeps the weapon but not the Nexus, and BG’s original conversion continues once.',
|
||||
related: ['choam-kull', 'choam-worthless', 'nexus-cards', 'faction-choam', 'card-karama', 'advanced-beneGesserit', 'special-karama', 'bidding', 'privacy', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'The explicit new profile adds source-aware Kull fuel to the existing typed interception, with accepted-declaration Nexus spend and saved original continuation. It does not retrofit old games or admit wider module/faction compositions.', evidence: ['game/choam-kull.ts', 'game/nexus-choam.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Actual private weapon-fuel selection, fuel and Nexus inspectors, required source Use, distinct counter/allow and refreshed Basic/Advanced phone paths pass. Wider controls/presentation acceptance remains open.', evidence: ['components/choam-kull.tsx', 'components/choam-power-cost.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four profiles complete source-aware legal choices, decline and distinct counter continuations from canonical own projections. These are bounded legal policies, not calibrated Nexus strategy.', evidence: ['game/bot-choam-kull.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'This guide separates the printed effect and Cunning fuel from user-selected priority/custody and application privacy/spend conventions. New-profile limits and old printed-only behavior are explicit; publisher clarification and full-family acceptance remain open.', evidence: ['docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/CHOAM_KULL_DESIGN.md', 'docs/CHOAM_KULL_SOURCE_UPDATE.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Types/lint/6,022 offline tests/build, authenticated SQLite source ownership/restart/races and actual Basic/Advanced CLI/runtime/phone success/prevention/refresh pass. Review repairs protect canonical fuel, suspended native parents, reserved counters, payment coverage and old printed saves. Fresh profile omits Sandtrout. Local HTTP 49/55 retains four 503s and two timeouts; full combinations and deployed acceptance remain open.', evidence: ['tests/nexus-choam-kull-engine.test.ts', 'tests/nexus-choam-kull-recovery.test.ts', 'tests/nexus-choam-kull-controls.test.tsx', 'tests/bot-nexus-choam-kull.test.ts', 'tests/choam-kull-recovery.test.ts', 'docs/NEXUS_CHOAM_RUNTIME.md'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'nexus-choam-trade',
|
||||
title: 'CHOAM Nexus: Collection trade or victorious inspection',
|
||||
@@ -4119,7 +4149,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Native Fremen Cunning has a bounded Nexus-only path: when a natural, accepted additional, or Advanced special-Karama summoned worm appears in a territory initially containing no forces, Fremen may spend their own Nexus card before the worm resolves. The special summon retains its original Spice Blow parent. After the applicable Nexus, choose some forces from one occupied desert territory and ride to a legal Arrakis destination without using normal movement. The choice appears even without the secret card, so declining does not disclose custody. Karama provisionally prevents only the additional remote ride; the original worm still resolves and the card remains spent. An interrupted existing Cunning control is restored, and a selected ride waits for the summoned Nexus without granting a second Cunning offer; Great Maker and combined optional modules await integration.',
|
||||
'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.',
|
||||
'CHOAM Cunning spends its Nexus card to use one held Treachery Card for one chosen Worthless effect at that effect’s normal timing, in Basic and Advanced. Choose the physical card separately from the effect. Five of the six printed effects are supported: Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont. Cunning Kull remains unavailable; the separate printed Kull development preview excludes optional modules and does not authorize that sixth Nexus choice.',
|
||||
'CHOAM Cunning spends its Nexus card to use one held Treachery Card for one chosen Worthless effect at that effect’s normal timing, in Basic and Advanced. Choose the physical card separately from the effect. Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont retain their existing runtime. The explicit new Nexus Kull profile adds Kull at the canonical Karama reaction only; it remains unavailable in other Nexus previews. The old printed Kull profile still excludes Nexus. See the source-aware Kull guide for cost custody, counters and pending verification.',
|
||||
'One Karama response covers the CHOAM effect. Cancellation leaves the chosen Treachery Card in hand and the Nexus spent. An allowed use discards the actual card once, retaining its printed identity and applicable discard consequences; it does not create a named Worthless card or a turn-long conversion permission.',
|
||||
'Moritani Cunning modifies its normal Mentat placement: choose one available supply token and a territory on the printed Arrakis board, including a territory already containing Terror. The explicit Cunning toggle keeps ordinary placement and relocation unchanged. Homeworlds, the Hidden Mobile Stronghold, Cunning relocation and combined Grumman use are unavailable.',
|
||||
'With Moritani seated, an unallied holder of its Nexus card may spend Betrayal at a clean play boundary to return one placed Terror token to hidden Moritani supply without revealing its face. Choose the public location and token, not its secret effect; this does not use Moritani’s Mentat placement or trigger that token.',
|
||||
@@ -4130,13 +4160,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When Ecaz is absent, an unallied holder may spend its Nexus card at a quiet play boundary to ask whether any one player, including themself, holds one of the holder’s native faction leaders as an ordinary Traitor Card. The owner receives a private yes/no answer saved at the moment of use; no name or count is disclosed. Dead or captured native leaders count, but foreign Gholas, Duke Vidal, Cheap Hero and Face Dancers do not. Native-leader scope and private-only audience are explicit product interpretations, not publisher rulings; exceptional controlled-leader and public-disclosure combinations remain open.',
|
||||
'When Richese is absent, its Secret Ally replaces your ordinary reserve shipment with up to five physical forces charged as one. Select the explicit shipment checkbox; forces, elite identity, destination restrictions, Homeworld reserve sources and normal movement remain unchanged. Fremen remain free within their normal reinforcement range; Guild and an active Karama rate retain their existing tariff and payment routing. This is not a concealed No-Field or an extra shipment.',
|
||||
'Native Richese Cunning has a bounded two-No-Field shipment in genuine Richese/CHOAM Nexus games without other optional modules. Choose distinct unused tokens: one reveals immediately into reserve-capped public forces, the other remains concealed; pay for one marker and spend the Nexus. The previous shipment token cannot be either selection; the concealed token becomes next last-used. Karama or Guild special prevention preserves physical tokens and unpaid spice, with the ordinary stopped-shipment allowance policy. Other module combinations and the full faceup-component display remain gated.',
|
||||
'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide; its verification remains Planned.',
|
||||
'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide for its existing local runtime/browser evidence and remaining release gates; that evidence does not verify Nexus Kull.',
|
||||
'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable.',
|
||||
'With Guild absent, its Secret Ally is an explicit optional source for one ordinary shipment at Guild prices. It supports physical reserve shipment, cross-shipment and return to abstract reserves, plus half-price independently legal world-to-world shipment. Fremen may retain free reinforcement or choose the paid card route beyond its normal range, outside the storm. Native faction, typed sources and normal movement stay unchanged. Homeworld reserve return awaits a ruling; arbitrary foreign-world transport and concealed No-Fields are not granted.',
|
||||
'Cunning spends its Nexus card and uses the native Mentat placement opportunity. A Karama response precedes placement; cancellation leaves the token where it was and the Nexus spent. Later Terror entry still uses the existing one-token stack selection and supported effect rules. If Extortion was revealed this turn, its bank award and payment choices follow that placement response; this does not implement Atomics or complete Moritani games.',
|
||||
'Hidden reactive Betrayal timing remains unresolved for other effects. Complete module games are unfinished, so public module starts remain disabled.',
|
||||
],
|
||||
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-richese-betrayal'],
|
||||
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, Face Dancers, Suboids, advisors, native Voice, Emperor Sardaukar and Betrayal, bounded Fremen revival/Cunning, CHOAM effects, Moritani Cunning/Betrayal, Ecaz Betrayal, native Duke Cunning and private Secret Ally inquiry, Richese shipment and bounded auction Betrayal, and bounded Emperor bank-auction purchase are integrated. Ecaz Cunning transfers the existing Duke at a quiet Battle boundary with a real card and distinct turn-end expiry. Ecaz inquiry scope/audience and Emperor purchase timing are provisional. Borrowed Voice, exceptional Duke custody, other reactions, Tleilaxu revival, Fremen worm protection and combined Cunning interactions remain unfinished.', 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/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Private lifecycle, inspections, exchanges, Tleilaxu refresh, Ixian activation, advisor selection, temporary Sardaukar and CHOAM effects, Moritani Betrayal, Ecaz Betrayal and owner-only Secret Ally inquiry, native Duke Cunning, Richese shipment and bounded auction Betrayal, Emperor purchase and Fremen Cunning controls are connected. Remaining effect controls are unfinished.', 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'] },
|
||||
|
||||
Reference in new issue
Block a user