Prototype Emperor Nexus battle betrayal response

This commit is contained in:
Quintine committed 2026-09-27 19:29:50 +10:00
1 parent 361a1def43
commit 22a81c6f7a
10 files changed
+329 -6

No files matched your search

+6
View File
@@ -1420,6 +1420,8 @@ function policyActions(g: GameView): Action[] {
{ type: 'card', mode: 'special', card: specialCard.id, amount, elite },
];
}
if (g.nexusEmperorBetrayal)
return [{ type: 'nexusEmperorBetrayal', event: g.nexusEmperorBetrayal.event }];
if (g.response) {
const benefitEnemy =
g.response.owner !== me.id && g.response.owner !== me.ally;
@@ -1471,6 +1473,10 @@ function policyActions(g: GameView): Action[] {
g.response.intent === 'double'))))
)
return [{ type: 'card', card: card.id, mode: 'cancel' }];
if (g.response.kind === 'eliteStrength' && g.nexusCards &&
g.players.some((player) => player.id === g.response!.owner && player.faction === 'emperor') &&
!g.responseControls?.hasPassed)
return [{ type: 'passResponse' }];
return !g.responseControls?.cancelCards.length ||
g.responseControls.hasPassed
? []
+61 -1
View File
@@ -1466,6 +1466,8 @@ export type Game = {
}[];
/** A spent Ecaz card binds one exact allied force group until Karama settles. */
nexusEcazBetrayalHistory?: { snapshot: EcazBetrayalSnapshot; stage: 'pending' | 'complete' }[];
/** One actual Sardaukar battle advantage suppressed by a spent Emperor card. */
nexusEmperorBetrayalHistory?: { event: string; turn: number; territory: string; owner: string; target: string }[];
nexusFaceDancerHistory?: NexusFaceDancerReceipt[];
nexusSuboidHistory?: NexusSuboidReceipt[];
nexusSuboidLast?: { event: string; owner: string; turn: number };
@@ -1755,6 +1757,60 @@ function currentNexusChoamTrade(g: Game, owner: string) {
return nexusRule(() => quoteNexusChoamTrade(g, owner,
g.phase === 7 && grummanCollectionAutomatic(g)));
}
function emperorBetrayalWindow(g: Game) {
const response = g.response, battle = g.battle;
return !!(g.nexusCards?.cards && g.status === 'playing' && g.phase === 6 &&
battle && !battle.revealed && battle.event && response?.kind === 'eliteStrength' &&
response.owner === byFaction(g, 'emperor')?.id &&
[battle.attacker, battle.defender].includes(response.owner) &&
!battle.eliteBlocked?.includes(response.owner));
}
function nexusEmperorBetrayalOffer(g: Game, owner: string) {
const player = g.players.find((p) => p.id === owner);
if (g.truthtrance || !emperorBetrayalWindow(g) || !player || player.ally ||
owner === g.response!.owner || g.response!.passed.includes(owner) ||
g.nexusCards!.cards!.hands[owner] !== 'emperor') return null;
return { event: g.battle!.event!, target: g.response!.owner };
}
function nexusEmperorBetrayalIntegrity(g: Game) {
const history = g.nexusEmperorBetrayalHistory;
if (history === undefined) return;
requireRule(g.nexusCards?.cards && Array.isArray(history) && history.length > 0,
'Emperor Betrayal has lost its physical card module.');
const events = new Set<string>();
for (const record of history) {
requireRule(record && typeof record.event === 'string' && record.event.length > 0 &&
Number.isInteger(record.turn) && record.turn > 0 && record.turn <= g.turn &&
typeof record.territory === 'string' && record.territory.length > 0 &&
record.owner !== record.target &&
g.players.some(p => p.id === record.owner) &&
g.players.some(p => p.id === record.target && p.faction === 'emperor') &&
!events.has(record.event),
'Emperor Betrayal has a malformed or repeated battle receipt.');
events.add(record.event);
}
const last = history.at(-1)!;
if (last.turn === g.turn) requireRule(g.nexusCards.cards.discard.includes('emperor'),
'Emperor Betrayal has lost its spent physical card.');
if (g.battle?.event === last.event) requireRule(g.battle.territory === last.territory &&
g.battle.eliteBlocked?.includes(last.target),
'The current battle has lost its Emperor Betrayal suppression.');
}
function playNexusEmperorBetrayal(g: Game, p: Player, action: Action) {
const offer = nexusEmperorBetrayalOffer(g, p.id);
requireRule(offer && action.event === offer.event &&
Object.keys(action).sort().join(',') === 'event,type',
'Choose your current Emperor Sardaukar battle response.');
const response = g.response!;
const record = { event: offer.event, turn: g.turn, territory: g.battle!.territory,
owner: p.id, target: response.owner };
g.nexusCards!.cards = nexusRule(() =>
discardNexusCard(g.nexusCards!.cards!, p.id, g.players));
(g.nexusEmperorBetrayalHistory ??= []).push(record);
finishResponse(g, true);
log(g, `${p.name} spent Emperor Nexus Betrayal to suppress ${getPlayer(g, record.target).name}'s Sardaukar strength in this battle. Actual starred counters remain Sardaukar.`,
{ faction: p.faction, name: 'Emperor Nexus Betrayal' });
}
function nexusEmperorSecretAllyOffer(g:Game,owner:string) {
const p = g.players.find(p => p.id === owner);
if (!p || g.nexusCards?.cards?.hands[owner] !== 'emperor' || byFaction(g,'emperor')) return null;
@@ -14818,6 +14874,7 @@ function marketGholaIntegrity(g: Game) {
saphoAggressorIntegrity(g);
nexusCardsIntegrity(g);
nexusEmperorSecretIntegrity(g);
nexusEmperorBetrayalIntegrity(g);
nexusFremenRevivalIntegrity(g);
nexusFremenCunningIntegrity(g);
nexusEcazBetrayalIntegrity(g);
@@ -21317,7 +21374,8 @@ function settleAutomaticContinuations(g: Game) {
g.players.some(
(p) =>
!response.passed.includes(p.id) &&
responseCancelCards(g, p, response).length > 0,
(emperorBetrayalWindow(g) ||
responseCancelCards(g, p, response).length > 0),
)
)
return;
@@ -21477,6 +21535,7 @@ function applyActionInner(
if (t === 'discovery') { playDiscovery(g, p, action); return g; }
if (t === 'nexusChoamTrade') { playNexusChoamTrade(g, p, action); return g; }
if (t === 'nexusEmperorRevive') { playNexusEmperorRevive(g,p,action); return g; }
if (t === 'nexusEmperorBetrayal') { playNexusEmperorBetrayal(g,p,action); return g; }
if (t === 'nexusFremenRevive') { playNexusFremenRevive(g,p,action); return g; }
if (t === 'nexusMoritaniBetrayal') { playMoritaniBetrayal(g, p, action); return g; }
if (t === 'nexusEcazBetrayal') { playEcazBetrayal(g, p, action); return g; }
@@ -25393,6 +25452,7 @@ export function viewGame(state: Game, id: string) {
nexusCards: projectedNexusCards(g, id),
nexusChoamTrade: currentNexusChoamTrade(g, id),
nexusEmperorSecretAlly: nexusEmperorSecretAllyOffer(g,id),
nexusEmperorBetrayal: nexusEmperorBetrayalOffer(g, id),
nexusFremenRevival: fremenNexusRevivalOffer(g,id),
nexusMoritani: nexusMoritaniOffer(g,id),
nexusMoritaniBetrayal: moritaniBetrayalOffer(g,id,
+3 -2
View File
@@ -4007,7 +4007,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'One Karama response covers the entire declared territory set. Cancellation keeps every selected group as advisors and leaves the Nexus card spent. Existing territory restrictions still apply; fresh accompanied advisors and storm-related conversion remain unavailable pending their rulings.',
'Bene Gesserit Secret Ally borrowed Voice remains unavailable pending its cancellation and timing interpretation. Its printed permission does not make the other unfinished effects available.',
'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 has its own connected controls; other Emperor modes remain unfinished.',
'Temporary Sardaukar use the applicable strength and support rules, including the Fremen exception, but remain physically ordinary counters. Their losses go to Tanks as ordinary counters, without changing starred reserves or Tanks. Existing inspected or promised plans remain binding. The role ends after this battle; fewer-than-five groups remain unavailable. The separate Secret Ally revival has its own connected controls; Emperor purchase modes 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 first path requires base factions and Nexus alone, before any normal force returns, without combined revival effects. Partial returns, prior returns, higher free rates and separate sandworm protection remain unavailable.',
'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. Its after-victory inspection remains unfinished.',
@@ -4024,7 +4025,7 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion, temporary Emperor Sardaukar, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return, and Richese Secret Ally reserve shipping priced as one. Borrowed Voice, other Betrayal 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/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Physical lifecycle, Atreides inspections, Harkonnen exchanges/declarations, revealed Face Dancer refresh, turn-wide Suboid strength, whole-territory advisor conversion, temporary Emperor Sardaukar and actual Sardaukar Betrayal suppression, bounded Fremen Secret Ally free-three revival and native empty-worm Cunning, five source-aware CHOAM Cunning effects, Moritani Cunning placement and Betrayal token return, Ecaz Betrayal allied-force return, and Richese Secret Ally reserve shipping priced as one. Borrowed Voice, other Betrayal reactions, Emperor purchase effects, 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/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.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 with native Karama responses, Moritani Betrayal token selection, Ecaz Betrayal shared-territory choice, Richese shipment pricing and Fremen Cunning offer/remote ride. 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/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All profiles handle integrated effects through private offers, including Moritani Betrayal public-token selection, Ecaz Betrayal public-territory selection and legal Fremen Cunning offer/ride decisions. Complete module strategy and calibration remain 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-richese-options.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Common rules, twelve printed faces and integrated effect families retain explicit unresolved boundaries, including Ecaz Duke/inquiry, a second Cunning offer during an interrupted Fremen control, Great Maker and combined modules, fresh advisors, storm and borrowed Voice.', 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_GUILD_CUNNING_RUNTIME.md'] },