Support Sapho ordering for simultaneous Truthtrances
This commit is contained in:
1 parent
4690200f59
commit
298c8245a6
16 files changed
+1020
-3
No files matched your search
@@ -1324,6 +1324,9 @@ function policyActions(g: GameView): Action[] {
|
||||
if (g.bidPromiseGuide) return [g.bidPromiseGuide.action];
|
||||
if (g.truthtrance) {
|
||||
const w = g.truthtrance;
|
||||
if (w.stage === 'order' && w.order)
|
||||
return w.order.holders.includes(me.id) && !w.order.passed.includes(me.id)
|
||||
? [{type:'truthOrderPass',event:w.order.event,revision:w.order.revision}] : [];
|
||||
if (w.stage === 'priority')
|
||||
return w.passed.includes(me.id) ? [] : [{ type: 'truthPass' }];
|
||||
if (w.stage === 'answer') {
|
||||
|
||||
+105
@@ -1,3 +1,4 @@
|
||||
import { SaphoTruthOrderError, quoteSaphoTruthOrder, reorderSaphoTruthOrder, validateSaphoTruthOrder } from './sapho-truth-order';
|
||||
import { RicheseBidPromiseError, richeseBidCompletionRange, richeseBidRequired, recordRicheseBid,
|
||||
liveRicheseBidPromises, reconcileRicheseBidPromises, finishRicheseBidLot, validateRicheseBidPromises,
|
||||
type RicheseBidClaim, type RicheseBidPromise, type RicheseBidOpportunity } from './richese-bid-promises';
|
||||
@@ -1617,6 +1618,17 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoTruthOrderDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
event: string;
|
||||
revision: number;
|
||||
mode: 'first' | 'last';
|
||||
truth: TruthWindow;
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoMovementDiscard';
|
||||
player: string;
|
||||
@@ -5888,6 +5900,14 @@ function distransDiscardSignature(g: Game, c: DistransDiscardContinuation) {
|
||||
recipientHand: recipient.hand.map(card => card.id),
|
||||
});
|
||||
}
|
||||
type SaphoTruthOrderDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'], { kind: 'saphoTruthOrderDiscard' }
|
||||
>;
|
||||
function saphoTruthOrderDiscardSignature(g: Game, c: SaphoTruthOrderDiscardContinuation) {
|
||||
return JSON.stringify({player:c.player,card:c.card,event:c.event,revision:c.revision,mode:c.mode,
|
||||
truth:c.truth,parent:nullentropyParentSignature(g,c.resume,c.event),turn:g.turn,phase:g.phase,
|
||||
order:g.order,active:g.active});
|
||||
}
|
||||
type SaphoMovementDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoMovementDiscard' }
|
||||
@@ -7541,6 +7561,21 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === distransDiscardSignature(g, c),
|
||||
'The completed private Distrans transfer no longer matches its fresh discard.',
|
||||
);
|
||||
} else if (continuation?.kind === 'saphoTruthOrderDiscard') {
|
||||
const c = continuation, entry = batch.entries[0], truth = c.truth;
|
||||
requireRule(batch.cause === 'sapho:truthOrder' && batch.entries.length === 1 && entry.publicFace &&
|
||||
entry.discardedBy === c.player && entry.card.id === c.card &&
|
||||
richeseCardDefinition(entry.card)?.card.effect === 'juiceOfSapho' &&
|
||||
!c.resume.response && !c.resume.decision && !c.resume.pendingKarama && !c.resume.phaseOpening &&
|
||||
truth && truth.question === null && (truth.stage === 'order' || truth.stage === 'ask') &&
|
||||
truth.queue.length > 0 && new Set(truth.queue.map(row => row.card)).size === truth.queue.length &&
|
||||
truth.queue.every(row => queuedTruthCardMatches(g.players.find(player => player.id === row.player)?.hand ?? [],row)) &&
|
||||
(truth.stage !== 'order' || !!truth.order && truth.order.event === c.event &&
|
||||
truth.order.revision === c.revision + 1 && truth.order.passed.includes(c.player)) &&
|
||||
c.stateSignature === saphoTruthOrderDiscardSignature(g,c),
|
||||
'The used Sapho no longer matches its suspended Truthtrance order.');
|
||||
if (truth.stage === 'order') try { validateSaphoTruthOrder(truth.order!,truth.queue); }
|
||||
catch (error) { if (error instanceof SaphoTruthOrderError) throw new RuleError(error.message); throw error; }
|
||||
} else if (continuation?.kind === 'saphoCharityDiscard') {
|
||||
const c=continuation,entry=batch.entries[0],frame=g.charityOrder;
|
||||
const latest=frame?.version===2 ? frame.uses.at(-1) : null;
|
||||
@@ -8739,6 +8774,7 @@ function semutaOfferSupported(
|
||||
continuation.kind !== 'terrorDiscard' &&
|
||||
continuation.kind !== 'nexusChoamTrade' &&
|
||||
continuation.kind !== 'nexusChoamBetrayal' &&
|
||||
continuation.kind !== 'saphoTruthOrderDiscard' &&
|
||||
continuation.kind !== 'saphoMovementDiscard' &&
|
||||
continuation.kind !== 'saphoRevivalDiscard' &&
|
||||
continuation.kind !== 'saphoCharityDiscard' &&
|
||||
@@ -9294,6 +9330,12 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'saphoTruthOrderDiscard') {
|
||||
g.response = next.resume.response; g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama; g.phaseOpening = next.resume.phaseOpening;
|
||||
requireRule(next.truth.queue.every(row => queuedTruthCardMatches(g.players.find(player => player.id === row.player)?.hand ?? [],row)),
|
||||
'The pending Truthtrance cards must remain held before the order resumes.');
|
||||
g.truthtrance = next.truth;
|
||||
} else if (next.kind === 'saphoCharityDiscard') {
|
||||
g.response=next.resume.response;
|
||||
g.decision=next.resume.decision;
|
||||
@@ -26379,6 +26421,67 @@ function saphoExtortionDecision(g: Game) {
|
||||
decision?.kind === 'moritaniExtortion' && decision.player === state.queue[state.cursor] &&
|
||||
decision.event === extortionEvent(state);
|
||||
}
|
||||
/** Quote the exact public parent/discard continuation on a detached copy.
|
||||
* This does not inspect whether anyone holds Semuta; it prevents offering a
|
||||
* Sapho action whose mandatory supported reaction cannot be staged. */
|
||||
function saphoTruthOrderSemutaSupported(g: Game,p: Player,card: string,mode: 'first'|'last'): boolean {
|
||||
if (!g.semutaPreview) return true;
|
||||
const trial = structuredClone(g), actor = getPlayer(trial,p.id), w = trial.truthtrance;
|
||||
if (w?.stage !== 'order' || !w.order) return false;
|
||||
try {
|
||||
const event = w.order.event, revision = w.order.revision;
|
||||
const result = reorderSaphoTruthOrder(w.order,w.queue,{event,revision,player:p.id,mode});
|
||||
recordNativePrintedCardPlay(trial,p.id,'juiceOfSapho');
|
||||
const used = discard(trial,actor,card);
|
||||
w.queue = result.queue;
|
||||
if (result.done) { delete w.order; w.stage = 'ask'; } else w.order = result.frame;
|
||||
const c: SaphoTruthOrderDiscardContinuation = {kind:'saphoTruthOrderDiscard',player:p.id,card:used.id,
|
||||
event,revision,mode,truth:structuredClone(w),resume:{response:trial.response,decision:trial.decision,
|
||||
pendingKarama:trial.pendingKarama,phaseOpening:trial.phaseOpening},stateSignature:''};
|
||||
trial.truthtrance = null; trial.response = null; trial.decision = null;
|
||||
trial.pendingKarama = null; trial.phaseOpening = null;
|
||||
return semutaOfferSupported(trial,c,[{card:used,discardedBy:p.id,publicFace:true}]);
|
||||
} catch (error) { if (error instanceof RuleError || error instanceof SaphoTruthOrderError) return false; throw error; }
|
||||
}
|
||||
function saphoTruthOrderChoices(g: Game,p: Player): {card:string;mode:'first'|'last'}[] {
|
||||
const w = g.truthtrance;
|
||||
if (w?.stage !== 'order' || !w.order || (g.semutaPreview && !bareBasicSemutaProfile(g)) ||
|
||||
printedRolePlayBlock(g,p.id,'juiceOfSapho')) return [];
|
||||
const card = p.hand.find(row => row.id === 'richese-juice-of-sapho' && row.effect === 'juiceOfSapho');
|
||||
if (!card || !richeseCardDefinition(card) || p.hand.filter(row => row.id === card.id).length !== 1 ||
|
||||
transferCardBlock(g,p,card) || giftReserved(g,p.id,card.id)) return [];
|
||||
try { return quoteSaphoTruthOrder(w.order,w.queue,p.id).modes
|
||||
.filter(mode => saphoTruthOrderSemutaSupported(g,p,card.id,mode)).map(mode => ({card:card.id,mode})); }
|
||||
catch (error) { if (error instanceof SaphoTruthOrderError) return []; throw error; }
|
||||
}
|
||||
function playSaphoTruthOrder(g: Game,p: Player,action: Action) {
|
||||
const w = g.truthtrance;
|
||||
requireRule(w?.stage === 'order' && w.order &&
|
||||
Object.keys(action).sort().join(',') === 'card,event,mode,revision,type' &&
|
||||
saphoTruthOrderChoices(g,p).some(choice => choice.card === action.card && choice.mode === action.mode),
|
||||
'This Juice of Sapho ordering choice is unavailable or has expired.');
|
||||
let result;
|
||||
try { result = reorderSaphoTruthOrder(w.order,w.queue,{event:action.event as string,
|
||||
revision:action.revision as number,player:p.id,mode:action.mode as 'first'|'last'}); }
|
||||
catch (error) { if (error instanceof SaphoTruthOrderError) throw new RuleError(error.message); throw error; }
|
||||
recordNativePrintedCardPlay(g,p.id,'juiceOfSapho');
|
||||
const used = discard(g,p,action.card as string);
|
||||
w.queue = result.queue;
|
||||
if (result.done) { delete w.order; w.stage = 'ask'; } else w.order = result.frame;
|
||||
log(g, `${p.name} discarded Juice of Sapho to ask ${String(action.mode)} among the simultaneous Truthtrance holders.`,
|
||||
{faction:p.faction,name:'Juice of Sapho'});
|
||||
if (g.semutaPreview) {
|
||||
const c: SaphoTruthOrderDiscardContinuation = {kind:'saphoTruthOrderDiscard',player:p.id,card:used.id,
|
||||
event:action.event as string,revision:action.revision as number,mode:action.mode as 'first'|'last',
|
||||
truth:structuredClone(w),resume:{response:g.response,decision:g.decision,pendingKarama:g.pendingKarama,phaseOpening:g.phaseOpening},
|
||||
stateSignature:''};
|
||||
g.truthtrance = null; g.response = null; g.decision = null; g.pendingKarama = null; g.phaseOpening = null;
|
||||
const entries = [{card:used,discardedBy:p.id,publicFace:true}];
|
||||
requireRule(semutaOfferSupported(g,c,entries), 'This Sapho discard cannot safely suspend its Semuta reaction in this window.');
|
||||
c.stateSignature = saphoTruthOrderDiscardSignature(g,c);
|
||||
stageTreacheryDiscard(g,'sapho:truthOrder',entries,c);
|
||||
}
|
||||
}
|
||||
function saphoCleanWindow(g: Game) {
|
||||
return (
|
||||
g.status === 'playing' &&
|
||||
@@ -39931,6 +40034,7 @@ function applyActionInner(
|
||||
),
|
||||
'The card reserved for this Black Market auction cannot be played or queued for another effect.',
|
||||
);
|
||||
if (t === 'saphoTruthOrder') { playSaphoTruthOrder(g,p,action); return g; }
|
||||
try {
|
||||
if (
|
||||
resolveTruthAction(g, p, action, {
|
||||
@@ -43951,6 +44055,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
truthMixedBattleAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'mixedBattlePlan' ? mixedBattleTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
mixedBattlePromises: projectedMixedBattlePromises(g, id),
|
||||
saphoTruthOrderChoices: saphoTruthOrderChoices(g,me),
|
||||
truthBattleAnswers:
|
||||
g.truthtrance?.stage === 'answer' &&
|
||||
g.truthtrance.question?.target === id &&
|
||||
|
||||
@@ -502,6 +502,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'At a clean boundary after another player has begun shipment and movement, an unstarted holder may provisionally reorder only later unstarted turns. First means first after that current actor finishes; last remains after Guild. The current actor keeps its entire turn and original counters/resources. This does not split your own started turn or interrupt pending card/arrival/payment preparations. Guild retains its next-boundary timing choice. The panel names the actor who must finish.',
|
||||
'Between completed battles, choose first or last among remaining battle choosers. The priority lasts for the rest of the Battle phase. You still need a real unresolved battle. Others may choose battles against you before your own turn; last is not immunity. This scope changes the chooser, not each battle’s aggressor or tie advantage.',
|
||||
'In a fresh Richese battle, use unreserved Sapho at a clean boundary before both combatants finish final battle actions, including after sealed plans and revealed declarations. Habbanya still precedes ordinary aggressor tie priority. Participants, plans, promises, weapon choices and votes do not swap or replay. Older battles retain their pre-plan opportunity.',
|
||||
'When two or more players declared Truthtrance in a supported clean parent, each queued holder may continue or use their held Sapho to move their entire pending question group first or last. Physical storm order and each holder’s card order stay unchanged. Supported Basic Semuta claim/pass restores the same question queue. Auction, battle and pending-parent combinations remain unfinished.',
|
||||
'During a clean native Extortion payer decision, use Sapho to take your optional choice first/last among unanswered non-Moritani players. Earlier declines, collected bank5 and the one-payment3 limit remain. The next payer event changes even when that player stays next. You may decline instead of paying.',
|
||||
'During ordered Advanced Charity, take your remaining turn first or last, then discard Sapho. An accepted current claim provisionally protects that actor while another holder reorders only later unstarted turns. Each player still claims at most once, completed turns stay closed, and the holder cannot split its own started turn. Physical First Player order is unchanged. Basic and older concurrent phases have no ordered scope.',
|
||||
'During ordered Advanced Revival, take your unfinished opportunity first or last, then discard Sapho. If the current player has already begun a native return or negotiation, provisional queued timing lets that player finish unchanged; first means first among the later unstarted turns. You cannot split your own started turn, repeat a completed turn or consume the card for a position you already hold. Basic and older concurrent Revival phases have no ordered scope.',
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/** Public pre-question opportunity for groups of simultaneous Truth cards.
|
||||
* The engine owns the physical Sapho and each queued Truth card. */
|
||||
export type SaphoTruthOrderFrame = {
|
||||
event: string;
|
||||
revision: number;
|
||||
holders: string[];
|
||||
passed: string[];
|
||||
/** Earlier Sapho choices remain in their declared boundary positions. */
|
||||
protectedFirst?: string;
|
||||
protectedLast?: string;
|
||||
};
|
||||
|
||||
export type SaphoTruthOrderEntry = { player: string };
|
||||
export type SaphoTruthOrderMode = 'first' | 'last';
|
||||
export type SaphoTruthOrderRequest = { event: string; revision: number; player: string };
|
||||
export type SaphoTruthOrderResult<T extends SaphoTruthOrderEntry> = {
|
||||
frame: SaphoTruthOrderFrame;
|
||||
queue: T[];
|
||||
done: boolean;
|
||||
};
|
||||
|
||||
export class SaphoTruthOrderError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'SaphoTruthOrderError';
|
||||
}
|
||||
}
|
||||
|
||||
function check(value: unknown, message: string): asserts value {
|
||||
if (!value) throw new SaphoTruthOrderError(message);
|
||||
}
|
||||
const identifier = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.length > 0 && value.trim() === value;
|
||||
const uniqueIdentifiers = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every(identifier) && new Set(value).size === value.length;
|
||||
|
||||
/** The queue is already sorted by the real Truth priority rule. Each holder's
|
||||
* physical cards must remain one contiguous group throughout this window. */
|
||||
function holders<T extends SaphoTruthOrderEntry>(queue: readonly T[]): string[] {
|
||||
check(Array.isArray(queue) && queue.every(row => row && identifier(row.player)),
|
||||
'Retain the original queued Truth holders.');
|
||||
const result: string[] = [];
|
||||
for (const row of queue) {
|
||||
if (result.at(-1) === row.player) continue;
|
||||
check(!result.includes(row.player), 'A holder’s simultaneous Truth cards must remain together.');
|
||||
result.push(row.player);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Creates a frame solely from public committed holders, not any hand or card
|
||||
* contents. No added response stage is needed for fewer than two holders. */
|
||||
export function createSaphoTruthOrder<T extends SaphoTruthOrderEntry>(
|
||||
queue: readonly T[], event: string,
|
||||
): SaphoTruthOrderFrame | null {
|
||||
check(identifier(event), 'The Truth ordering window needs an opaque event.');
|
||||
const originalHolders = holders(queue);
|
||||
return originalHolders.length < 2 ? null : { event, revision: 0, holders: originalHolders, passed: [] };
|
||||
}
|
||||
|
||||
export function validateSaphoTruthOrder<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[],
|
||||
): void {
|
||||
check(frame && typeof frame === 'object' && identifier(frame.event) &&
|
||||
Number.isSafeInteger(frame.revision) && frame.revision >= 0,
|
||||
'Retain the current Truth ordering event and revision.');
|
||||
check(uniqueIdentifiers(frame.holders) && frame.holders.length >= 2 && uniqueIdentifiers(frame.passed) &&
|
||||
frame.passed.every(player => frame.holders.includes(player)),
|
||||
'Retain the original holders and their completed ordering responses.');
|
||||
const current = holders(queue);
|
||||
check(current.length === frame.holders.length && current.every(player => frame.holders.includes(player)),
|
||||
'Retain every original Truth holder in this ordering window.');
|
||||
check(frame.revision === frame.passed.length,
|
||||
'Retain one ordering revision for each completed holder response.');
|
||||
check(frame.protectedFirst === undefined || identifier(frame.protectedFirst) &&
|
||||
frame.passed.includes(frame.protectedFirst) && current[0] === frame.protectedFirst,
|
||||
'Retain the holder protected at the first Truth position.');
|
||||
check(frame.protectedLast === undefined || identifier(frame.protectedLast) &&
|
||||
frame.passed.includes(frame.protectedLast) && current.at(-1) === frame.protectedLast,
|
||||
'Retain the holder protected at the last Truth position.');
|
||||
}
|
||||
|
||||
function participant<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[], request: SaphoTruthOrderRequest,
|
||||
): string[] {
|
||||
validateSaphoTruthOrder(frame, queue);
|
||||
check(request && request.event === frame.event && request.revision === frame.revision,
|
||||
'This Truth ordering opportunity has expired.');
|
||||
check(identifier(request.player) && frame.holders.includes(request.player),
|
||||
'Only an original Truth holder may respond to this ordering window.');
|
||||
check(!frame.passed.includes(request.player), 'This holder already completed the ordering opportunity.');
|
||||
return holders(queue);
|
||||
}
|
||||
|
||||
export function quoteSaphoTruthOrder<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[], player: string,
|
||||
): { event: string; revision: number; modes: SaphoTruthOrderMode[] } {
|
||||
validateSaphoTruthOrder(frame, queue);
|
||||
const {event,revision} = frame, current = participant(frame, queue, { event, revision, player });
|
||||
const modes: SaphoTruthOrderMode[] = [];
|
||||
if (current[0] !== player && !frame.protectedFirst && frame.protectedLast !== player) modes.push('first');
|
||||
if (current.at(-1) !== player && !frame.protectedLast && frame.protectedFirst !== player) modes.push('last');
|
||||
return { event, revision, modes };
|
||||
}
|
||||
|
||||
function complete<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[], player: string,
|
||||
): SaphoTruthOrderResult<T> {
|
||||
check(frame.revision < Number.MAX_SAFE_INTEGER, 'The Truth ordering revision cannot advance safely.');
|
||||
const next = { ...frame, revision: frame.revision + 1, holders: [...frame.holders], passed: [...frame.passed, player] };
|
||||
return { frame: next, queue: [...queue], done: next.passed.length === next.holders.length };
|
||||
}
|
||||
|
||||
/** Passing is available to every original holder, independently of cards held. */
|
||||
export function passSaphoTruthOrder<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[], request: SaphoTruthOrderRequest,
|
||||
): SaphoTruthOrderResult<T> {
|
||||
participant(frame, queue, request);
|
||||
return complete(frame, queue, request.player);
|
||||
}
|
||||
|
||||
/** Move the entire holder group; never sort or alter the physical card rows. */
|
||||
export function reorderSaphoTruthOrder<T extends SaphoTruthOrderEntry>(
|
||||
frame: SaphoTruthOrderFrame, queue: readonly T[], request: SaphoTruthOrderRequest & { mode: SaphoTruthOrderMode },
|
||||
): SaphoTruthOrderResult<T> {
|
||||
const current = participant(frame, queue, request);
|
||||
check(request.mode === 'first' || request.mode === 'last', 'Choose first or last.');
|
||||
check(request.mode === 'first' ? current[0] !== request.player : current.at(-1) !== request.player,
|
||||
'The holder already occupies that Truth position.');
|
||||
check(request.mode === 'first' ? !frame.protectedFirst && frame.protectedLast !== request.player
|
||||
: !frame.protectedLast && frame.protectedFirst !== request.player,
|
||||
'An earlier Juice of Sapho use protects that Truth position.');
|
||||
const own = queue.filter(row => row.player === request.player), rest = queue.filter(row => row.player !== request.player);
|
||||
const result = complete(frame, request.mode === 'first' ? [...own, ...rest] : [...rest, ...own], request.player);
|
||||
if (request.mode === 'first') result.frame.protectedFirst = request.player;
|
||||
else result.frame.protectedLast = request.player;
|
||||
return result;
|
||||
}
|
||||
+28
-2
@@ -1,3 +1,4 @@
|
||||
import { createSaphoTruthOrder, passSaphoTruthOrder, SaphoTruthOrderError, type SaphoTruthOrderFrame } from './sapho-truth-order';
|
||||
import { parseTravelActionClaim, type TravelClaim } from './travel-expression-promises';
|
||||
import { parseTurnActionLeaf, TURN_ACTION_MAX_DEPTH, TURN_ACTION_MAX_LEAVES,
|
||||
turnActionPromiseModeSupported, type TurnActionClaim } from './turn-action-promises';
|
||||
@@ -126,7 +127,8 @@ export type TruthQueueEntry = {
|
||||
source?: 'shrine';
|
||||
};
|
||||
export type TruthWindow = {
|
||||
stage: 'priority' | 'ask' | 'answer' | 'unknown';
|
||||
stage: 'priority' | 'order' | 'ask' | 'answer' | 'unknown';
|
||||
order?: SaphoTruthOrderFrame;
|
||||
queue: TruthQueueEntry[];
|
||||
passed: string[];
|
||||
question: TruthQuestion | null;
|
||||
@@ -901,6 +903,19 @@ export function resolveTruthAction(
|
||||
cardBlock: (g: Game, p: Player, card: Player['hand'][number]) => string | null;
|
||||
},
|
||||
): boolean {
|
||||
if (g.truthtrance?.stage === 'order') {
|
||||
const w = g.truthtrance;
|
||||
check(w.order && a.type === 'truthOrderPass' &&
|
||||
Object.keys(a).sort().join(',') === 'event,revision,type',
|
||||
'Finish the current Truthtrance ordering opportunity.');
|
||||
try {
|
||||
const result = passSaphoTruthOrder(w.order,w.queue,{event:a.event as string,revision:a.revision as number,player:p.id});
|
||||
w.queue = result.queue;
|
||||
if (result.done) { delete w.order; w.stage = 'ask'; }
|
||||
else w.order = result.frame;
|
||||
} catch (error) { if (error instanceof SaphoTruthOrderError) throw new TruthError(error.message); throw error; }
|
||||
return true;
|
||||
}
|
||||
const selectedCard =
|
||||
a.type === 'card' && typeof a.card === 'string'
|
||||
? p.hand.find((card) => card.id === a.card)
|
||||
@@ -1271,7 +1286,18 @@ export function resolveTruthAction(
|
||||
g.truthtrance.queue.sort(
|
||||
(a, b) => order.indexOf(a.player) - order.indexOf(b.player),
|
||||
);
|
||||
g.truthtrance.stage = 'ask';
|
||||
// Every committed holder gets the same public response window. The
|
||||
// decision to open it never inspects who holds Juice of Sapho.
|
||||
const clean = g.status === 'playing' && g.expansions.includes('choam') && !g.response && !g.decision && !g.phaseOpening &&
|
||||
!g.pendingKarama && !g.pendingTreacheryDiscard && !g.pendingNullentropy && !g.auction &&
|
||||
!g.richeseAuction && !g.battle && !g.pendingRevival && !g.pendingShipment &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingChoamMove &&
|
||||
!g.pendingIxMove && !g.pendingFremenMove && !g.pendingExchange && !g.pendingRicheseGift &&
|
||||
!g.karamaShipping && !g.ornithopter && !g.summonedWorm && g.wormRides.length === 0 &&
|
||||
!g.pendingArrivalOverlap && !g.pendingArrivalReaction?.length;
|
||||
const frame = clean ? createSaphoTruthOrder(g.truthtrance.queue,crypto.randomUUID()) : null;
|
||||
if (frame) { g.truthtrance.order = frame; g.truthtrance.stage = 'order'; }
|
||||
else g.truthtrance.stage = 'ask';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user