Integrate Harkonnen Nexus traitor retirement and Mentat replacement

This commit is contained in:
Quintine committed 2026-10-02 02:12:18 +10:00
1 parent 0c25aad62f
commit 1cf8ff72c7
25 files changed
+3049 -8

No files matched your search

+258 -1
View File
@@ -49,6 +49,7 @@ import { shipmentAvailable } from './shipment-opportunity';
import { createGuildBetrayalInvoice, createGuildBetrayalReceipt, guildBetrayalEligible, guildBetrayalResponders, validateGuildBetrayalInvoice, validateGuildBetrayalHistory, nexusGuildBetrayalSourceReceipt, type GuildBetrayalInvoice, type GuildBetrayalCursor, type GuildBetrayalReceipt, type GuildBetrayalAuthority } from './nexus-guild-betrayal';
import { createIxianReplacementSource, validateIxianReplacementSource, closeIxianReplacementSource, validateIxianReplacementHistory, initialIxianReplacementCursor, ixianReplacementEvent, type IxianReplacementSource, type IxianReplacementReceipt, type IxianReplacementCursor } from './nexus-ixian-replacement';
import { createIxianBetrayalSource, validateIxianBetrayalFrame, initialIxianBetrayalCursor, closeIxianBetrayalSource, validateIxianBetrayalHistory, ixianBetrayalResponders, ixianBetrayalEligible, type IxianBetrayalSource, type IxianBetrayalCursor, type IxianBetrayalReceipt } from './nexus-ixian-betrayal';
import { createHarkonnenBetrayalSource, validateHarkonnenBetrayalFrame, initialHarkonnenBetrayalCursor, closeHarkonnenBetrayalSource, validateHarkonnenBetrayalHistory, harkonnenBetrayalResponders, harkonnenBetrayalEligible, createHarkonnenBetrayalReplacement, validateHarkonnenBetrayalReplacements, drawHarkonnenBetrayalReplacement, type HarkonnenBetrayalSource, type HarkonnenBetrayalCursor, type HarkonnenBetrayalReceipt, type HarkonnenBetrayalReplacement, type HarkonnenBetrayalAuthority } from './nexus-harkonnen-betrayal';
import { createNexusGuildCunning, validateNexusGuildCunning, nexusGuildCunningMoves, type NexusGuildCunningReceipt } from './nexus-guild-cunning';
import { createNexusRichese, validateNexusRichese, quoteNexusRicheseShipment, type NexusRicheseReceipt } from './nexus-richese';
import { createNexusInspection, allowNexusInspection, answerNexusInspection, reopenNexusInspection, cancelNexusInspection, reopenNexusNative, answerNexusNative, validateNexusInspection, committedPlanElements, type NexusInspection, type BattleInspectionContext } from './battle-inspections';
@@ -724,6 +725,13 @@ export type Battle = {
plans: Record<string, Plan>;
revealed: boolean;
traitorCalls: Record<string, boolean>;
/** Original native allowance, retained separately from the Nexus source. */
nexusHarkonnenAllowance?: {
provider: string;
nativeWindow: 'direct' | 'harkonnenTraitor';
required: string[];
passed: string[];
};
preparation?: {
kind: 'voice' | 'prescience' | 'prescienceAnswer' | 'nexusPrescienceAnswer';
owner: string;
@@ -1139,6 +1147,17 @@ export type Game = {
} | null;
nexusIxianBetrayalHistory?: IxianBetrayalReceipt[];
nexusIxianBetrayalCursor?: IxianBetrayalCursor;
/** Fresh bounded native Harkonnen traitor-cancellation profile. */
nexusHarkonnenBetrayalPreview?: true;
pendingNexusHarkonnenBetrayal?: {
source: HarkonnenBetrayalSource;
required: string[];
passed: string[];
} | null;
nexusHarkonnenBetrayalHistory?: HarkonnenBetrayalReceipt[];
nexusHarkonnenBetrayalCursor?: HarkonnenBetrayalCursor;
pendingNexusHarkonnenReplacement?: HarkonnenBetrayalReplacement | null;
nexusHarkonnenReplacementHistory?: HarkonnenBetrayalReplacement[];
kullSequence?: number;
kullRestrictions?: KullPhaseRestriction[];
pendingKull?: {
@@ -8125,6 +8144,217 @@ export function initializeNexusGameForAudit(state: Game): Game {
'Enable Nexus cards in a fresh audit lobby first.');
return initializeSetupGameForAudit(state, !!state.homeworlds, true);
}
/** Fresh audit admission only; ordinary games and public starts never opt in. */
export function initializeHarkonnenNexusBetrayalGameForAudit(state: Game): Game {
requireRule(harkonnenNexusBetrayalModeSupported(state) &&
state.nexusCards?.cards === null && state.nexusCards.phase === null &&
state.nexusHarkonnenBetrayalPreview === undefined &&
state.pendingNexusHarkonnenBetrayal === undefined &&
state.nexusHarkonnenBetrayalHistory === undefined &&
state.nexusHarkonnenBetrayalCursor === undefined &&
state.pendingNexusHarkonnenReplacement === undefined &&
state.nexusHarkonnenReplacementHistory === undefined,
'Harkonnen Nexus Betrayal requires a fresh classic Harkonnen lobby with only base Treachery and Nexus.');
requireFreshBaseRuntime(state);
const g = initializeNexusGameForAudit(state);
g.nexusHarkonnenBetrayalPreview = true;
g.pendingNexusHarkonnenBetrayal = null;
g.nexusHarkonnenBetrayalHistory = [];
g.nexusHarkonnenBetrayalCursor = initialHarkonnenBetrayalCursor();
g.pendingNexusHarkonnenReplacement = null;
g.nexusHarkonnenReplacementHistory = [];
return g;
}
function harkonnenNexusBetrayalModeSupported(g: Game): boolean {
return typeof g.advanced === 'boolean' && !!g.nexusCards && g.expansions.length === 0 &&
g.players.length >= 2 && g.players.length <= 6 && !!byFaction(g,'harkonnen') &&
new Set(g.players.map(p => p.faction)).size === g.players.length &&
g.players.every(p => CLASSIC_FACTIONS[p.faction]) &&
!g.homeworlds && !g.leaderSkills && !g.discoveryEnabled && !g.discoveries &&
!g.discoveryStash && !g.greatMaker && !g.techTokens && !g.strongholdCards &&
!g.ecazTreachery && !g.semutaPreview && !g.kullPreview && !g.nexusKullPreview &&
!g.richeseBetrayalPreview && !g.guildBetrayalPreview &&
!g.nexusIxianReplacementPreview && !g.nexusIxianBetrayalPreview && !g.moritaniAssassinatePreview;
}
function harkonnenNexusBetrayalContext(g: Game) {
const b = g.battle!;
const plans = Object.fromEntries([b.attacker,b.defender].map(id => [id,{
leader:b.plans[id].leader,...(b.plans[id].kwisatz === undefined ? {} : {kwisatz:b.plans[id].kwisatz}),
}]));
const battle = {event:b.event!,attacker:b.attacker,defender:b.defender,plans,heroLeaderIds:[]};
return {status:g.status,phase:g.phase,turn:g.turn,advanced:g.advanced,
sequence:g.nexusHarkonnenBetrayalCursor!.sequence,players:g.players,
cards:g.nexusCards!.cards!,physicalCards:physicalTreacheryCards(g),
traitors:nexusTraitorSnapshot(g),universe:nexusTraitorUniverse(g),battle};
}
function harkonnenNexusBetrayalAuthority(g: Game): HarkonnenBetrayalAuthority {
const b = g.battle, allowance = b?.nexusHarkonnenAllowance;
requireRule(b?.revealed && b.event && allowance && g.phase === 6 &&
b.traitorCalls[allowance.provider] === true &&
traitorVoters(g,b).includes(allowance.provider) &&
getPlayer(g,allowance.provider).faction === 'harkonnen',
'The Harkonnen reaction lost its original legal native call.');
const declaration = nexusRule(() => createTraitorDeclaration(traitorDeclarationContext(g),allowance.provider));
requireRule(JSON.stringify(declaration) === JSON.stringify(b.traitorDeclarations?.[allowance.provider]) &&
allowance.nativeWindow === ([b.attacker,b.defender].includes(allowance.provider) ? 'direct' : 'harkonnenTraitor'),
'The Harkonnen call lost its original canonical declaration or native window.');
const required = allowance.nativeWindow === 'direct' ? [] :
g.players.filter(p => responseCancelCards(g,p,{kind:'harkonnenTraitor',owner:allowance.provider,passed:[]}).length > 0).map(p => p.id);
requireRule(JSON.stringify(required) === JSON.stringify(allowance.required) &&
new Set(allowance.passed).size === allowance.passed.length &&
allowance.passed.every(id => g.players.some(p => p.id === id)) &&
required.every(id => allowance.passed.includes(id)) &&
(allowance.nativeWindow !== 'direct' || allowance.passed.length === 0),
'The original native Harkonnen counters were not legally completed.');
return {provider:allowance.provider,declaration,nativeWindow:allowance.nativeWindow,
nativeRequired:required,nativePassed:allowance.passed,
nativeContext:JSON.stringify({event:b.event,territory:b.territory,attacker:b.attacker,
defender:b.defender,plans:b.plans,allowance})};
}
function harkonnenNexusBetrayalParent(g: Game): string {
return JSON.stringify({status:g.status,turn:g.turn,phase:g.phase,advanced:g.advanced,
active:g.active,order:g.order,ready:g.ready,battle:g.battle,
players:g.players.map(p => ({id:p.id,faction:p.faction,ally:p.ally,spice:p.spice,
hand:p.hand,traitors:p.traitors,revealedTraitors:p.revealedTraitors,leaders:p.leaders,
forces:p.forces,reserves:p.reserves,tanks:p.tanks,elites:p.elites,advisors:p.advisors})),
deck:g.deck,discard:g.discard,traitorReserve:g.traitorReserve,nexus:g.nexusCards,
response:g.response,decision:g.decision,truthtrance:g.truthtrance,
phaseOpening:g.phaseOpening,pendingKarama:g.pendingKarama,
pendingNullentropy:g.pendingNullentropy,pendingTreacheryDiscard:g.pendingTreacheryDiscard,
replacement:g.pendingNexusHarkonnenReplacement});
}
function harkonnenNexusBetrayalIntegrity(g: Game, mentatOpening = false): void {
const frame = g.pendingNexusHarkonnenBetrayal;
if (!g.nexusHarkonnenBetrayalPreview) {
requireRule(g.nexusHarkonnenBetrayalPreview === undefined && !frame &&
g.nexusHarkonnenBetrayalHistory === undefined && g.nexusHarkonnenBetrayalCursor === undefined &&
!g.pendingNexusHarkonnenReplacement && g.nexusHarkonnenReplacementHistory === undefined,
'Harkonnen Nexus Betrayal lost its explicit fresh profile.');
return;
}
requireRule(harkonnenNexusBetrayalModeSupported(g) && g.nexusHarkonnenBetrayalCursor &&
Array.isArray(g.nexusHarkonnenBetrayalHistory) && Array.isArray(g.nexusHarkonnenReplacementHistory),
'The Harkonnen profile lost its bounded history.');
const universe = nexusTraitorUniverse(g);
nexusCardsIntegrity(g);
nexusRule(() => validateHarkonnenBetrayalHistory(g.nexusHarkonnenBetrayalHistory!,g.nexusHarkonnenBetrayalCursor!,universe));
nexusRule(() => validateHarkonnenBetrayalReplacements(g.nexusHarkonnenBetrayalHistory!,
g.nexusHarkonnenBetrayalCursor!,[...g.nexusHarkonnenReplacementHistory!,
...(g.pendingNexusHarkonnenReplacement ? [g.pendingNexusHarkonnenReplacement] : [])],universe));
requireRule(g.nexusHarkonnenBetrayalHistory!.every(r => r.source.turn <= g.turn) &&
g.nexusHarkonnenReplacementHistory!.every(r => r.status === 'drawn' && r.turn <= g.turn) &&
(!g.pendingNexusHarkonnenReplacement || (g.status === 'playing' &&
g.pendingNexusHarkonnenReplacement.status === 'due' &&
g.pendingNexusHarkonnenReplacement.turn === g.turn &&
(g.phase === 6 || g.phase === 7 || (mentatOpening && g.phase === 8)))),
'The private replacement lost its current native Mentat obligation.');
if (g.status !== 'setup' && g.status !== 'lobby')
nexusRule(() => validateNexusTraitorSnapshot(nexusTraitorSnapshot(g),universe));
if (!frame) {
const battle = g.battle, allowance = battle?.nexusHarkonnenAllowance;
if (allowance) {
const closed = g.nexusHarkonnenBetrayalHistory!.find(receipt =>
receipt.source.declaration.event === battle!.event && receipt.source.provider === allowance.provider);
requireRule(closed && closed.source.nativeWindow === allowance.nativeWindow &&
JSON.stringify(closed.source.nativeRequired) === JSON.stringify(allowance.required) &&
JSON.stringify(closed.source.nativePassed) === JSON.stringify(allowance.passed) &&
JSON.stringify(closed.source.declaration) === JSON.stringify(battle!.traitorDeclarations?.[allowance.provider]) &&
battle!.traitorCalls[allowance.provider] === (closed.outcome === 'pass'),
'The original native Harkonnen allowance lost its pending or closed Nexus acknowledgement.');
}
return;
}
requireRule(Object.keys(frame).sort().join(',') === 'passed,required,source' &&
Array.isArray(frame.required) && Array.isArray(frame.passed) &&
frame.required.some(id => !frame.passed.includes(id)) &&
!g.response && !g.decision && !g.phaseOpening && !g.truthtrance &&
!g.pendingKarama && !g.pendingNullentropy && !g.pendingTreacheryDiscard,
'The Harkonnen acknowledgement lost original native priority.');
nexusRule(() => validateHarkonnenBetrayalFrame(harkonnenNexusBetrayalContext(g),frame.source,
harkonnenNexusBetrayalAuthority(g),harkonnenNexusBetrayalParent(g),frame.required,frame.passed));
}
function offerHarkonnenNexusBetrayal(g: Game, provider: string, response?: ResponseWindow): boolean {
if (!g.nexusHarkonnenBetrayalPreview || getPlayer(g,provider).faction !== 'harkonnen') return false;
const b = g.battle!;
const required = nexusRule(() => harkonnenBetrayalResponders(g.nexusCards!.cards!,g.players));
if (!required.length) return false;
requireRule(!b.nexusHarkonnenAllowance && (!response || response.kind === 'harkonnenTraitor'),
'The original Harkonnen declaration was already acknowledged.');
b.nexusHarkonnenAllowance = {provider,nativeWindow:response ? 'harkonnenTraitor' : 'direct',
required:response ? g.players.filter(p => responseCancelCards(g,p,response).length > 0).map(p => p.id) : [],
passed:response ? [...response.passed] : []};
const declaration = harkonnenNexusBetrayalAuthority(g).declaration;
const owner = getPlayer(g,provider);
owner.revealedTraitors = [...new Set([...(owner.revealedTraitors ?? []),declaration.identity])];
const name = g.players.flatMap(p => p.leaders).find(l => l.id === declaration.leader)?.name ?? declaration.leader;
log(g,`${owner.name} revealed ${name} as a traitor.`);
const source = nexusRule(() => createHarkonnenBetrayalSource(harkonnenNexusBetrayalContext(g),
harkonnenNexusBetrayalAuthority(g),harkonnenNexusBetrayalParent(g)));
g.pendingNexusHarkonnenBetrayal = {source,required,passed:[]};
return true;
}
function harkonnenNexusBetrayalView(g: Game,id: string) {
const frame = g.pendingNexusHarkonnenBetrayal;
if (!frame) return null;
const hasPassed = frame.passed.includes(id), canPass = frame.required.includes(id) && !hasPassed;
const d = frame.source.declaration;
return {event:frame.source.event,provider:frame.source.provider,beneficiary:d.beneficiary,
target:d.target,leader:d.leader,identity:d.identity,canPass,hasPassed,
canUse:canPass && nexusRule(() => harkonnenBetrayalEligible(g.nexusCards!.cards!,g.players,id)),blocked:null as string | null};
}
function decideHarkonnenNexusBetrayal(g: Game,id: string,action: Action): void {
harkonnenNexusBetrayalIntegrity(g);
const frame = g.pendingNexusHarkonnenBetrayal!;
requireRule(['nexusHarkonnenBetrayalPass','nexusHarkonnenBetrayalUse'].includes(action?.type) &&
action.event === frame.source.event && Object.keys(action).length === 2 &&
Object.keys(action).every(key => key === 'type' || key === 'event') &&
frame.required.includes(id) && !frame.passed.includes(id),
'A current required seat must acknowledge only the original Harkonnen event.');
const use = action.type === 'nexusHarkonnenBetrayalUse';
if (use) requireRule(!g.pendingNexusHarkonnenReplacement &&
nexusRule(() => harkonnenBetrayalEligible(g.nexusCards!.cards!,g.players,id)),
'You do not own an eligible Harkonnen Nexus card or the prior replacement is still due.');
if (!use) {
frame.passed.push(id);
if (frame.required.some(seat => !frame.passed.includes(seat))) return;
}
const closed = nexusRule(() => closeHarkonnenBetrayalSource(frame.source,use ? 'use' : 'pass',
use ? id : null,g.nexusHarkonnenBetrayalCursor!));
if (use) {
const owner = getPlayer(g,frame.source.provider), identity = frame.source.declaration.identity;
requireRule(owner.traitors.filter(card => card === identity).length === 1 && g.traitorReserve,
'The original called physical traitor is no longer held exactly once.');
const replacement = nexusRule(() => createHarkonnenBetrayalReplacement(closed.receipt));
g.nexusCards!.cards = nexusRule(() => discardNexusCard(g.nexusCards!.cards!,id,g.players));
owner.traitors = owner.traitors.filter(card => card !== identity);
owner.revealedTraitors = (owner.revealedTraitors ?? []).filter(card => card !== identity);
g.traitorReserve = shuffle([...g.traitorReserve,identity]);
g.battle!.traitorCalls[owner.id] = false;
g.pendingNexusHarkonnenReplacement = replacement;
log(g,`${getPlayer(g,id).name} spent Harkonnen Nexus Betrayal. The declared Traitor Card returned to the shuffled Traitor Deck; Harkonnen will draw one private replacement during this turn’s Mentat Pause.`);
}
g.nexusHarkonnenBetrayalHistory!.push(closed.receipt);
g.nexusHarkonnenBetrayalCursor = closed.cursor;
g.pendingNexusHarkonnenBetrayal = null;
if (traitorVoters(g,g.battle!).every(voter => g.battle!.traitorCalls[voter] !== undefined))
advanceBattleOutcome(g);
}
function drawHarkonnenNexusBetrayalReplacement(g: Game): void {
const due = g.pendingNexusHarkonnenReplacement;
if (!due) return;
harkonnenNexusBetrayalIntegrity(g,true);
requireRule(g.status === 'playing' && g.phase === 8 && due.turn === g.turn &&
g.traitorReserve && g.traitorReserve.length > 0,
'The private Harkonnen replacement requires its actual current Mentat opening and Traitor Deck.');
const identity = g.traitorReserve[0];
const drawn = nexusRule(() => drawHarkonnenBetrayalReplacement(due,g.nexusHarkonnenBetrayalHistory!,
g.nexusHarkonnenBetrayalCursor!,identity,nexusTraitorUniverse(g)));
g.pendingNexusHarkonnenReplacement = null;
g.nexusHarkonnenReplacementHistory!.push(drawn);
g.traitorReserve.shift();
getPlayer(g,due.provider).traitors.push(identity);
log(g,`${getPlayer(g,due.provider).name} drew the one private Traitor Card replacement owed by Harkonnen Nexus Betrayal.`);
}
/** Offline-only, fresh native Ixian/Ix47 + Nexus profile. */
export function initializeIxianNexusBetrayalGameForAudit(state: Game): Game {
requireRule(ixianNexusBetrayalModeSupported(state) &&
@@ -16945,6 +17175,7 @@ function beginPhase(g: Game) {
collect(g);
}
if (g.phase === 8) {
drawHarkonnenNexusBetrayalReplacement(g);
replaceMoritaniAssassinationTraitors(g);
if (g.inflation && g.inflation.updatedTurn < g.turn) {
if (g.inflation.flipped) {
@@ -19869,6 +20100,8 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
for (const revelation of quote.revelations) {
const holder = getPlayer(g, revelation.player),
identity = revelation.identity;
if (b.nexusHarkonnenAllowance?.provider === holder.id &&
b.traitorDeclarations?.[holder.id]?.identity === identity) continue;
holder.revealedTraitors = [
...new Set([...(holder.revealedTraitors ?? []), identity]),
];
@@ -22732,6 +22965,7 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
} else if (response.kind === 'harkonnenTraitor') {
const b = g.battle!;
if (canceled) b.traitorCalls[response.owner] = false;
else if (offerHarkonnenNexusBetrayal(g,response.owner,response)) return;
if (
traitorVoters(g, b).every((voter) => b.traitorCalls[voter] !== undefined)
)
@@ -25464,6 +25698,7 @@ function normalizeCardNames(g: Game) {
]);
}
export function applyAction(state: Game, id: string, action: Action): Game {
harkonnenNexusBetrayalIntegrity(state);
ixianNexusBetrayalIntegrity(state);
ixianNexusReplacementIntegrity(state);
guildBetrayalIntegrity(state);
@@ -25552,6 +25787,8 @@ export function applyAction(state: Game, id: string, action: Action): Game {
return normalizeAutomaticGame(state);
}
const g = applyActionInner(state, id, action);
harkonnenNexusBetrayalIntegrity(g);
if (g.pendingNexusHarkonnenBetrayal) return g;
ixianNexusBetrayalIntegrity(g);
if (g.pendingNexusIxianBetrayal) return g;
ixianNexusReplacementIntegrity(g);
@@ -25686,9 +25923,11 @@ export function applyAction(state: Game, id: string, action: Action): Game {
guildRateIntegrity(g);
arrivalOverlapIntegrity(g);
guildBetrayalIntegrity(g);
harkonnenNexusBetrayalIntegrity(g);
return g;
}
function finishActionContinuations(g: Game) {
if (g.pendingNexusHarkonnenBetrayal) return;
if (g.pendingNexusIxianBetrayal) return;
if (g.pendingGuildBetrayal) return;
if (g.pendingRicheseBetrayal) return;
@@ -25754,6 +25993,7 @@ function finishActionContinuations(g: Game) {
finishMoritaniPlacement(g);
}
function settleAutomaticContinuations(g: Game) {
if (g.pendingNexusHarkonnenBetrayal) return;
if (g.pendingNexusIxianBetrayal) return;
if (g.pendingNexusIxianReplacement) return;
if (g.pendingGuildBetrayal) return;
@@ -25762,6 +26002,7 @@ function settleAutomaticContinuations(g: Game) {
if (pendingNexusTraitors(g)) return;
if (g.pendingNullentropy) return;
for (let iteration = 0; iteration < 128; iteration++) {
if (g.pendingNexusHarkonnenBetrayal) return;
if (g.pendingNexusIxianBetrayal) return;
if (g.pendingGuildBetrayal) return;
if (g.pendingNexusIxianReplacement) return;
@@ -25787,6 +26028,7 @@ function settleAutomaticContinuations(g: Game) {
finishResponse(g, false);
} else if (!finishAutomaticDecision(g)) return;
finishActionContinuations(g);
if (g.pendingNexusHarkonnenBetrayal) return;
if (g.pendingNexusIxianBetrayal) return;
if (g.pendingGuildBetrayal) return;
if (g.pendingRicheseBetrayal) return;
@@ -25805,6 +26047,7 @@ function settleAutomaticContinuations(g: Game) {
}
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
export function normalizeAutomaticGame(state: Game): Game {
harkonnenNexusBetrayalIntegrity(state);
ixianNexusBetrayalIntegrity(state);
ixianNexusReplacementIntegrity(state);
guildBetrayalIntegrity(state);
@@ -25843,6 +26086,7 @@ export function normalizeAutomaticGame(state: Game): Game {
ecazCollectionIntegrity(state);
ecazAllianceIntegrity(state);
const g = structuredClone(state);
if (g.pendingNexusHarkonnenBetrayal) return g;
if (g.pendingNexusIxianBetrayal) return g;
if (g.pendingNexusIxianReplacement) return g;
if (g.pendingGuildBetrayal) return g;
@@ -25886,6 +26130,7 @@ export function normalizeAutomaticGame(state: Game): Game {
moritaniExtortionIntegrity(g);
guildRateIntegrity(g);
arrivalOverlapIntegrity(g);
harkonnenNexusBetrayalIntegrity(g);
return g;
}
@@ -25897,6 +26142,12 @@ function applyActionInner(
): Game {
ecazTreacheryIntegrity(state);
const g = structuredClone(state);
if (g.pendingNexusHarkonnenBetrayal) {
getPlayer(g,id);
if (action?.type === 'advanceBots') return g;
decideHarkonnenNexusBetrayal(g,id,action);
return g;
}
if (g.pendingNexusIxianBetrayal) {
getPlayer(g,id);
if (action?.type === 'advanceBots') return g;
@@ -29592,6 +29843,7 @@ function applyActionInner(
g.response = { kind: 'harkonnenTraitor', owner: id, passed: [] };
return g;
}
if (action.call && offerHarkonnenNexusBetrayal(g,id)) return g;
if (
traitorVoters(g, b).every((voter) => b.traitorCalls[voter] !== undefined)
@@ -29758,6 +30010,7 @@ function applyActionInner(
throw new RuleError('That action is not available.');
}
export function viewGame(state: Game, id: string) {
harkonnenNexusBetrayalIntegrity(state);
ixianNexusBetrayalIntegrity(state);
ixianNexusReplacementIntegrity(state);
guildBetrayalIntegrity(state);
@@ -30133,6 +30386,8 @@ export function viewGame(state: Game, id: string) {
? {...nexusRule(() => quoteDiscoveryEntry({...g,discoveries:g.discoveries!},g.discoveryEntry!,id)),event:g.decision.event} : null,
greatMaker: g.greatMaker ? { event:g.greatMaker.event, turn:g.greatMaker.turn, stage:g.greatMaker.stage, votes:structuredClone(g.greatMaker.votes), order:[...g.greatMaker.order], ride:greatMakerRideOptions(g,id) } : null,
nexusCards: projectedNexusCards(g, id),
nexusHarkonnenBetrayalPreview: !!g.nexusHarkonnenBetrayalPreview,
nexusHarkonnenBetrayalReaction: harkonnenNexusBetrayalView(g,id),
nexusIxianBetrayalPreview: !!g.nexusIxianBetrayalPreview,
nexusIxianBetrayalReaction: ixianNexusBetrayalView(g,id),
nexusIxianReplacementPreview: !!g.nexusIxianReplacementPreview,
@@ -30204,7 +30459,9 @@ export function viewGame(state: Game, id: string) {
storm: g.status === 'lobby' ? STORM_START_SECTOR : g.storm,
playerPositions: normalizedPlayerPositions(g),
order: g.order,
active: g.pendingNexusIxianBetrayal
active: g.pendingNexusHarkonnenBetrayal
? (() => {const remaining = g.pendingNexusHarkonnenBetrayal.required.filter(seat => !g.pendingNexusHarkonnenBetrayal!.passed.includes(seat)); return remaining.length === 1 ? remaining[0] : null;})()
: g.pendingNexusIxianBetrayal
? (() => {const remaining = g.pendingNexusIxianBetrayal.required.filter(seat => !g.pendingNexusIxianBetrayal!.passed.includes(seat)); return remaining.length === 1 ? remaining[0] : null;})()
: g.active,
movementRemaining: g.movementRemaining ?? null,