Prototype conditional future battle Truthtrance
Verify and publish checkpoint container / container (push) Failing after 11m11s

This commit is contained in:
Quintine committed 2026-10-28 21:17:44 +11:00
1 parent 71009d73e3
commit 02695aea7c
15 files changed
+721 -8

No files matched your search

+83
View File
@@ -1,5 +1,7 @@
import { createTwoPlayerControllers, validateTwoPlayerControllersLobby, validateTwoPlayerControllersComplete,
addTwoPlayerControlledSeat, TwoPlayerControllersError, type TwoPlayerControllers } from './two-player-controllers';
import { createFutureBattleLeader, bindFutureBattleLeader, settleFutureBattleLeader, releaseFutureBattleLeader,
expireFutureBattleLeader, type FutureBattleLeaderReceipt } from './future-battle-leader';
import { createFactionDraft, validateFactionDraft, currentFactionDrafter, chooseFaction,
FactionDraftError, type FactionDraft } from './faction-draft';
import { createFactionSwap, validateFactionSwap, answerFactionSwap, cancelFactionSwap,
@@ -682,6 +684,7 @@ import {
type TruthQueueEntry,
type TruthRecord,
type TruthAnswer,
type TruthQuestion,
type MixedMovementClaim,
} from './truthtrance';
import type { MixedShipmentExpression } from './mixed-shipment-question';
@@ -2319,6 +2322,7 @@ export type Game = {
};
truthtrance?: TruthWindow | null;
truthHistory?: TruthRecord[];
futureBattleLeaderPromises?: FutureBattleLeaderReceipt[];
travelPromises?: TravelPromise[];
turnActionPromises?: TurnActionPromise[];
/** Actual accepted printed-role uses; old games need no baseline retrofit. */
@@ -7550,6 +7554,7 @@ function truthtranceDiscardSignature(g: Game, next: TruthDiscardContinuation) {
...(['travel', 'turnActions'].includes(next.record.question.kind) ? { movementExpressionPromises: g.movementExpressionPromises } : {}),
...(g.turnActionPromises ? { turnActionPromises: g.turnActionPromises, bribePromises: g.bribePromises,
bribeExpressionPromises: g.bribeExpressionPromises, bribePromiseSequence: g.bribePromiseSequence } : {}),
...(next.record.question.kind === 'futureBattleLeader' ? {futureBattleLeaderPromises: g.futureBattleLeaderPromises} : {}),
...(next.record.question.kind === 'mixedTraitorCall' ? { mixedTraitorQuestions: g.mixedTraitorQuestions } : {}),
...(next.record.question.kind === 'mixedBattlePlan' ? { mixedBattleQuestions: g.mixedBattleQuestions } : {}),
...(['allianceExpression', 'mixedAlliance'].includes(next.record.question.kind) ? {
@@ -8622,6 +8627,13 @@ function treacheryDiscardIntegrity(g: Game) {
}
const q = record.question,
promise = c.promise;
if (q.kind === 'futureBattleLeader') {
const binding = g.futureBattleLeaderPromises?.at(-1);
requireRule(binding && binding.turn === g.turn && binding.asker === record.asker &&
binding.player === q.target && binding.leader === q.leader &&
binding.answer === (record.answer === 'yes') && binding.status === 'waiting' && !binding.battle,
'The completed Truthtrance lost its conditional battle answer.');
}
const expectedShipment = q.kind === 'shipment' || q.kind === 'mixedShipment';
const expectedBattle =
q.kind === 'battlePlan' &&
@@ -31674,6 +31686,61 @@ function battleTruthAnswers(
answers.push(answer ? 'yes' : 'no');
return answers.length ? answers : ['unknown'];
}
type FutureBattleLeaderQuestion = Extract<TruthQuestion, {kind: 'futureBattleLeader'}>;
function futureBattleLeaderTruthAnswers(g: Game, p: Player, q: FutureBattleLeaderQuestion): TruthAnswer[] {
const otherPromises: {player: string; turn?: number; fulfilled?: boolean; released?: boolean}[] = [
...(g.travelPromises ?? []), ...(g.shipmentPromises ?? []), ...(g.movementPromises ?? []),
...(g.movementExpressionPromises ?? []), ...(g.bribePromises ?? []), ...(g.bribeExpressionPromises ?? []),
...(g.revivalPromises ?? []), ...(g.leaderRevivalPromises ?? []), ...(g.revivalExpressionPromises ?? []),
...(g.bidPromises ?? []), ...(g.phaseBidPromises ?? []), ...(g.richeseBidPromises ?? []),
...(g.earlyRicheseBidPromises ?? []), ...(g.alliancePromises ?? []), ...(g.allianceExpressionPromises ?? []),
];
if (q.turn !== g.turn || q.target !== p.id || g.phase !== 5 ||
(g.futureBattleLeaderPromises ?? []).some(row => row.turn === g.turn && row.player === p.id &&
(row.status === 'waiting' || row.status === 'bound')) ||
otherPromises.some(row => row.player === p.id && row.turn === g.turn && !row.fulfilled && !row.released) ||
liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length) return ['unknown'];
const available = p.leaders.filter(leader => nativeAvailable(leader) && !leader.usedAt && leader.faction === p.faction);
const result: TruthAnswer[] = [];
if (available.some(leader => leader.id === q.leader)) result.push('yes');
if (available.some(leader => leader.id !== q.leader) || p.hand.some(card => card.kind === 'hero')) result.push('no');
return result.length ? result : ['unknown'];
}
function bindFutureBattleLeaderTruth(g: Game, p: Player, q: FutureBattleLeaderQuestion, answer: boolean, asker: string) {
requireRule(futureBattleLeaderTruthAnswers(g, p, q).includes(answer ? 'yes' : 'no'),
'Choose an answer supported by your own available leaders.');
(g.futureBattleLeaderPromises ??= []).push(createFutureBattleLeader(
{ turn: g.turn, asker, player: p.id, leader: q.leader, answer }, leaders(p.faction).map(leader => leader.id)));
}
function bindFutureBattleLeaderQuestions(g: Game) {
const battle = g.battle;
if (!battle?.event || !g.futureBattleLeaderPromises) return;
g.futureBattleLeaderPromises = g.futureBattleLeaderPromises.map(receipt => {
const bound = bindFutureBattleLeader(receipt, {turn: g.turn, event: battle.event!, attacker: battle.attacker,
defender: battle.defender, native: true, plansOpen: !battle.revealed});
if (bound !== receipt) {
const claim: PlanClaim = {kind: 'leader', leader: bound.leader};
bindBattleTruth(g, getPlayer(g, bound.player), claim, bound.answer, bound.asker);
}
return bound;
});
}
function reconcileFutureBattleLeaderQuestions(g: Game) {
const battle = g.battle;
if (!g.futureBattleLeaderPromises) return;
g.futureBattleLeaderPromises = g.futureBattleLeaderPromises.map(receipt => {
if (receipt.status === 'waiting' && (g.turn > receipt.turn ||
receipt.turn === g.turn && (g.phase > 6 || g.status !== 'playing')))
return expireFutureBattleLeader(receipt, {turn: receipt.turn, battlePhaseEnded: true});
if (receipt.status !== 'bound' || !battle || battle.event !== receipt.battle) return receipt;
const bound = battle.truthPromises?.find(row => row.player === receipt.player && row.asker === receipt.asker &&
row.answer === receipt.answer && row.claim.kind === 'leader' && row.claim.leader === receipt.leader);
if (bound?.released) return releaseFutureBattleLeader(receipt, {turn: receipt.turn, battle: receipt.battle});
if (battle.revealed && bound) return settleFutureBattleLeader(receipt, {turn: receipt.turn,
battle: receipt.battle!, player: receipt.player, revealed: true, leader: battle.plans[receipt.player].leader});
return receipt;
});
}
function bindBattleTruth(
g: Game,
p: Player,
@@ -40911,6 +40978,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
finishLeaderSkillCustody(g, state);
leaderSkillsIntegrity(g);
reconcileBattlePromises(g, { actor: id, action });
reconcileFutureBattleLeaderQuestions(g);
reconcileShipmentPromises(g, { actor: id, action, phase: state.phase });
reconcileBidPromises(g, { actor: id, action });
reconcilePhaseBids(g, { actor: id, action });
@@ -41575,6 +41643,8 @@ function applyActionInner(
bindMixedBattle: bindMixedBattleTruth,
battleAnswers: battleTruthAnswers,
bindBattle: bindBattleTruth,
futureBattleLeaderAnswers: futureBattleLeaderTruthAnswers,
bindFutureBattleLeader: bindFutureBattleLeaderTruth,
cardBlock: transferCardBlock,
})
)
@@ -45038,6 +45108,7 @@ function applyActionInner(
revealed: false,
traitorCalls: {},
};
bindFutureBattleLeaderQuestions(g);
if (combined) {
g.battle.ecazOccupy = { event: battleEvent, ecaz: ecaz!.id, ally: ally!.id, lead: null, canceled: false };
const opponent = [choice.attacker, choice.defender].find(id => id !== ecaz!.id && id !== ally!.id)!;
@@ -45200,6 +45271,7 @@ function applyActionInner(
b.revealed = true;
recordTurnActionRevealedPlans(g);
reconcileMixedBattleQuestions(g);
reconcileFutureBattleLeaderQuestions(g);
for (const plan of Object.values(b.plans)) {
const l = g.players
.flatMap((p) => p.leaders)
@@ -45717,6 +45789,17 @@ export function viewGame(state: Game, id: string, now?: number) {
g.truthtrance.question.kind === 'battlePlan'
? battleTruthAnswers(g, me, g.truthtrance.question.claim)
: null,
truthFutureBattleLeaderAnswers: g.truthtrance?.stage === 'answer' &&
g.truthtrance.question?.target === id && g.truthtrance.question.kind === 'futureBattleLeader'
? futureBattleLeaderTruthAnswers(g, me, g.truthtrance.question) : null,
truthFutureBattleLeaderTargets: g.status === 'playing' && !g.advanced && g.phase === 5 &&
!g.expansions.length && !g.techTokens && !g.strongholdCards && !g.nexusCards && !g.homeworlds &&
!g.leaderSkills && !g.discoveryEnabled && !g.ecazTreachery && !g.battle && !g.response &&
!g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard
? g.players.filter(player => player.id !== id && !(g.futureBattleLeaderPromises ?? []).some(row =>
row.turn === g.turn && row.player === player.id && (row.status === 'waiting' || row.status === 'bound')))
.map(player => player.id) : [],
futureBattleLeaderPromises: (g.futureBattleLeaderPromises ?? []).filter(row => row.player === id),
bidPromiseLot: (() => { const lot = bidOpportunity(g); return lot ? { turn: lot.turn, lot: lot.lot } : null; })(),
karamaAuctionCards: quotedAuctionKaramaCards(g, me),
karamaShipmentCards: quotedShipmentKaramaCards(g, me),