Prototype named Ghola leader Truthtrance promises
Verify and publish checkpoint container / container (push) Successful in 10m45s
Verify and publish checkpoint container / container (push) Successful in 10m45s
This commit is contained in:
1 parent
f8078ed5e2
commit
c1ffa1c28a
16 files changed
+689
-9
No files matched your search
+102
-7
@@ -1,3 +1,4 @@
|
||||
import { GholaLeaderPromiseError, liveGholaLeaderPromises, gholaLeaderRequirements, recordPromisedGholaLeader, validateGholaLeaderPromises, type GholaLeaderClaim, type GholaLeaderPromise } from './ghola-leader-promises';
|
||||
import { basicPendingMovement } from './basic-pending-movement';
|
||||
import { createTwoPlayerControllers, validateTwoPlayerControllersLobby, validateTwoPlayerControllersComplete,
|
||||
addTwoPlayerControlledSeat, TwoPlayerControllersError, type TwoPlayerControllers } from './two-player-controllers';
|
||||
@@ -2468,6 +2469,7 @@ export type Game = {
|
||||
})[];
|
||||
revivalPromises?: RevivalPromise[];
|
||||
leaderRevivalPromises?: LeaderRevivalPromise[];
|
||||
gholaLeaderPromises?: GholaLeaderPromise[];
|
||||
revivalExpressionPromises?: RevivalExpressionPromise[];
|
||||
/** Original mixed questions link to the existing ordinary-return ledger. */
|
||||
mixedRevivalQuestions?: (MixedRevivalClaim & {
|
||||
@@ -22049,7 +22051,7 @@ function turnActionRule<T>(operation: () => T): T {
|
||||
try { return operation(); }
|
||||
catch (error) { if (error instanceof TurnActionError) throw new RuleError(error.message); throw error; }
|
||||
}
|
||||
type TurnActionCardRequirement = { effect: PrintedCardEffect; answer: boolean; baseline: number };
|
||||
type TurnActionCardRequirement = { effect: PrintedCardEffect; answer: boolean; baseline: number; gholaEvent?: string };
|
||||
/** This field exists only on detached completion clones and is never returned
|
||||
* as a real game or installed as a selected whole-answer branch. */
|
||||
type TurnActionCardProbe = Game & {
|
||||
@@ -22086,7 +22088,9 @@ function recordNativePrintedCardPlay(g: Game, player: string, effect: PrintedCar
|
||||
recordTurnActionCardPlay(g.turnActionPromises!, player, g.turn, effect));
|
||||
}
|
||||
function turnActionCardsMatch(g: Game, actor: string, requirements: readonly TurnActionCardRequirement[]): boolean {
|
||||
return requirements.every(required => (nativePrintedCardCount(g, actor, required.effect) > required.baseline) === required.answer);
|
||||
return requirements.every(required => required.gholaEvent
|
||||
? !!g.gholaLeaderPromises?.find(promise => promise.event === required.gholaEvent && promise.fulfilled)
|
||||
: (nativePrintedCardCount(g, actor, required.effect) > required.baseline) === required.answer);
|
||||
}
|
||||
function* nativePrintedCardActions(g: Game, actor: string, effect: PrintedCardEffect): Generator<Action> {
|
||||
const player = getPlayer(g, actor);
|
||||
@@ -22163,7 +22167,8 @@ function* turnActionBoxSeeds(source: Game, actor: string, onlyBox?: string):
|
||||
/** Reject the known printed fee before paid inspection can suspend legality.
|
||||
* The native owner chooses the recovered card only after actual payment. */
|
||||
function guardTurnActionBoxCost(source: Game, owner: Player, box: string) {
|
||||
if (!liveTurnActionPromises(source.turnActionPromises ?? [], owner.id, source.turn).length) return;
|
||||
if (!liveTurnActionPromises(source.turnActionPromises ?? [], owner.id, source.turn).length &&
|
||||
!liveGholaLeaderPromises(source.gholaLeaderPromises ?? [], owner.id, source.turn).some(promise => promise.answer)) return;
|
||||
for (const seed of turnActionBoxSeeds(source, owner.id, box)) if (turnActionCompletion(seed.game, owner.id)) return;
|
||||
throw new RuleError('The two-spice Box payment must preserve your accepted whole Truthtrance answers.');
|
||||
}
|
||||
@@ -22502,7 +22507,8 @@ function reopenTurnActionWindowReady(g: Game, actor: string) {
|
||||
/** Check the actual spent card and returned pieces before a later foreign
|
||||
* income/discard response can hide an own voluntary loss of feasibility. */
|
||||
function guardTurnActionPrintedCardCost(g: Game, player: Player, used: Card) {
|
||||
if (!liveTurnActionPromises(g.turnActionPromises ?? [], player.id, g.turn).length) return;
|
||||
if (!liveTurnActionPromises(g.turnActionPromises ?? [], player.id, g.turn).length &&
|
||||
!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player.id, g.turn).some(promise => promise.answer)) return;
|
||||
const trial = structuredClone(g), owner = getPlayer(trial, player.id);
|
||||
owner.spice += pendingBattleRevivalIncome(trial, owner);
|
||||
const suspended = g.pendingTreacheryDiscard?.continuation;
|
||||
@@ -23071,6 +23077,9 @@ function turnActionCompletionAfterFreshDiscard(source: Game, actor: string, unkn
|
||||
delete trial.travelPromiseOrder;
|
||||
const cardRequirements: TurnActionCardRequirement[] = leaves.flatMap(leaf => leaf.kind === 'cardPlay'
|
||||
? [{ effect: leaf.effect, answer: leaf.answer, baseline: nativePrintedCardCount(source, actor, leaf.effect) }] : []);
|
||||
for (const promise of liveGholaLeaderPromises(source.gholaLeaderPromises ?? [], actor, source.turn))
|
||||
if (promise.answer) cardRequirements.push({ effect: 'ghola', answer: true,
|
||||
baseline: nativePrintedCardCount(source, actor, 'ghola'), gholaEvent: promise.event });
|
||||
if (cardRequirements.length) (trial as TurnActionCardProbe)._turnActionCardPolicy = {
|
||||
player: actor, forbidden: cardRequirements.filter(required => !required.answer).map(required => required.effect),
|
||||
};
|
||||
@@ -32959,6 +32968,10 @@ function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
|
||||
);
|
||||
if (cardId) stageHomeworldRevivalReturn(g, p, 'ghola', group, grant, cardId);
|
||||
}
|
||||
if (typeof action.leader === 'string') {
|
||||
try { g.gholaLeaderPromises = recordPromisedGholaLeader(g.gholaLeaderPromises ?? [], p.id, g.turn, action.leader); }
|
||||
catch (error) { if (error instanceof GholaLeaderPromiseError) throw new RuleError(error.message); throw error; }
|
||||
}
|
||||
techIncome(g, 'axlotl', p);
|
||||
collectRevivalIncome(g, p, 0, false, true);
|
||||
recordNativePrintedCardPlay(g, p.id, 'ghola');
|
||||
@@ -37474,6 +37487,57 @@ function leaderRevivalIdentity(g: Game, p: Player, leader: string): boolean {
|
||||
return leader === 'kwisatz' ? g.advanced && p.faction === 'atreides' :
|
||||
leader === DUKE_VIDAL_ID ? p.faction === 'ecaz' && !!g.dukeVidal : p.leaders.some(l => l.id === leader);
|
||||
}
|
||||
function gholaLeaderTruthAnswers(g: Game, p: Player, claim: GholaLeaderClaim): TruthAnswer[] {
|
||||
if (claim.turn !== g.turn || !leaderRevivalIdentity(g, p, claim.leader)) return [];
|
||||
const ask = g.truthtrance?.queue[0]?.player ?? g.host;
|
||||
return (['yes', 'no'] as const).filter(answer => {
|
||||
const promises = [...(g.gholaLeaderPromises ?? []), { ...claim, event: 'ghola-candidate',
|
||||
player: p.id, asker: ask, answer: answer === 'yes' }];
|
||||
const requirements = gholaLeaderRequirements(promises, p.id, g.turn);
|
||||
if (!requirements || requirements.required.length > 1) return false;
|
||||
return !!turnActionCompletion({ ...g, gholaLeaderPromises: promises }, p.id);
|
||||
});
|
||||
}
|
||||
function bindGholaLeaderTruth(g: Game, p: Player, claim: GholaLeaderClaim, answer: boolean, asker: string) {
|
||||
requireRule(gholaLeaderTruthAnswers(g, p, claim).includes(answer ? 'yes' : 'no'),
|
||||
'Choose an answer with a legal named Ghola completion.');
|
||||
(g.gholaLeaderPromises ??= []).push({ ...claim, event: crypto.randomUUID(), player: p.id, asker, answer });
|
||||
}
|
||||
function reconcileGholaLeaderTruth(g: Game, voluntary?: { actor: string; action: Action }) {
|
||||
if (!g.gholaLeaderPromises?.length) return;
|
||||
try { validateGholaLeaderPromises(g.gholaLeaderPromises, g.players.map(p => p.id)); }
|
||||
catch (error) { if (error instanceof GholaLeaderPromiseError) throw new RuleError(error.message); throw error; }
|
||||
if (g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama) return;
|
||||
for (const promise of g.gholaLeaderPromises) {
|
||||
if (promise.fulfilled || promise.released) continue;
|
||||
if (!promise.answer && (promise.turn !== g.turn || g.status !== 'playing')) { promise.fulfilled = true; continue; }
|
||||
if (promise.answer && (promise.turn !== g.turn || g.status !== 'playing' || !turnActionCompletion(g, promise.player))) {
|
||||
requireRule(voluntary?.actor !== promise.player, 'You cannot voluntarily evade your named Ghola Truthtrance answer.');
|
||||
promise.released = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
function projectedGholaLeaderGuide(g: Game, player: string): { action: Action } | null {
|
||||
if (g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama) return null;
|
||||
const promise = liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player, g.turn).find(p => p.answer);
|
||||
if (!promise) return null;
|
||||
// A completion can defer the Ghola play into a later movement forecast.
|
||||
// Offer an immediate action only if its actual physical play is legal now
|
||||
// and leaves a joint completion for every other accepted answer.
|
||||
for (const action of nativePrintedCardActions(g, player, 'ghola')) {
|
||||
if (action.leader !== promise.leader) continue;
|
||||
try {
|
||||
const trial = applyActionInner(g, player, action, 'movementPreparation');
|
||||
if (!trial.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled)) continue;
|
||||
trial.truthtrance = null; trial.response = null; trial.pendingKarama = null;
|
||||
const owner = getPlayer(trial, player);
|
||||
owner.spice += pendingBattleRevivalIncome(trial, owner);
|
||||
reconcileRevivalPromises(trial, { actor: player, action });
|
||||
if (turnActionCompletion(trial, player)) return { action };
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function leaderRevivalTruthAnswers(g: Game, p: Player, claim: LeaderRevivalClaim): ('yes' | 'no' | 'unknown')[] {
|
||||
if (!revivalQuestionWindow(g) || !revivalOpportunity(g, p.id) || claim.turn !== g.turn ||
|
||||
!leaderRevivalIdentity(g, p, claim.leader)) return [];
|
||||
@@ -37586,7 +37650,8 @@ function guardPromisedLeaderRevival(g: Game, p: Player, action: Action,
|
||||
!liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).length &&
|
||||
!liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length &&
|
||||
!hasMovementPromises(g, p.id) &&
|
||||
!hasBribePromises(g, p.id, true)) return;
|
||||
!hasBribePromises(g, p.id, true) &&
|
||||
!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], p.id, g.turn).some(promise => promise.answer)) return;
|
||||
if (!quote) {
|
||||
requireRule(leaderRevivalPromisesSatisfied(g.leaderRevivalPromises ?? [], p.id, g.turn, []),
|
||||
'Complete the named leader returns required by your Truthtrance Yes before finishing Revival.');
|
||||
@@ -37596,8 +37661,10 @@ function guardPromisedLeaderRevival(g: Game, p: Player, action: Action,
|
||||
trial.truthtrance = null;
|
||||
if (!trial.revivalOrder) trial.ready = trial.ready.filter(player => player !== p.id);
|
||||
stagePromisedLeaderReturn(trial, actor, quote);
|
||||
requireRule(!!revivalPromiseCompletion(trial, actor) && bribeCompatibleLaterPromises(trial, actor, {}),
|
||||
'This leader request would prevent you from honoring your Revival answers.');
|
||||
requireRule(!!revivalPromiseCompletion(trial, actor) && bribeCompatibleLaterPromises(trial, actor, {}) &&
|
||||
(!liveGholaLeaderPromises(trial.gholaLeaderPromises ?? [], p.id, g.turn).some(promise => promise.answer) ||
|
||||
!!turnActionCompletion(trial, p.id)),
|
||||
'This leader request would prevent you from honoring your Truthtrance answers.');
|
||||
}
|
||||
/** Reconcile the shared purse in actual answer order across both ledgers.
|
||||
* Existing records without an order stamp precede newly accepted answers. */
|
||||
@@ -43181,6 +43248,24 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
|
||||
// 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;
|
||||
// Voluntary actions may create a response owned by somebody else before the
|
||||
// regular reconciler can run. Quote the already-completed action on a
|
||||
// detached state before that response can hide loss of the last witness.
|
||||
if (voluntary && liveGholaLeaderPromises(state.gholaLeaderPromises ?? [], id, state.turn).some(promise => promise.answer)) {
|
||||
const prior = liveGholaLeaderPromises(state.gholaLeaderPromises ?? [], id, state.turn).filter(promise => promise.answer);
|
||||
const unresolved = prior.some(promise => !g.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled));
|
||||
if (unresolved && (g.turn !== state.turn || g.status !== 'playing'))
|
||||
throw new RuleError('Complete your named Ghola Truthtrance answer before ending this turn.');
|
||||
if (unresolved && !state.truthtrance && !state.response && !state.decision && !state.phaseOpening &&
|
||||
!state.pendingTreacheryDiscard && !state.pendingKarama &&
|
||||
(g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama)) {
|
||||
const trial = structuredClone(g);
|
||||
trial.truthtrance = null; trial.response = null; trial.decision = null;
|
||||
trial.phaseOpening = null; trial.pendingTreacheryDiscard = null; trial.pendingKarama = null;
|
||||
requireRule(!!turnActionCompletion(trial, id),
|
||||
'This action would remove the last legal completion of your named Ghola Truthtrance answer.');
|
||||
}
|
||||
}
|
||||
reconcilePledgeCommitments(g, pledgeVoluntary);
|
||||
reconcileTravelPromises(g, promiseActor, action, state.phase, state);
|
||||
reconcileBidPromises(g, voluntary);
|
||||
@@ -43189,6 +43274,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
|
||||
reconcileRicheseBids(g, voluntary);
|
||||
reconcileCharityTruth(g, voluntary);
|
||||
reconcileRevivalPromises(g, voluntary);
|
||||
reconcileGholaLeaderTruth(g, voluntary);
|
||||
reconcileBribePromises(g, voluntary);
|
||||
reconcilePublicDealTruth(g, voluntary, options?.insolventDealPayment ? undefined : state);
|
||||
reconcileGroundMovementPromises(g, promiseActor, action, state.phase);
|
||||
@@ -44055,6 +44141,8 @@ function applyActionInner(
|
||||
bindRevivalExpression: bindRevivalExpressionTruth,
|
||||
mixedRevivalAnswers: mixedRevivalTruthAnswers,
|
||||
bindMixedRevival: bindMixedRevivalTruth,
|
||||
gholaLeaderAnswers: gholaLeaderTruthAnswers,
|
||||
bindGholaLeader: bindGholaLeaderTruth,
|
||||
leaderRevivalAnswers: leaderRevivalTruthAnswers,
|
||||
bindLeaderRevival: bindLeaderRevivalTruth,
|
||||
revivalAnswers: revivalTruthAnswers,
|
||||
@@ -48186,6 +48274,8 @@ function applyActionInner(
|
||||
return g;
|
||||
}
|
||||
if (t === 'ready') {
|
||||
if (g.phase === 8) requireRule(!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], id, g.turn)
|
||||
.some(promise => promise.answer), 'Complete your named Ghola Truthtrance answer before finishing Mentat Pause.');
|
||||
requireRule(!(g.phase === 1 && g.nexus && g.coalitionState?.proposal),
|
||||
'Resolve the group invitation before finishing the Nexus.');
|
||||
if (g.phase === 2) {
|
||||
@@ -48545,6 +48635,11 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
g.truthtrance.question.kind === 'revival' ? revivalTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
revivalPromises: (g.revivalPromises ?? []).filter(promise => promise.player === id).map(promise => ({ ...promise })),
|
||||
revivalPromiseGuide: projectedRevivalPromiseGuide(g, id),
|
||||
truthGholaLeaderTargets: g.status === 'playing' && g.phase >= 0 && g.phase <= 7 ? g.players.map(player => player.id) : [],
|
||||
truthGholaLeaderAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'gholaLeader' ? gholaLeaderTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
gholaLeaderPromises: (g.gholaLeaderPromises ?? []).filter(promise => promise.player === id).map(promise => ({ ...promise })),
|
||||
gholaLeaderPromiseGuide: projectedGholaLeaderGuide(g, id),
|
||||
truthLeaderRevivalTargets: revivalQuestionWindow(g) ? g.players.filter(player => revivalOpportunity(g, player.id)).map(player => player.id) : [],
|
||||
truthLeaderRevivalAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'leaderRevival' ? leaderRevivalTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
|
||||
Reference in new issue
Block a user