Prototype Truthtrance ally funding commitments
Verify and publish checkpoint container / container (push) Successful in 10m48s

This commit is contained in:
Quintine committed 2026-10-29 05:44:01 +11:00
1 parent 68643925e7
commit c414a03a19
14 files changed
+795 -4

No files matched your search

+114
View File
@@ -18,6 +18,10 @@ import { createVictoryContinuation, validateVictoryContinuation, voteVictoryCont
import { SabotageGiftError, quoteSabotageGift, quoteSabotageGiftDiscard } from './sabotage-gift';
import { mobileStrongholdCollectionCapacity } from './mobile-stronghold-collection';
import { basicKullTechRoster, kullDefaultEligible } from './kull-default-profile';
import { PledgePromiseError, livePledgePromises, pledgePromiseRequirements, recordPromisedPledge,
reconcilePledgePromises as reconcilePledgeRows, validatePledgePromises, pledgeQuestionHistoryBlocked,
type PledgeClaim, type PledgePromise } from './pledge-promises';
import { nativePromiseScopeSupported } from './native-promise-scope';
import { advancedKullLobbyEligible } from './kull-advanced-profile';
import { SaphoTruthOrderError, quoteSaphoTruthOrder, reorderSaphoTruthOrder, validateSaphoTruthOrder } from './sapho-truth-order';
import { createSaphoArrivalOrder, validateSaphoArrivalOrder, quoteSaphoArrivalOrder,
@@ -2420,6 +2424,7 @@ export type Game = {
/** Shared accepted order for typed force and named leader answers. */
revivalPromiseSequence?: string[];
bribePromises?: BribePromise[];
pledgePromises?: PledgePromise[];
bribeExpressionPromises?: BribeExpressionPromise[];
/** Public original mixed questions; native residuals own physical payment rules. */
mixedBribeQuestions?: (MixedBribeClaim & {
@@ -16344,6 +16349,97 @@ function pledgeAid(g: Game, p: Player, requested: unknown) {
g.aid[p.id] = { recipient: p.ally, amount };
log(g, `${p.name} updated the spice available to their ally this phase.`);
}
function pledgeRule<T>(run: () => T): T {
try { return run(); }
catch (error) { if (error instanceof PledgePromiseError) throw new RuleError(error.message); throw error; }
}
function pledgeIntegrity(g: Game) {
pledgeRule(() => validatePledgePromises(g.pledgePromises ?? [], g.players.map(player => player.id)));
}
/** The first version excludes simultaneous independently funded obligations.
* These rows are public timing facts; no opponent wallet or card is inspected. */
function otherPledgeCommitments(g: Game): boolean {
const rows: unknown[] = [
...(g.shipmentPromises ?? []), ...(g.bidPromises ?? []), ...(g.phaseBidPromises ?? []),
...(g.richeseBidPromises ?? []), ...(g.earlyRicheseBidPromises ?? []),
...(g.revivalPromises ?? []), ...(g.leaderRevivalPromises ?? []), ...(g.revivalExpressionPromises ?? []),
...(g.bribePromises ?? []), ...(g.bribeExpressionPromises ?? []),
...(g.alliancePromises ?? []), ...(g.allianceExpressionPromises ?? []),
...(g.movementPromises ?? []), ...(g.movementExpressionPromises ?? []),
...(g.travelPromises ?? []), ...(g.turnActionPromises ?? []), ...(g.charityPromises ?? []),
...(g.battle?.truthPromises ?? []), ...(g.futureBattleLeaderPromises ?? []),
];
return rows.some(value => {
if (!value || typeof value !== 'object') return true;
const row = value as {turn?: number; fulfilled?: boolean; released?: boolean; status?: string};
return (row.turn === undefined || row.turn === g.turn) && !row.fulfilled && !row.released &&
(row.status === undefined || row.status === 'waiting' || row.status === 'bound');
});
}
function pledgePublicWindow(g: Game): boolean {
return g.status === 'playing' && !g.advanced && (g.phase === 3 || g.phase === 5) &&
nativePromiseScopeSupported(g) && !g.kullPreview && !g.semutaPreview && !g.nexusKullPreview &&
!g.coalitionPreview && !g.coalitionState &&
!g.response && !g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard &&
!g.pendingKull && !g.pendingArrivalOverlap && !g.pendingArrivalReaction?.length;
}
function pledgeQuestionWindow(g: Game): boolean {
return pledgePublicWindow(g) && !otherPledgeCommitments(g);
}
function pledgeMutualAlly(g: Game, player: string): string | null {
const owner = g.players.find(seat => seat.id === player), ally = owner?.ally;
return ally && g.players.some(seat => seat.id === ally && seat.ally === player) ? ally : null;
}
/** Ask the native pledge routine on a detached state, retaining its real floor,
* locked contributions and funding rules. This is not an automatic payment. */
function pledgeCompletion(g: Game, p: Player, promises = g.pledgePromises ?? []): Action | null {
const ally = pledgeMutualAlly(g, p.id);
if (!ally || (g.phase !== 3 && g.phase !== 5)) return null;
const required = pledgeRule(() => pledgePromiseRequirements(promises, p.id, g.turn, g.phase, ally));
if (!required) return null;
if (!required.minimum) return {type: 'pledgeAid', amount: 0};
const current = g.aid[p.id]?.amount ?? 0;
const silent = g.richeseAuction?.method === 'silent' && !g.richeseAuction.outcome &&
Object.hasOwn(g.richeseAuction.sealed, ally);
const amount = Math.max(required.minimum, committedOutgoingAid(g, p),
richeseAllyCommitment(g.richeseAuction, g.richeseFunding ?? {}, p.id), silent ? current : 0);
if (required.maximum !== undefined && amount > required.maximum) return null;
try {
const trial = structuredClone(g);
pledgeAid(trial, getPlayer(trial, p.id), amount);
return {type: 'pledgeAid', amount};
} catch (error) { if (error instanceof RuleError) return null; throw error; }
}
function pledgeTruthAnswers(g: Game, p: Player, claim: PledgeClaim): ('yes' | 'no')[] {
if (!pledgeQuestionWindow(g) || pledgeQuestionHistoryBlocked(g.truthHistory ?? [], g.turn, g.phase) ||
claim.turn !== g.turn || claim.phase !== g.phase ||
pledgeMutualAlly(g, p.id) !== claim.ally ||
(g.pledgePromises ?? []).some(row => row.turn === g.turn && row.phase === g.phase &&
!row.fulfilled && !row.released)) return [];
return (['yes', 'no'] as const).filter(answer => {
const rows = [...(g.pledgePromises ?? []), {...claim, event: 'candidate', player: p.id,
asker: g.truthtrance?.queue[0]?.player ?? g.host, answer: answer === 'yes'}];
return !!pledgeCompletion(g, p, rows);
});
}
function bindPledgeTruth(g: Game, p: Player, claim: PledgeClaim, answer: boolean, asker: string) {
(g.pledgePromises ??= []).push({...claim, event: crypto.randomUUID(), player: p.id, asker, answer});
}
function reconcilePledgeCommitments(g: Game, voluntary?: {actor: string; action: Action}) {
if (!g.pledgePromises?.length) return;
pledgeIntegrity(g);
g.pledgePromises = pledgeRule(() => reconcilePledgeRows(g.pledgePromises!, {
turn: g.turn, phase: g.phase, mutualAlly: player => pledgeMutualAlly(g, player),
canFulfill: promise => !!pledgeCompletion(g, getPlayer(g, promise.player)),
...(voluntary ? {voluntaryActor: voluntary.actor} : {}),
}));
}
function projectedPledgeGuide(g: Game, player: Player): {action: Action} | null {
if (!pledgeQuestionWindow(g) || g.truthtrance || !livePledgePromises(g.pledgePromises ?? [], player.id, g.turn, g.phase)
.some(row => row.answer)) return null;
const action = pledgeCompletion(g, player);
return action ? {action} : null;
}
function alliancePromiseRule<T>(calculate: () => T): T {
try { return calculate(); }
catch (error) { if (error instanceof AlliancePromiseError || error instanceof AllianceExpressionPromiseError) throw new RuleError(error.message); throw error; }
@@ -41782,6 +41878,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
'Finish or cancel the faction draft before changing the lobby.',
);
victoryContinuationIntegrity(state);
pledgeIntegrity(state);
bidPromiseIntegrity(state);
phaseBidIntegrity(state);
richeseBidIntegrity(state);
@@ -42110,6 +42207,8 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
// A final counter pass settles the opposing claimant's effect; it does not
// turn that forced debit into the passer's voluntary payment or expenditure.
const voluntary = forcedThumperDraw || action.type === 'passResponse' ? undefined : { actor: id, action };
const pledgeVoluntary = initiator === id ? voluntary : undefined;
reconcilePledgeCommitments(g, pledgeVoluntary);
reconcileTravelPromises(g, promiseActor, action, state.phase);
reconcileBidPromises(g, voluntary);
reconcilePhaseBids(g, voluntary);
@@ -42248,6 +42347,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
reconcileCharityTruth(g, { actor: id, action });
reconcileRevivalPromises(g, { actor: id, action });
reconcileBribePromises(g, { actor: id, action });
reconcilePledgeCommitments(g, pledgeVoluntary);
reconcilePublicDealTruth(g, voluntary, options?.insolventDealPayment ? undefined : state);
reconcileAlliancePromises(g, { actor: id, action });
settleAutomaticContinuations(g);
@@ -42487,6 +42587,7 @@ function settleAutomaticContinuations(g: Game) {
reconcileCharityTruth(g);
reconcileRevivalPromises(g);
reconcileBribePromises(g);
reconcilePledgeCommitments(g);
reconcilePublicDealTruth(g);
reconcileGroundMovementPromises(g);
reconcileAlliancePromises(g);
@@ -42507,6 +42608,7 @@ export function normalizeAutomaticGame(state: Game): Game {
factionSwapIntegrity(state);
publicDealIntegrity(state);
victoryContinuationIntegrity(state);
pledgeIntegrity(state);
if (state.victoryContinuation) return state;
bidPromiseIntegrity(state);
phaseBidIntegrity(state);
@@ -42605,6 +42707,7 @@ export function normalizeAutomaticGame(state: Game): Game {
reconcileCharityTruth(g);
reconcileRevivalPromises(g);
reconcileBribePromises(g);
reconcilePledgeCommitments(g);
reconcilePublicDealTruth(g);
reconcileGroundMovementPromises(g);
reconcileAlliancePromises(g);
@@ -42871,6 +42974,8 @@ function applyActionInner(
bindAlliance: bindAllianceTruth,
bribeAnswers: bribeTruthAnswers,
bindBribe: bindBribeTruth,
pledgeAnswers: pledgeTruthAnswers,
bindPledge: bindPledgeTruth,
bribeExpressionAnswers: bribeExpressionTruthAnswers,
bindBribeExpression: bindBribeExpressionTruth,
mixedBribeAnswers: mixedBribeTruthAnswers,
@@ -45604,6 +45709,8 @@ function applyActionInner(
'You may fund an ally during bidding/shipment, or as advanced CHOAM during Battle.',
);
pledgeAid(g, p, action.amount);
if (g.pledgePromises?.length) g.pledgePromises = pledgeRule(() => recordPromisedPledge(
g.pledgePromises!, p.id, g.turn, g.phase, {ally: p.ally!, amount: action.amount as number}));
return g;
}
if (t === 'richeseBid') {
@@ -47015,6 +47122,7 @@ export function viewGame(state: Game, id: string, now?: number) {
factionSwapIntegrity(state);
publicDealIntegrity(state);
victoryContinuationIntegrity(state);
pledgeIntegrity(state);
revivalOrderIntegrity(state);
charityOrderIntegrity(state);
state = migrateAtomicsTargets(state);
@@ -47266,6 +47374,12 @@ export function viewGame(state: Game, id: string, now?: number) {
alliancePromises: projectedAlliancePromises(g, id),
alliancePromiseGuide: projectedAlliancePromiseGuide(g, id),
truthBribeTargets: bribeQuestionWindow(g) ? g.players.map(player => player.id) : [],
truthPledgeTargets: pledgePublicWindow(g) && !pledgeQuestionHistoryBlocked(g.truthHistory ?? [], g.turn, g.phase)
? g.players.filter(player => !!pledgeMutualAlly(g, player.id)).map(player => player.id) : [],
truthPledgeAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
g.truthtrance.question.kind === 'pledgeAid' ? pledgeTruthAnswers(g, me, g.truthtrance.question) : null,
pledgePromises: (g.pledgePromises ?? []).filter(row => row.player === id).map(row => ({...row})),
pledgePromiseGuide: projectedPledgeGuide(g, me),
truthBribeAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
g.truthtrance.question.kind === 'bribe' ? bribeTruthAnswers(g, me, g.truthtrance.question) : null,
bribePromises: (g.bribePromises ?? []).filter(promise => promise.player === id).map(promise => ({ ...promise })),