Connect Guild Nexus Betrayal receipt to Bureaucrat per ruling

This commit is contained in:
Quintine committed 2026-10-13 17:47:00 +11:00
1 parent 9b83b61669
commit df9bbc5e04
11 files changed
+534 -16

No files matched your search

+2 -1
View File
@@ -1,4 +1,4 @@
import { homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, noOtherLeaderSkillModules, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile, type LeaderSkillProfile } from './leader-skill-profile';
import { classicDiscoveryLeaderSkillsProfile } from './discovery-module-profile';
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe' | 'revival';
export type BureaucratPaymentSource = {
@@ -43,6 +43,7 @@ export function bureaucratPaymentModeSupported(game: LeaderSkillProfile): boolea
(game.expansions[0] === 'choam' || game.expansions[0] === 'ix') &&
pairedNexusLeaderSkillsProfile(game)) ||
mixedE3NexusLeaderSkillsProfile(game) || mixedE1E2NexusLeaderSkillsProfile(game) || pairedE3NexusLeaderSkillsProfile(game) ||
guildBetrayalNexusLeaderSkillsProfile(game) ||
(noOtherLeaderSkillModules(game) &&
(!game.expansions.length ||
(game.expansions.length === 1 && game.expansions[0] === 'choam') ||
+47 -4
View File
@@ -1,7 +1,7 @@
import { classicNexusModulesProfile, mixedE1E2NexusProfile, mixedE3NexusProfile, pairedE3NexusProfile, pairedNexusModulesProfile, singleE1E2NexusProfile, standaloneE3NexusProfile } from './nexus-module-profile';
import { nativeFactionTechProfile } from './faction-module-profile';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, homeworldLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
@@ -2040,7 +2040,7 @@ export type Game = {
bureaucratPayments?: {used:BureaucratPaymentUse[];pending?:{
source:BureaucratPaymentSource;owner:string;leader:string;
resume:{response:ResponseWindow|null;decision:Decision|null};
continuation:{kind:'response'}|{kind:'auction';quote:AuctionContinuationQuote}|{kind:'bribe'};
continuation:{kind:'response'}|{kind:'auction';quote:AuctionContinuationQuote}|{kind:'bribe'}|{kind:'guildBetrayal';event:string;recipient:string;amount:number};
frame:string;signature:string;
}};
bureaucratPaymentEvent?: string;
@@ -8562,6 +8562,16 @@ function bureaucratPaymentIntegrity(g: Game) {
else if (pending.continuation.kind === 'auction') requireRule(g.currentAuctionSale?.bureaucratPaymentEvent === pending.source.event &&
JSON.stringify(pending.continuation.quote) === JSON.stringify(currentAuctionContinuationQuote(g,{kind:'sale',free:false})),
'The Bureaucrat sale lost its original seller-credit continuation.');
else if (pending.continuation.kind === 'guildBetrayal') {
const betrayal = pending.continuation;
const receipt = (g.nexusGuildBetrayalHistory ?? []).find(entry => entry.invoice.event === betrayal.event);
requireRule(pending.source.kind === 'shipment' && g.guildBetrayal?.status === 'completed' &&
!!receipt && receipt.recipient === betrayal.recipient &&
receipt.invoice.price === betrayal.amount &&
pending.source.payee === betrayal.recipient &&
pending.source.amount === betrayal.amount,
'The Bureaucrat Betrayal receipt lost its original holder payment.');
}
else requireRule(pending.continuation.kind === 'bribe' && pending.source.kind === 'bribe',
'The Bureaucrat payment has an invalid continuation.');
}
@@ -8604,6 +8614,11 @@ function actBureaucratPayment(g: Game,action:Action) {
g.response = pending.resume.response;g.decision = pending.resume.decision;
if (pending.continuation.kind === 'response') finishResponse(g,false,redirected);
else if (pending.continuation.kind === 'auction') commitAuctionContinuation(g,pending.continuation.quote,redirected);
else if (pending.continuation.kind === 'guildBetrayal') {
// The native adapter already credited the full fee to the holder; the ruled
// diversion takes exactly two from that receipt.
if (redirected) getPlayer(g,pending.continuation.recipient).spice -= redirected;
}
else getPlayer(g,pending.source.payee).bribes += pending.source.amount-redirected;
}
function leaderSkillRevivalBlocked(g: Game, owner: string): boolean {
@@ -9485,11 +9500,31 @@ export function initializeGuildBetrayalGameForAudit(state: Game): Game {
g.pendingGuildBetrayal = null;
return g;
}
/** Development-only composition of the bounded Betrayal preview with classic Leader Skills. */
export function initializeGuildBetrayalLeaderSkillsGameForAudit(state: Game): Game {
requireRule(!state.leaderSkills && !state.guildBetrayalPreview && !state.guildBetrayal &&
!state.pendingGuildBetrayal && !state.nexusGuildBetrayalHistory && !state.guildBetrayalSourceReceipts &&
(!state.nexusCards || state.nexusCards.cards === null && state.nexusCards.phase === null) &&
!state.homeworlds && !state.discoveryEnabled && !state.discoveries && !state.techTokens &&
!state.strongholdCards && !state.ecazTreachery && !state.kullPreview && !state.nexusKullPreview &&
!state.richeseBetrayalPreview && !state.mobileStronghold && state.expansions.length === 0 &&
state.players.length >= 2 && state.players.length <= 6 && !!byFaction(state, 'guild') &&
state.players.every(p => CLASSIC_FACTIONS[p.faction] === true),
'Guild Betrayal with Leader Skills needs a fresh classic native-Guild lobby with only Nexus.');
const g = structuredClone(state);
g.nexusCards = {cards: null, phase: null};
g.guildBetrayalPreview = true;
g.guildBetrayal = {sequence: 0, event: null, turn: null, sourceSignature: null, status: 'completed'};
g.nexusGuildBetrayalHistory = [];
g.guildBetrayalSourceReceipts = [];
g.pendingGuildBetrayal = null;
return initializeLeaderSkillsGameForAudit(g);
}
function guildBetrayalModeSupported(g: Game): boolean {
return !!g.nexusCards && g.expansions.length === 0 && g.players.length >= 2 &&
g.players.length <= 6 && !!byFaction(g, 'guild') &&
g.players.every(p => CLASSIC_FACTIONS[p.faction] === true) &&
!g.leaderSkills && !g.discoveryEnabled && !g.discoveries && !g.techTokens &&
(!g.leaderSkills || guildBetrayalNexusLeaderSkillsProfile(g)) && !g.discoveryEnabled && !g.discoveries && !g.techTokens &&
!g.strongholdCards && !g.ecazTreachery && !g.kullPreview && !g.nexusKullPreview &&
!g.richeseBetrayalPreview && !g.mobileStronghold;
}
@@ -9717,6 +9752,14 @@ function decideGuildBetrayal(g: Game, p: Player, action: Action): void {
sourceSignature: frame.invoice.sourceSignature, status: 'completed'};
if (use) log(g, `${p.name} spent Guild Nexus Betrayal to receive the full ${frame.invoice.price}-spice shipment payment. The original funded shipment completed once.`,
{faction: 'guild', name: 'Betrayal'});
if (use && settlement.recipient) {
const legs = [{payer: frame.invoice.shipper, amount: frame.invoice.ownPayment},
...(frame.invoice.donor ? [{payer: frame.invoice.donor, amount: frame.invoice.allyPayment}] : [])];
const source = stampBureaucratContributors(g, 'shipment', settlement.recipient, legs,
frame.invoice.allyPayment > 0).bureaucratPayment;
if (offerBureaucratPayment(g, source, {kind: 'guildBetrayal', event: frame.invoice.event,
recipient: settlement.recipient, amount: frame.invoice.price})) return;
}
}
/** Offline paired-faction Nexus setup; public expansion/module starts remain gated. */
export function initializePairedNexusGameForAudit(state: Game): Game {
@@ -9997,7 +10040,7 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
const mixedE3 = mixedE3NexusLeaderSkillsProfile(g);
const initialized = initializeSetupGameForAudit(g, homeworlds, classicNexus || pairedNexusLeaderSkillsProfile(g) ||
singleE1E2NexusLeaderSkillsProfile(g) || mixedE1E2NexusLeaderSkillsProfile(g) ||
mixedE3 || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3, false,
mixedE3 || standaloneE3NexusLeaderSkillsProfile(g) || pairedE3 || guildBetrayalNexusLeaderSkillsProfile(g), false,
classicDiscoveryLeaderSkillsProfile(g) || nativeDiscoveryLeaderSkillsProfile(g) || (g.discoveryEnabled === true && classicNexus), true,
g.expansions.length === 1 && g.expansions[0] === 'choam', nativeExpansionLeaderSkillsProfile(g));
if (advancedMoritaniLeaderSkillsProfile(initialized) || ((mixedE3 || pairedE3) && initialized.advanced &&
+17 -2
View File
@@ -34,7 +34,7 @@ export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean {
!game.discoveryEnabled && !game.strongholdCards && !game.techTokens && !game.ecazTreachery;
}
function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile, assassination = false, discovery = false): boolean {
function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile, assassination = false, discovery = false, betrayal = false): boolean {
return !!game.nexusCards && typeof game.advanced === 'boolean' &&
!!game.players && game.players.length >= 2 && game.players.length <= 6 &&
((discovery && game.discoveryEnabled === true) ||
@@ -46,7 +46,7 @@ function nexusLeaderSkillModulesSupported(game: LeaderSkillProfile, assassinatio
!game.discoveryEnabled && !game.discoveries)) &&
(!game.moritaniAssassinatePreview || assassination) &&
!game.advancedPreview && !game.kullPreview && !game.nexusKullPreview &&
!game.guildBetrayalPreview && !game.richeseBetrayalPreview &&
(!game.guildBetrayalPreview || betrayal) && !game.richeseBetrayalPreview &&
!game.nexusIxianReplacementPreview && !game.nexusIxianBetrayalPreview &&
!game.nexusHarkonnenBetrayalPreview &&
!game.spiceBankerIncomePreview;
@@ -66,6 +66,21 @@ export function classicNexusLeaderSkillsProfile(game: LeaderSkillProfile): boole
return true;
}
/** Fresh classic Nexus/Skills composed only with the bounded Guild Betrayal preview. */
export function guildBetrayalNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (game.expansions.length || !game.guildBetrayalPreview ||
!nexusLeaderSkillModulesSupported(game, false, false, true)) return false;
const players = game.players!;
for (let i = 0; i < players.length; i++) {
for (let j = 0; j < i; j++) if (players[j].faction === players[i].faction) return false;
let classic = false;
for (const faction of FACTIONS)
if (faction.id === players[i].faction) { classic = faction.expansion === 'base'; break; }
if (!classic) return false;
}
return true;
}
/** Both original natives in one E1/E2 family, optionally Homeworlds/Discovery/Tech/Strongholds. */
export function pairedNexusLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
if (game.expansions.length !== 1 || !nexusLeaderSkillModulesSupported(game, false, true)) return false;
+1 -1
View File
@@ -4802,7 +4802,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic or supported native banker-income profiles, including the bounded Advanced Moritani roster; ordinary/default starts are unchanged.',
'Diplomat may use one committed Worthless as the opponent’s canonical printed defensive role when you played no actual defense: Shield, Snooper, Shield Snooper, defensive Weirding Way or Chemistry, or sealed Portable Snooper. Hybrid protection, Weirding’s non-Shield behavior and Chemistry’s Poison Tooth exception remain distinct. Copying the used role without its activation or own-slot prerequisite is the explicit first-version interpretation. Choose or decline after reveal, before traitors; the Worthless must be discarded even if you win. Already-open base-only battles keep that source window. A native Diplomat can protect its own selected disc; a captive grants no upper band. A surviving trained loser may retreat selected undialed physical counters up to printed/copied disc strength to one empty adjacent non-stronghold before losses. Late/mirror/numeric modifications remain separate.',
'Normal Mentat questions use the user-approved uniform private card-show step before posture and faction powers in supported Leader Skills games. The opponent shows the named held weapon or chooses another held card, even for an exact match or sole eligible card. Weirding Way is a nameable default weapon; Chemistry is not. Cards stay held and need not enter a Battle Plan. New battles activate the question without an extra opt-in; already-opened unversioned battles retain their original flow. Empty hands skip without an answer. Private history records an observation, not a later hand guarantee.',
'Bureaucrat may redirect two spice when one other player pays at least five to a player other than the skill owner, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with their remaining share unavailable until Mentat Pause. Supported auctions, Guild income, Richese sales and paid Tleilaxu revival have connected paths; a separate free-revival award does not make a smaller price qualify. Allied payer shares below five each, such as three plus three, never pool to qualify. Supported classic, E1, E2 and E3 games without Homeworlds additionally connect qualifying allied-funded ordinary auctions, Richese cache/Advanced Black Market and shipments, including fully ally-funded five: one optional two-spice diversion from the gross recipient receipt, unchanged full contributor costs. Supported Homeworlds plus Leader Skills games connect the same allied-funded auction and Guild-shipment income. Original Guild/Emperor income cancellation resolves first; Richese proceeds use their original direct seller-credit callback without an invented cancellation window. Basic still has no Black Market and free Karama purchases remain normal-only. A trainer or payee contribution cannot qualify, but does not hide an eligible other payer. The prompt shows public gross and recipient, not private shares or which payer qualified. By user ruling the diversion comes off the gross before any Homeworld occupation split, but that split-recipient composition remains fenced from Leader Skills. By user ruling a Guild Nexus Betrayal receipt still carries Bureaucrat (holder receives the fee minus two), pending that composition. By user ruling, Emperor gifts are not payments: no Bureaucrat prompt/use or threshold disclosure, and exact gift amounts remain private. Other payment-family/module interactions remain unfinished.',
'Bureaucrat may redirect two spice when one other player pays at least five to a player other than the skill owner, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with their remaining share unavailable until Mentat Pause. Supported auctions, Guild income, Richese sales and paid Tleilaxu revival have connected paths; a separate free-revival award does not make a smaller price qualify. Allied payer shares below five each, such as three plus three, never pool to qualify. Supported classic, E1, E2 and E3 games without Homeworlds additionally connect qualifying allied-funded ordinary auctions, Richese cache/Advanced Black Market and shipments, including fully ally-funded five: one optional two-spice diversion from the gross recipient receipt, unchanged full contributor costs. Supported Homeworlds plus Leader Skills games connect the same allied-funded auction and Guild-shipment income. Original Guild/Emperor income cancellation resolves first; Richese proceeds use their original direct seller-credit callback without an invented cancellation window. Basic still has no Black Market and free Karama purchases remain normal-only. A trainer or payee contribution cannot qualify, but does not hide an eligible other payer. The prompt shows public gross and recipient, not private shares or which payer qualified. By user ruling the diversion comes off the gross before any Homeworld occupation split, but that split-recipient composition remains fenced from Leader Skills. By user ruling a Guild Nexus Betrayal receipt still carries Bureaucrat: the holder receives the fee minus two, and the bounded classic Nexus plus Leader Skills Betrayal composition now connects that offer from the invoice’s own payment legs. By user ruling, Emperor gifts are not payments: no Bureaucrat prompt/use or threshold disclosure, and exact gift amounts remain private. Other payment-family/module interactions remain unfinished.',
'Smuggler records all existing positive battle-territory piles when plans reveal, capped in total by the selected leader’s unmodified strength. A surviving controller collects even when losing or no forces remain. A partial collection across several piles asks that owner for exact sector amounts; all/none and single-pile collection stay automatic. This allocation policy is provisional. Pending spice cannot fund the plan. Collection precedes Sandmaster addition; a drained pile receives none. Modified strength and other combined-module limits remain guarded. Older saved battles retain their recorded behavior.',
'In classic and supported E1/E2 native Tech composition, actual Smuggler prices and normal Banker qualifying fees remain distinct from phase-end Tech income. Tech piles cannot fund their triggering action; Banker front-shield grants still wait for actual Mentat. Winning physical Suk rescue and Sandmaster board spice precede original played-card cleanup and mandatory winner token reward. One token transfers automatically, several require a selection, and no token skips without an invented decline. A later Face Dancer retains the original winner’s cards, bounty and Tech reward, kills the matching trainer without another bounty and replaces only actual survivors from real sources. Native Tleilaxu-only free revival excludes Axlotl; CHOAM initial income is not Charity. Richese’s free companion does not change its marker price or create the concealed counters early.',
'Physical Fremen Ambassador relocation now offers optional Sandmaster destination collection before arrival reactions, without an invented route or ordinary movement use. Same-territory repositioning earns none; concealed relocation and other special movement still retain their own limits. Normal Banker income has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz skills follow their bounded contracts; shared-Duke assignment, combined Occupy skills and other combined modules remain unfinished. This prototype does not enable public module starts.',