Prototype Truthtrance ally funding commitments
Verify and publish checkpoint container / container (push) Successful in 10m48s
Verify and publish checkpoint container / container (push) Successful in 10m48s
This commit is contained in:
1 parent
68643925e7
commit
c414a03a19
14 files changed
+795
-4
No files matched your search
+114
@@ -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 })),
|
||||
|
||||
Reference in new issue
Block a user