Connect Bureaucrat payment redirection and saved continuation
Add an optional owner-controlled two-spice diversion from eligible single-payer ordinary auctions, Guild shipment income, Richese auction proceeds and actual bribes in the bounded base/CHOAM Leader Skills profile. Debit the payer once, credit the recipient once, retain bribes until Mentat Pause and restore the original response/auction/advisor callback. Decline preserves later phase opportunities; successful physical-card usage persists ownership changes. Keep qualifying allied funding and unfinished payment families explicitly guarded or outside the connected batch. Record source authority and the out-of-battle Karama composition and physical-card usage inferences; no public mode or publication gate opens. Connect readable owner controls and all four AI profiles using public payment/alliance information. Independent review found and fixed duplicate usage-event aliases and an overbroad guard on Bank-bound independent Karama shipments. Focused tests cover native/hidden/captured/dead eligibility, unchanged costs and card/force custody, normal and Richese auction callbacks, real Bene Gesserit advisor continuation, private views, JSON and production SQLite restore, stale/duplicate rejection and same-version competing writes. Validation: typecheck, lint, 4631 offline tests, production build and 43 HTTP tests passed with unchanged source. Four controlled-shuffle genuine-setup samples completed: base Basic 718 actions/20 restores, base Advanced 551/15, CHOAM Basic 681/19 and CHOAM Advanced 174/5. All had zero rejected candidates and preserved full physical Treachery, force and skill custody. None reached a Bureaucrat payment decision; these are regression-continuation samples, not natural full-game redirection acceptance. The separate conserved staged browser exercise allowed one five-spice bribe and redirected two from a second; the payer spent ten, the payee retained eight deferred spice, and one skill use was recorded. Pending and resolved states survived refresh with unchanged physical custody and private projection. After the reported outage, all 699 opening games and original seat/recovery records were preserved (733 after HTTP checks); database integrity and foreign keys passed. Reused the healthy server without restart, reset or recurring automation. Verified source tree: a30615deed306e06ab4c153b9a7eabdc92d646ed79474e348a3500d525bcb8c8
This commit is contained in:
1 parent
9f1f783be4
commit
d0dc7007d9
18 files changed
+1649
-49
No files matched your search
+9
-1
@@ -1438,7 +1438,7 @@ function policyActions(g: GameView): Action[] {
|
||||
if (g.decision) {
|
||||
if (g.decision.player !== me.id) return [];
|
||||
const d = g.decision;
|
||||
if (d.kind === 'leaderSkillVisibility' || d.kind === 'leaderSkillRevival' || d.kind === 'mentatQuestion') return [];
|
||||
if (d.kind === 'leaderSkillVisibility' || d.kind === 'leaderSkillRevival' || d.kind === 'mentatQuestion' || d.kind === 'bureaucratPayment') return [];
|
||||
if (d.kind === 'choamAudit')
|
||||
return [{ type: 'decision', event: d.event, audit: true }];
|
||||
if (d.kind === 'choamAuditPayment') {
|
||||
@@ -3659,6 +3659,14 @@ function standaloneGholaAction(g: GameView, ordinary: Action[]): Action | null {
|
||||
|
||||
/** Obligations apply across policy branches, including choosing to move first. */
|
||||
export function botActions(g: GameView): Action[] {
|
||||
const bureaucrat = g.bureaucrat?.pending;
|
||||
if (bureaucrat) {
|
||||
if (bureaucrat.owner !== g.me || g.decision?.kind !== 'bureaucratPayment' ||
|
||||
g.decision.player !== g.me || g.decision.event !== bureaucrat.event ||
|
||||
g.truthtrance || g.response || g.automaticContinuationPending) return [];
|
||||
const ally = g.players.find((player) => player.id === g.me)?.ally;
|
||||
return [{ type: 'decision', event: bureaucrat.event, redirect: bureaucrat.payee !== ally }];
|
||||
}
|
||||
const mentat = g.mentat?.pending;
|
||||
if (mentat) {
|
||||
if (mentat.player !== g.me || g.decision?.kind !== 'mentatQuestion' ||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe';
|
||||
export type BureaucratPaymentSource = {
|
||||
event: string;
|
||||
turn: number;
|
||||
phase: number;
|
||||
kind: BureaucratPaymentKind;
|
||||
payer: string;
|
||||
payee: string;
|
||||
amount: number;
|
||||
signature: string;
|
||||
};
|
||||
/** The one physical skill card's use survives death, redraw and controller changes. */
|
||||
export type BureaucratPaymentUse = {
|
||||
event: string;
|
||||
owner: string;
|
||||
turn: number;
|
||||
phase: number;
|
||||
signature: string;
|
||||
};
|
||||
export type BureaucratPaymentView = {
|
||||
pending: null | {
|
||||
event: string;
|
||||
owner: string;
|
||||
payer: string;
|
||||
payee: string;
|
||||
amount: number;
|
||||
kind: BureaucratPaymentKind;
|
||||
redirect: 2;
|
||||
};
|
||||
usedThisPhase: boolean;
|
||||
};
|
||||
export class BureaucratPaymentError extends Error {}
|
||||
export function bureaucratPaymentSignature(value: object): string {
|
||||
return JSON.stringify({ ...value, signature: undefined });
|
||||
}
|
||||
export function bureaucratPaymentModeSupported(game: {
|
||||
expansions: readonly string[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
(!game.expansions.length ||
|
||||
(game.expansions.length === 1 && game.expansions[0] === 'choam')) &&
|
||||
!game.homeworlds &&
|
||||
!game.nexusCards &&
|
||||
!game.discoveries &&
|
||||
!game.discoveryEnabled &&
|
||||
!game.strongholdCards &&
|
||||
!game.techTokens
|
||||
);
|
||||
}
|
||||
export function bureaucratUsed(
|
||||
used: readonly BureaucratPaymentUse[],
|
||||
turn: number,
|
||||
phase: number,
|
||||
): boolean {
|
||||
return used.some((use) => use.turn === turn && use.phase === phase);
|
||||
}
|
||||
/** Caller supplies only a living face-up native trainer and actual player payees. */
|
||||
export function quoteBureaucratPayment(input: {
|
||||
owner: string | null;
|
||||
payer: string;
|
||||
payee: string;
|
||||
amount: number;
|
||||
turn: number;
|
||||
phase: number;
|
||||
used: readonly BureaucratPaymentUse[];
|
||||
}): { redirect: 2; income: number } | null {
|
||||
if (
|
||||
!Number.isSafeInteger(input.amount) ||
|
||||
input.amount < 0 ||
|
||||
!Number.isSafeInteger(input.turn) ||
|
||||
input.turn < 1 ||
|
||||
!Number.isSafeInteger(input.phase) ||
|
||||
input.phase < 0 ||
|
||||
input.phase > 8 ||
|
||||
!input.payer ||
|
||||
!input.payee
|
||||
)
|
||||
throw new BureaucratPaymentError(
|
||||
'Bureaucrat needs a valid actual player payment.',
|
||||
);
|
||||
return input.owner &&
|
||||
input.owner !== input.payer &&
|
||||
input.owner !== input.payee &&
|
||||
input.payer !== input.payee &&
|
||||
input.amount >= 5 &&
|
||||
!bureaucratUsed(input.used, input.turn, input.phase)
|
||||
? { redirect: 2, income: input.amount - 2 }
|
||||
: null;
|
||||
}
|
||||
+188
-12
@@ -1,5 +1,6 @@
|
||||
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
|
||||
import { leaderSkillCard, type LeaderSkillId } from './leader-skill-cards';
|
||||
import { bureaucratPaymentModeSupported, bureaucratPaymentSignature, bureaucratUsed, quoteBureaucratPayment, type BureaucratPaymentSource, type BureaucratPaymentUse, type BureaucratPaymentView } from './bureaucrat-payment';
|
||||
import { MENTAT_EMPTY_HAND, MentatQuestionError, mentatQuestionModeSupported, mentatWeaponNames, mentatHand, quoteMentatReveal, mentatSignature, type MentatQuestionReceipt, type MentatObservation, type MentatView } from './mentat-question';
|
||||
import {
|
||||
canUsePlanetologistBattleSpecial,
|
||||
@@ -746,6 +747,7 @@ export type Decision =
|
||||
| { kind: 'sukRescue'; player: string; event: string; territory: string; mode: 'normal' | 'skilled'; options: SukRescueOption[] }
|
||||
| { kind: 'rihani'; player: string; event: string; stage: 'offer' | 'return' }
|
||||
| { kind: 'fullPlanOffer'; player: string }
|
||||
| {kind:'bureaucratPayment';player:string;event:string}
|
||||
| { kind: 'mentatQuestion'; player: string; event: string; owner: string; target: string; stage: 'name' | 'reveal'; weapon: string | null }
|
||||
| { kind: 'fullPlanRead'; player: string; target: string }
|
||||
| { kind: 'homeworldShipmentGuild'; player: string; shipper: string; destination: string; amount: number; event: string }
|
||||
@@ -823,6 +825,8 @@ export type Decision =
|
||||
}
|
||||
| { kind: 'battleCards'; player: string; territory: string; cards: string[] };
|
||||
export type ResponseWindow = {
|
||||
bureaucratPayment?: BureaucratPaymentSource;
|
||||
bureaucratPaymentEvent?: string;
|
||||
noFieldSkillProof?: string;
|
||||
/** Original optional collection binds a pending ground movement, including saved responses. */
|
||||
sandmasterProof?: string;
|
||||
@@ -1149,6 +1153,8 @@ export type Game = {
|
||||
};
|
||||
ixRicheseTechnologyEvent?: string;
|
||||
currentAuctionSale?: {
|
||||
bureaucratPayment?: BureaucratPaymentSource;
|
||||
bureaucratPaymentEvent?: string;
|
||||
winner: string;
|
||||
amount: number;
|
||||
free: boolean;
|
||||
@@ -1467,6 +1473,14 @@ export type Game = {
|
||||
/** Absent on legacy rooms whose starting cards were already dealt. */
|
||||
setupStage?: 'prediction' | 'skillTreachery' | 'leaderSkills' | 'traitors' | 'forces';
|
||||
leaderSkills?: LeaderSkillsState;
|
||||
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'};
|
||||
frame:string;signature:string;
|
||||
}};
|
||||
bureaucratPaymentEvent?: string;
|
||||
bureaucratUseEvents?: string[];
|
||||
/** Private development opt-in pending the uniform Mentat response UX decision. */
|
||||
mentatQuestionPreview?: true;
|
||||
mentatHistory?: MentatObservation[];
|
||||
@@ -1985,7 +1999,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.pendingRichesePurchaseIncome?.resume, g.summonedWorm?.resume,g.bureaucratPayments?.pending?.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;
|
||||
@@ -4977,6 +4991,7 @@ function leaderSkillAssignmentUnavailable(
|
||||
: null;
|
||||
}
|
||||
function leaderSkillsIntegrity(g: Game) {
|
||||
bureaucratPaymentIntegrity(g);
|
||||
mentatQuestionIntegrity(g);
|
||||
diplomatDefenseIntegrity(g);
|
||||
const noFieldShipment = leaderSkillNoFieldIntegrity(g);
|
||||
@@ -5069,6 +5084,138 @@ function nativeLeaderSkill(g: Game, owner: string) {
|
||||
(a) => a.owner === owner && leaderSkillController(g, a) === owner,
|
||||
);
|
||||
}
|
||||
function nativeBureaucrat(g: Game) {
|
||||
return g.leaderSkills?.assignments.find(a => a.skill === 'bureaucrat' &&
|
||||
getPlayer(g,a.owner).leaders.some(l => l.id === a.leader && !l.dead && !l.capturedBy && !l.gholaBy) &&
|
||||
!g.battle?.leaderSkillHidden?.[a.owner]);
|
||||
}
|
||||
function bureaucratPaymentQuote(g: Game,source: Pick<BureaucratPaymentSource,'payer'|'payee'|'amount'>) {
|
||||
const owner = nativeBureaucrat(g)?.owner ?? null;
|
||||
return quoteBureaucratPayment({...source,owner,turn:g.turn,phase:g.phase,used:g.bureaucratPayments?.used ?? []});
|
||||
}
|
||||
function checkBureaucratContributors(g: Game,payee:string|undefined,shares:{payer:string;amount:number}[],privateFunding=false) {
|
||||
if (!g.leaderSkills || !payee) return;
|
||||
const paid = shares.filter(share => share.amount > 0);
|
||||
const owner = nativeBureaucrat(g)?.owner;
|
||||
if (!owner || owner === payee || bureaucratUsed(g.bureaucratPayments?.used ?? [],g.turn,g.phase) ||
|
||||
paid.reduce((total,share) => total+share.amount,0) < 5 || !paid.some(share => share.payer !== owner && share.payer !== payee)) return;
|
||||
requireRule(bureaucratPaymentModeSupported(g),'Bureaucrat with other optional modules is still being integrated.');
|
||||
requireRule(paid.length === 1 && !privateFunding,'Bureaucrat on allied funding or multiple payer contributions is awaiting private payment-split integration.');
|
||||
}
|
||||
function stampBureaucratPayment(g: Game,kind:BureaucratPaymentSource['kind'],payer:string,payee:string,amount:number):
|
||||
{bureaucratPayment?:BureaucratPaymentSource;bureaucratPaymentEvent?:string} {
|
||||
if (!g.leaderSkills || !bureaucratPaymentModeSupported(g) || amount < 5 || payer === payee) return {};
|
||||
const source:BureaucratPaymentSource = {event:crypto.randomUUID(),turn:g.turn,phase:g.phase,kind,payer,payee,amount,signature:''};
|
||||
source.signature = bureaucratPaymentSignature(source);
|
||||
return {bureaucratPayment:source,bureaucratPaymentEvent:source.event};
|
||||
}
|
||||
function bureaucratPaymentFrame(g: Game): string {
|
||||
return JSON.stringify({turn:g.turn,phase:g.phase,active:g.active,order:g.order,
|
||||
players:g.players.map(p => ({id:p.id,spice:p.spice,bribes:p.bribes,hand:p.hand,leaders:p.leaders,forces:p.forces,
|
||||
elites:p.elites,reserves:p.reserves,tanks:p.tanks,ally:p.ally,shipped:p.shipped,moved:p.moved})),
|
||||
aid:g.aid,auction:g.auction,sale:g.currentAuctionSale,richeseAuction:g.richeseAuction,richeseBidding:g.richeseBidding,
|
||||
richeseFunding:g.richeseFunding,richeseCache:g.richeseCache,pendingIxAlly:g.pendingIxAlly,
|
||||
battle:g.battle,leaderSkills:g.leaderSkills,phaseOpening:g.phaseOpening});
|
||||
}
|
||||
function validateBureaucratSource(g: Game,source:BureaucratPaymentSource) {
|
||||
requireRule(source && typeof source === 'object' && typeof source.event === 'string' && source.event.length > 0 &&
|
||||
source.turn === g.turn && source.phase === g.phase && ['auction','shipment','bribe'].includes(source.kind) &&
|
||||
g.players.some(p => p.id === source.payer) && g.players.some(p => p.id === source.payee) && source.payer !== source.payee &&
|
||||
Number.isSafeInteger(source.amount) && source.amount >= 5 && source.signature === bureaucratPaymentSignature(source),
|
||||
'The saved Bureaucrat payment changed its actual payer, payee, amount or event.');
|
||||
}
|
||||
function bureaucratPaymentIntegrity(g: Game) {
|
||||
const state = g.bureaucratPayments,events = g.bureaucratUseEvents ?? [];
|
||||
requireRule(!state || Array.isArray(state.used),'The saved Bureaucrat usage ledger is invalid.');
|
||||
requireRule(Array.isArray(events) && events.length === (state?.used.length ?? 0) && new Set(events).size === events.length &&
|
||||
new Set((state?.used ?? []).map(use => use?.event)).size === (state?.used.length ?? 0),
|
||||
'The physical Bureaucrat card lost its independent usage events.');
|
||||
const phases = new Set<string>();
|
||||
for (const use of state?.used ?? []) {
|
||||
const phase = `${use.turn}:${use.phase}`;
|
||||
requireRule(use && typeof use.event === 'string' && events.includes(use.event) && !phases.has(phase) &&
|
||||
Number.isSafeInteger(use.turn) && use.turn >= 1 && use.turn <= g.turn && Number.isSafeInteger(use.phase) && use.phase >= 0 && use.phase <= 8 &&
|
||||
(use.turn < g.turn || use.phase <= g.phase) && g.players.some(p => p.id === use.owner) && use.signature === bureaucratPaymentSignature(use),
|
||||
'The physical Bureaucrat skill cannot gain a second use in the same phase.');
|
||||
phases.add(phase);
|
||||
}
|
||||
const pending = state?.pending;
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const contexts = [g,g.pendingExchange,g.pendingNullentropy?.resume,g.pendingRicheseGift?.resume,g.pendingRichesePurchaseIncome?.resume,
|
||||
g.summonedWorm?.resume,continuation && 'resume' in continuation ? continuation.resume : null,pending?.resume];
|
||||
const responses = contexts.flatMap(context => context?.response ? [context.response] : []);
|
||||
if (g.pendingKarama?.use.kind === 'cancel') responses.push(g.pendingKarama.use.response);
|
||||
for (const carrier of [g.currentAuctionSale,...responses].filter((value): value is NonNullable<typeof value> => !!value)) {
|
||||
const source = carrier.bureaucratPayment;
|
||||
if (!source) {requireRule(!carrier.bureaucratPaymentEvent,'The saved payment lost its original Bureaucrat source.');continue;}
|
||||
requireRule(g.leaderSkills && bureaucratPaymentModeSupported(g) && carrier.bureaucratPaymentEvent === source.event,
|
||||
'The saved Bureaucrat payment lost its module or independent event.');
|
||||
validateBureaucratSource(g,source);
|
||||
if ('winner' in carrier) requireRule(source.kind === 'auction' && !carrier.free && source.amount === carrier.amount &&
|
||||
(source.payer === carrier.winner || getPlayer(g,carrier.winner).ally === source.payer) &&
|
||||
source.payee === (carrier.origin !== 'normal' && carrier.seller !== carrier.winner ? carrier.seller : byFaction(g,'emperor')?.id),
|
||||
'The Bureaucrat source no longer matches its paid auction sale.');
|
||||
else requireRule(source.payee === carrier.owner &&
|
||||
(carrier.kind === 'guildIncome' && source.kind === 'shipment' && source.amount === carrier.amount ||
|
||||
carrier.kind === 'emperorIncome' && source.kind === 'auction' && g.currentAuctionSale?.bureaucratPaymentEvent === source.event),
|
||||
'The Bureaucrat source no longer matches its original native income.');
|
||||
}
|
||||
const decisions = homeworldSavedDecisions(g).filter(d => d.kind === 'bureaucratPayment');
|
||||
if (!pending) {requireRule(!g.bureaucratPaymentEvent && !decisions.length,'The saved Bureaucrat decision lost its original payment.');return;}
|
||||
validateBureaucratSource(g,pending.source);
|
||||
const assignment = nativeBureaucrat(g);
|
||||
requireRule(g.status === 'playing' && g.leaderSkills && bureaucratPaymentModeSupported(g) && !g.response &&
|
||||
g.bureaucratPaymentEvent === pending.source.event && pending.signature === bureaucratPaymentSignature(pending) &&
|
||||
pending.frame === bureaucratPaymentFrame(g) && assignment?.owner === pending.owner && assignment.leader === pending.leader &&
|
||||
bureaucratPaymentQuote(g,pending.source) && decisions.length === 1 && decisions[0] === g.decision &&
|
||||
decisions[0].player === pending.owner && decisions[0].event === pending.source.event,
|
||||
'The pending Bureaucrat payment changed its trainer, resources, callback or unique decision.');
|
||||
if (pending.continuation.kind === 'response') requireRule(pending.resume.response?.bureaucratPaymentEvent === pending.source.event,
|
||||
'The Bureaucrat income lost its uncanceled response continuation.');
|
||||
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 requireRule(pending.continuation.kind === 'bribe' && pending.source.kind === 'bribe',
|
||||
'The Bureaucrat payment has an invalid continuation.');
|
||||
}
|
||||
function offerBureaucratPayment(g: Game,source:BureaucratPaymentSource|undefined,
|
||||
continuation:NonNullable<NonNullable<Game['bureaucratPayments']>['pending']>['continuation']): boolean {
|
||||
if (!source || !bureaucratPaymentQuote(g,source)) return false;
|
||||
requireRule(!g.bureaucratPayments?.pending,'Finish the current Bureaucrat payment first.');
|
||||
validateBureaucratSource(g,source);
|
||||
const assignment = nativeBureaucrat(g)!;
|
||||
const pending = {source,owner:assignment.owner,leader:assignment.leader,
|
||||
resume:{response:g.response,decision:g.decision},continuation,frame:bureaucratPaymentFrame(g),signature:''};
|
||||
pending.signature = bureaucratPaymentSignature(pending);
|
||||
(g.bureaucratPayments ??= {used:[]}).pending = pending;
|
||||
g.bureaucratPaymentEvent = source.event;
|
||||
g.response = null;
|
||||
g.decision = {kind:'bureaucratPayment',player:assignment.owner,event:source.event};
|
||||
return true;
|
||||
}
|
||||
function projectedBureaucrat(g: Game): BureaucratPaymentView {
|
||||
const pending = g.bureaucratPayments?.pending;
|
||||
return {pending:pending ? {event:pending.source.event,owner:pending.owner,payer:pending.source.payer,payee:pending.source.payee,
|
||||
amount:pending.source.amount,kind:pending.source.kind,redirect:2} : null,
|
||||
usedThisPhase:bureaucratUsed(g.bureaucratPayments?.used ?? [],g.turn,g.phase)};
|
||||
}
|
||||
function actBureaucratPayment(g: Game,action:Action) {
|
||||
const pending = g.bureaucratPayments!.pending!;
|
||||
requireRule(action.event === pending.source.event && typeof action.redirect === 'boolean' &&
|
||||
Object.keys(action).every(key => ['type','event','redirect'].includes(key)), 'Choose whether to redirect this exact Bureaucrat payment.');
|
||||
const redirected = action.redirect ? 2 : 0;
|
||||
if (redirected) {
|
||||
const use:BureaucratPaymentUse = {event:pending.source.event,owner:pending.owner,turn:g.turn,phase:g.phase,signature:''};
|
||||
use.signature = bureaucratPaymentSignature(use);
|
||||
g.bureaucratPayments!.used.push(use);(g.bureaucratUseEvents ??= []).push(use.event);
|
||||
log(g,`${getPlayer(g,pending.owner).name} used Bureaucrat to redirect 2 spice of the ${pending.source.amount}-spice payment to the Bank.`);
|
||||
}
|
||||
delete g.bureaucratPayments!.pending;delete g.bureaucratPaymentEvent;
|
||||
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 getPlayer(g,pending.source.payee).bribes += pending.source.amount-redirected;
|
||||
}
|
||||
function requireLeaderSkillRevival(g: Game, owner: string) {
|
||||
requireRule(
|
||||
!g.leaderSkills?.assignments.some(
|
||||
@@ -6580,6 +6727,7 @@ function settleRicheseLot(g: Game) {
|
||||
free: false,
|
||||
origin: lot.source,
|
||||
seller: owner.id,
|
||||
...stampAuctionBureaucrat(g,winner,quote.amount,quote.allyPayment,owner.id),
|
||||
};
|
||||
log(
|
||||
g,
|
||||
@@ -7286,6 +7434,14 @@ function payWithAlly(g: Game, p: Player, cost: number, allyPayment: number) {
|
||||
p.spice -= cost - allyPayment;
|
||||
if (credit) credit.amount -= allyPayment;
|
||||
}
|
||||
function auctionBureaucratParties(g:Game,p:Player,amount:number,allyPayment:number,seller?:string) {
|
||||
return {payee:seller && seller !== p.id ? seller : byFaction(g,'emperor')?.id,
|
||||
shares:[{payer:p.id,amount:amount-allyPayment},...(allyPayment ? [{payer:p.ally!,amount:allyPayment}] : [])].filter(share => share.amount > 0)};
|
||||
}
|
||||
function stampAuctionBureaucrat(g:Game,p:Player,amount:number,allyPayment:number,seller?:string) {
|
||||
const {payee,shares} = auctionBureaucratParties(g,p,amount,allyPayment,seller);
|
||||
return payee && !allyPayment && shares.length === 1 ? stampBureaucratPayment(g,'auction',shares[0].payer,payee,amount) : {};
|
||||
}
|
||||
function auctionNext(g: Game) {
|
||||
const a = g.auction!;
|
||||
const eligible = g.order.filter((id) => {
|
||||
@@ -7323,6 +7479,7 @@ function settleAuction(g: Game, free = false, automatic = false) {
|
||||
free,
|
||||
origin: 'normal',
|
||||
seller: null,
|
||||
...(!free ? stampAuctionBureaucrat(g,winner,a.bid,a.allyPayment ?? 0) : {}),
|
||||
};
|
||||
log(
|
||||
g,
|
||||
@@ -7393,21 +7550,24 @@ function validateAuctionContinuationCancellation(
|
||||
);
|
||||
return null;
|
||||
}
|
||||
function commitAuctionContinuation(g: Game, quote: AuctionContinuationQuote) {
|
||||
function commitAuctionContinuation(g: Game, quote: AuctionContinuationQuote,bureaucratDiversion?:number) {
|
||||
if (bureaucratDiversion === undefined && quote.steps.some(step => step.kind === 'sellerCredit') &&
|
||||
offerBureaucratPayment(g,g.currentAuctionSale?.bureaucratPayment,{kind:'auction',quote})) return;
|
||||
for (const step of quote.steps) {
|
||||
if (step.kind === 'clearIxAlly') g.pendingIxAlly = null;
|
||||
else {
|
||||
const seller = getPlayer(g, step.player);
|
||||
seller.spice = step.balance;
|
||||
seller.spice = step.balance-(bureaucratDiversion ?? 0);
|
||||
log(
|
||||
g,
|
||||
`${seller.name} collected ${step.amount} spice from the ${quote.sale.origin === 'cache' ? 'Richese cache' : 'Black Market'} sale.`,
|
||||
`${seller.name} collected ${step.amount-(bureaucratDiversion ?? 0)} spice from the ${quote.sale.origin === 'cache' ? 'Richese cache' : 'Black Market'} sale.`,
|
||||
{ faction: 'richese', name: 'Auction income' },
|
||||
);
|
||||
}
|
||||
}
|
||||
const next = quote.next;
|
||||
if (next.kind === 'response') g.response = next.response;
|
||||
if (next.kind === 'response') g.response = {...next.response,...(next.response.kind === 'emperorIncome' && g.currentAuctionSale?.bureaucratPayment ?
|
||||
{bureaucratPayment:g.currentAuctionSale.bureaucratPayment,bureaucratPaymentEvent:g.currentAuctionSale.bureaucratPaymentEvent} : {})};
|
||||
else if (next.kind === 'richeseEnd') finishRicheseLot(g);
|
||||
else {
|
||||
g.currentAuctionSale = null;
|
||||
@@ -10378,13 +10538,16 @@ function checkShipmentIncomeRounding(g: Game, p: Player, cost: number, allyPayme
|
||||
const guild = byFaction(g, 'guild');
|
||||
if (!guild) return;
|
||||
const amounts = shipmentIncomeContributions(g, p, cost, allyPayment);
|
||||
checkBureaucratContributors(g,g.karamaShipping?.player === p.id ? undefined : guild.id,[{payer:p.id,amount:p.id === guild.id ? 0 : cost-allyPayment},
|
||||
...(allyPayment ? [{payer:p.ally!,amount:p.ally === guild.id ? 0 : allyPayment}] : [])],allyPayment > 0);
|
||||
validateGuildPaymentRounding(g, guild.id, amounts.reduce((sum, amount) => sum + amount, 0), amounts);
|
||||
}
|
||||
function guildPaymentResponse(g: Game, owner: string, amounts: number[]): ResponseWindow {
|
||||
function guildPaymentResponse(g: Game, owner: string, amounts: number[],payer?:string): ResponseWindow {
|
||||
const contributions = amounts.filter(amount => amount > 0);
|
||||
const amount = contributions.reduce((sum, value) => sum + value, 0);
|
||||
validateGuildPaymentRounding(g, owner, amount, contributions);
|
||||
return { kind: 'guildIncome', owner, amount, passed: [],
|
||||
...(contributions.length === 1 && payer ? stampBureaucratPayment(g,'shipment',payer,owner,amount) : {}),
|
||||
...(g.homeworlds?.custody ? { guildContributions: contributions,
|
||||
guildPaymentProof: guildPaymentSignature(g, owner, amount, contributions) } : {}) };
|
||||
}
|
||||
@@ -16271,7 +16434,7 @@ function currentPlacementCancellationQuote(g: Game, response: ResponseWindow) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function finishResponse(g: Game, canceled: boolean) {
|
||||
function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number) {
|
||||
karamaConversionIntegrity(g);
|
||||
currentFactionPayment(g);
|
||||
const response = g.response!;
|
||||
@@ -16317,6 +16480,7 @@ function finishResponse(g: Game, canceled: boolean) {
|
||||
const allianceCancellation = canceled
|
||||
? moritaniAllianceCancellationQuote(g, response)
|
||||
: null;
|
||||
if (!canceled && bureaucratDiversion === undefined && offerBureaucratPayment(g,response.bureaucratPayment,{kind:'response'})) return;
|
||||
g.response = null;
|
||||
if (response.kind === 'nexusGuildCunning') { settleGuildCunning(g,response,canceled); return; }
|
||||
if (response.kind === 'nexusAdvisorFlip') {
|
||||
@@ -17200,7 +17364,7 @@ function finishResponse(g: Game, canceled: boolean) {
|
||||
);
|
||||
}
|
||||
} else if (response.kind === 'guildIncome') {
|
||||
const payment = !canceled ? creditFactionPayment(g, response.owner, 'shipment', response.amount!) : null;
|
||||
const payment = !canceled ? creditFactionPayment(g, response.owner, 'shipment', response.amount!-(bureaucratDiversion ?? 0)) : null;
|
||||
log(
|
||||
g,
|
||||
canceled
|
||||
@@ -17251,7 +17415,7 @@ function finishResponse(g: Game, canceled: boolean) {
|
||||
if (!canceled) {
|
||||
const owner = getPlayer(g, response.owner);
|
||||
const amount = g.currentAuctionSale?.amount ?? g.auction!.bid;
|
||||
const payment = creditFactionPayment(g, owner.id, 'treachery', amount);
|
||||
const payment = creditFactionPayment(g, owner.id, 'treachery', amount-(bureaucratDiversion ?? 0));
|
||||
log(
|
||||
g,
|
||||
`${owner.name} received ${payment.income} spice from another faction’s ${amount}-spice paid auction bid.${payment.bank ? ` Low-population Kaitain leaves ${payment.bank} spice in the bank.` : ''}`,
|
||||
@@ -17912,7 +18076,7 @@ function commitShipment(g: Game, shipment: PendingShipment) {
|
||||
bankOnly: g.karamaShipping?.player === p.id,
|
||||
});
|
||||
if (guild && guildPayment > 0)
|
||||
g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment));
|
||||
g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment),allyPayment ? undefined : p.id);
|
||||
g.karamaShipping = null;
|
||||
log(
|
||||
g,
|
||||
@@ -19792,6 +19956,7 @@ function applyActionInner(
|
||||
requireRule(g.status !== 'finished', 'This game has ended.');
|
||||
const t = action.type;
|
||||
if (t === 'advanceBots') return g;
|
||||
requireRule(!g.bureaucratPayments?.pending || t === 'decision','Resolve the pending Bureaucrat payment before another action.');
|
||||
requireRule(!(g.battle?.mentatQuestion?.stage === 'name' || g.battle?.mentatQuestion?.stage === 'reveal') || t === 'decision',
|
||||
'Resolve the current Mentat question before another action.');
|
||||
if (t === 'nexusCardChoice') { decideNexusCard(g, p, action); return g; }
|
||||
@@ -20113,6 +20278,7 @@ function applyActionInner(
|
||||
actMentatQuestion(g, decision, action);
|
||||
return g;
|
||||
}
|
||||
if (decision.kind === 'bureaucratPayment') {actBureaucratPayment(g,action);return g;}
|
||||
if (decision.kind === 'ixRicheseTechnology') {
|
||||
const pending = g.pendingIxRicheseTechnology!;
|
||||
requireRule(action.event === pending.event && action.decline === true &&
|
||||
@@ -21596,12 +21762,14 @@ function applyActionInner(
|
||||
'Bribes are between different, non-allied factions.',
|
||||
);
|
||||
const n = integer(action.amount, 1, uncommittedSpice(g, p), 'Spice');
|
||||
checkBureaucratContributors(g,target.id,[{payer:p.id,amount:n}]);
|
||||
p.spice -= n;
|
||||
target.bribes += n;
|
||||
log(
|
||||
g,
|
||||
`${p.name} promised ${n} spice to ${target.name}; collect it at the Mentat pause.`,
|
||||
);
|
||||
const source = stampBureaucratPayment(g,'bribe',p.id,target.id,n).bureaucratPayment;
|
||||
if (!offerBureaucratPayment(g,source,{kind:'bribe'})) target.bribes += n;
|
||||
return g;
|
||||
}
|
||||
if (t === 'stormDial') {
|
||||
@@ -21735,6 +21903,10 @@ function applyActionInner(
|
||||
);
|
||||
if (amount !== null)
|
||||
validateRicheseFunding(amount, allyPayment, ownAvailable, allyAvailable);
|
||||
if (amount !== null) {
|
||||
const parties = auctionBureaucratParties(g,p,amount,allyPayment,lot.owner);
|
||||
checkBureaucratContributors(g,parties.payee,parties.shares,allyPayment > 0);
|
||||
}
|
||||
g.richeseAuction = submitRicheseBid(
|
||||
lot,
|
||||
{ event: stringField(action.event), actor: p.id, amount },
|
||||
@@ -21780,6 +21952,8 @@ function applyActionInner(
|
||||
a.bid <= p.spice + (aidFor(g, p)?.amount ?? 0)
|
||||
? contribution(g, p, a.bid, action.allyPayment)
|
||||
: 0;
|
||||
const parties = auctionBureaucratParties(g,p,a.bid,a.allyPayment);
|
||||
checkBureaucratContributors(g,parties.payee,parties.shares,a.allyPayment > 0);
|
||||
a.bidder = id;
|
||||
a.passed = [];
|
||||
} else if (!a.passed.includes(id)) a.passed.push(id);
|
||||
@@ -22471,7 +22645,7 @@ function applyActionInner(
|
||||
bankOnly: g.karamaShipping?.player === id,
|
||||
});
|
||||
if (guild && guildPayment > 0)
|
||||
g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment));
|
||||
g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment),allyPayment ? undefined : p.id);
|
||||
if (fromReserves) {
|
||||
p.reserves -= n;
|
||||
if (p.elites) p.elites.reserves -= elite;
|
||||
@@ -23262,6 +23436,7 @@ export function viewGame(state: Game, id: string) {
|
||||
setupStage: g.status === 'setup' ? (g.setupStage ?? null) : null,
|
||||
setupPending: setupPending(g),
|
||||
leaderSkills: projectedLeaderSkills(g, id),
|
||||
bureaucrat: projectedBureaucrat(g),
|
||||
mentat: projectedMentat(g, id),
|
||||
rihani: projectedRihani(g, id),
|
||||
advanced: g.advanced,
|
||||
@@ -23715,6 +23890,7 @@ export function viewGame(state: Game, id: string) {
|
||||
response: g.response
|
||||
? {
|
||||
...g.response,
|
||||
...(g.response.bureaucratPayment ? {bureaucratPayment:undefined,bureaucratPaymentEvent:undefined} : {}),
|
||||
...(g.response.guildContributions ? { guildContributions: undefined } : {}),
|
||||
...(g.response.guildPaymentProof ? { guildPaymentProof: undefined } : {}),
|
||||
...(g.response.sandmasterProof ? { sandmasterProof: undefined } : {}),
|
||||
|
||||
+8
-7
@@ -3563,7 +3563,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping, Sandmaster route collection sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
|
||||
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring and Bureaucrat payment redirection, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping, Sandmaster route collection sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
|
||||
steps: [
|
||||
'With this optional module, starting Treachery Cards are dealt after prediction and before skill assignment. Every faction privately receives two skills, keeps one, returns the other to the shuffled deck, and assigns its chosen card to an eligible leader. Skill and leader become public before traitor selection.',
|
||||
'Before Battle Plans, leave the skilled leader face up to keep its normal effect, or move it behind the shield for possible use. A face-up leader cannot be selected for the plan. Concealing it while choosing another leader gives neither effect. A leader that must be used because no alternative remains moves behind the shield automatically.',
|
||||
@@ -3580,16 +3580,17 @@ 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 remains unfinished.',
|
||||
'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. Lower retreat and modified defenses remain unfinished.',
|
||||
'Mentat has a private developer preview for naming a specific weapon before posture and faction powers. The target privately shows the named held card or chooses another held card. It stays in hand and need not enter a Battle Plan. Every answer uses the same private response step; ordinary activation awaits the user’s timing-design decision. Empty hands skip without an answer. Private history records an observation, not a later hand guarantee.',
|
||||
'Bureaucrat payments, special Sandmaster relocation and Smuggler battle collection, Spice Banker normal income and Diplomat retreat remain missing. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
|
||||
'Bureaucrat may redirect two spice from a payment of at least five between two other players, 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 the remaining share still unavailable until Mentat Pause. Ordinary auctions, Guild income and Richese sales have connected payment paths; split funding, other payment families and combined modules remain unfinished.',
|
||||
'Special Sandmaster relocation and Smuggler battle collection, Spice Banker normal income and Diplomat retreat remain missing. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
|
||||
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
|
||||
],
|
||||
related: ['setup','choam-modules','implementation-checklist'],
|
||||
checklist: [
|
||||
{ area:'Implementation',status:'Partial',detail:'Exact fourteen-card custody, source-ordered setup, public capture, concealment, death and own revival connect five role bonuses, Planetologist movement/Special substitution and Suk Graduate casualty rescue. Rihani inspection/exchange and lower Mentat/Bureaucrat/Sandmaster effects are connected. Smuggler normal reserve shipping, the owned Richese No-Field companion and optional Sandmaster ground-route collection are connected. A separately opted-in Mentat preview connects private pre-plan questions and historical observations while ordinary activation awaits the uniform-response decision. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; normal income, other remaining bands, Diplomat retreat and combined modules remain missing.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/mentat-question.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/spice-banker.ts','game/sandmaster-movement.ts','game/diplomat-defense.ts','game/engine.ts'] },
|
||||
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes and per-territory collection choices. Diplomat offers a named committed Worthless choice or decline after public reveal. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/diplomat-defense.tsx','components/game-table.tsx'] },
|
||||
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments, Sandmaster route collection funded Banker commitments and optional Diplomat copying from public revealed defenses. This is a legal prototype path; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
|
||||
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody; replacement entitlement and other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/DIPLOMAT_DEFENSE.md'] },
|
||||
{ area:'Verification',status:'Partial',detail:'Focused inventory, scoring, Voice, capture, saved private setup/revival Planetologist movement/cancellation and Suk Graduate casualty/SQLite checks, plus Rihani privacy/concurrent recovery battle-effect regressions and Smuggler quote/decision/legacy recovery and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. Diplomat checks cover self-protection, mandatory discard, public-choice privacy, concurrent saved decisions and legacy continuation. Sample games and browser continuation are recorded separately; full module acceptance remains open.',evidence:['tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-skills-recovery.test.ts','tests/leader-skills-controls.test.ts','tests/suk-graduate.test.ts','tests/suk-graduate-recovery.test.ts','tests/leader-battle-effects.test.ts','tests/leader-battle-effects-recovery.test.ts','tests/leader-battle-effects-controls.test.tsx','tests/mentat-question.test.ts','tests/mentat-question-bots.test.ts','tests/mentat-question-controls.test.tsx','tests/mentat-question-recovery.test.ts','tests/smuggler-shipment.test.ts','tests/smuggler-shipment-controls.test.tsx','tests/smuggler-shipment-recovery.test.ts','tests/smuggler-no-field.test.ts','tests/smuggler-no-field-controls.test.tsx','tests/smuggler-no-field-recovery.test.ts','tests/spice-banker.test.ts','tests/spice-banker-recovery.test.ts','tests/spice-banker-controls.test.tsx','tests/sandmaster-movement.test.ts','tests/sandmaster-movement-recovery.test.ts','tests/sandmaster-movement-controls.test.tsx','tests/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts'] },
|
||||
{ area:'Implementation',status:'Partial',detail:'Exact fourteen-card custody, source-ordered setup, public capture, concealment, death and own revival connect five role bonuses, Planetologist movement/Special substitution and Suk Graduate casualty rescue. Rihani inspection/exchange and lower Mentat/Bureaucrat/Sandmaster effects are connected. Smuggler normal reserve shipping, the owned Richese No-Field companion and optional Sandmaster ground-route collection are connected. A separately opted-in Mentat preview connects private pre-plan questions and historical observations while ordinary activation awaits the uniform-response decision. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; Spice Banker normal income, other remaining bands, Diplomat retreat and combined modules remain missing. Bureaucrat redirects supported third-party auction, shipment and bribe payments with exact saved once-per-phase use.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/mentat-question.ts','game/bureaucrat-payment.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/spice-banker.ts','game/sandmaster-movement.ts','game/diplomat-defense.ts','game/engine.ts'] },
|
||||
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes and per-territory collection choices. Diplomat offers a named committed Worthless choice or decline after public reveal. Bureaucrat offers redirect or full payment with explicit recipient amounts and deferred bribe custody. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/bureaucrat-payment.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/diplomat-defense.tsx','components/game-table.tsx'] },
|
||||
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments, Sandmaster route collection funded Banker commitments and optional Diplomat copying from public revealed defenses. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
|
||||
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody; replacement entitlement and other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/BUREAUCRAT_PAYMENTS.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/DIPLOMAT_DEFENSE.md'] },
|
||||
{ area:'Verification',status:'Partial',detail:'Focused inventory, scoring, Voice, capture, saved private setup/revival Planetologist movement/cancellation and Suk Graduate casualty/SQLite checks, plus Rihani privacy/concurrent recovery battle-effect regressions and Smuggler quote/decision/legacy recovery and Banker funding, death, traitor, privacy and concurrent settlement checks cover this boundary. Diplomat checks cover self-protection, mandatory discard, public-choice privacy, concurrent saved decisions and legacy continuation. Bureaucrat checks cover once-per-phase use, private source receipts and resumed payment custody. Sample games and browser continuation are recorded separately; full module acceptance remains open.',evidence:['tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-skills-recovery.test.ts','tests/leader-skills-controls.test.ts','tests/suk-graduate.test.ts','tests/suk-graduate-recovery.test.ts','tests/leader-battle-effects.test.ts','tests/leader-battle-effects-recovery.test.ts','tests/leader-battle-effects-controls.test.tsx','tests/mentat-question.test.ts','tests/mentat-question-bots.test.ts','tests/mentat-question-controls.test.tsx','tests/mentat-question-recovery.test.ts','tests/bureaucrat-payment.test.ts','tests/bureaucrat-payment-controls.test.tsx','tests/bureaucrat-payment-bots.test.ts','tests/bureaucrat-payment-recovery.test.ts','tests/smuggler-shipment.test.ts','tests/smuggler-shipment-controls.test.tsx','tests/smuggler-shipment-recovery.test.ts','tests/smuggler-no-field.test.ts','tests/smuggler-no-field-controls.test.tsx','tests/smuggler-no-field-recovery.test.ts','tests/spice-banker.test.ts','tests/spice-banker-recovery.test.ts','tests/spice-banker-controls.test.tsx','tests/sandmaster-movement.test.ts','tests/sandmaster-movement-recovery.test.ts','tests/sandmaster-movement-controls.test.tsx','tests/diplomat-defense.test.ts','tests/diplomat-defense-controls.test.tsx','tests/diplomat-defense-recovery.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ const DECISIONS = {
|
||||
leaderSkillVisibility: true,
|
||||
leaderSkillRevival: true,
|
||||
mentatQuestion: true,
|
||||
bureaucratPayment: true,
|
||||
sukRescue: true,
|
||||
rihani: true,
|
||||
diplomatDefense: true,
|
||||
|
||||
Reference in new issue
Block a user