Support Sapho ordering for shared arrivals
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
b18a767470
commit
e457478f7d
18 files changed
+808
-23
No files matched your search
+197
-2
@@ -20,6 +20,8 @@ import { mobileStrongholdCollectionCapacity } from './mobile-stronghold-collecti
|
||||
import { basicKullTechRoster, kullDefaultEligible } from './kull-default-profile';
|
||||
import { advancedKullLobbyEligible } from './kull-advanced-profile';
|
||||
import { SaphoTruthOrderError, quoteSaphoTruthOrder, reorderSaphoTruthOrder, validateSaphoTruthOrder } from './sapho-truth-order';
|
||||
import { createSaphoArrivalOrder, validateSaphoArrivalOrder, quoteSaphoArrivalOrder,
|
||||
passSaphoArrivalOrder, reorderSaphoArrivalOrder, type SaphoArrivalOrder } from './sapho-arrival-order';
|
||||
import { RicheseBidPromiseError, richeseBidCompletionRange, richeseBidRequired, recordRicheseBid,
|
||||
liveRicheseBidPromises, reconcileRicheseBidPromises, finishRicheseBidLot, validateRicheseBidPromises,
|
||||
type RicheseBidClaim, type RicheseBidPromise, type RicheseBidOpportunity } from './richese-bid-promises';
|
||||
@@ -1722,6 +1724,16 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoArrivalOrderDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
event: string;
|
||||
revision: number;
|
||||
mode: 'first' | 'last';
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoMovementDiscard';
|
||||
player: string;
|
||||
@@ -2285,6 +2297,8 @@ export type Game = {
|
||||
moritani: string;
|
||||
first: 'ambassador' | 'terror';
|
||||
stage: 'first' | 'second';
|
||||
/** Present only for freshly opened public Sapho ordering windows. */
|
||||
saphoOrder?: SaphoArrivalOrder;
|
||||
ambassadorToken: string;
|
||||
terrorTokens: string[];
|
||||
ambassadorEvent?: string;
|
||||
@@ -6357,6 +6371,14 @@ function distransDiscardSignature(g: Game, c: DistransDiscardContinuation) {
|
||||
type SaphoTruthOrderDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'], { kind: 'saphoTruthOrderDiscard' }
|
||||
>;
|
||||
type SaphoArrivalOrderDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'], { kind: 'saphoArrivalOrderDiscard' }
|
||||
>;
|
||||
function saphoArrivalOrderDiscardSignature(g: Game, c: SaphoArrivalOrderDiscardContinuation) {
|
||||
return JSON.stringify({ player: c.player, card: c.card, event: c.event, revision: c.revision,
|
||||
mode: c.mode, overlap: g.pendingArrivalOverlap, turn: g.turn, phase: g.phase,
|
||||
active: g.active, order: g.order, movementRemaining: g.movementRemaining });
|
||||
}
|
||||
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,
|
||||
@@ -8194,6 +8216,16 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === distransDiscardSignature(g, c),
|
||||
'The completed private Distrans transfer no longer matches its fresh discard.',
|
||||
);
|
||||
} else if (continuation?.kind === 'saphoArrivalOrderDiscard') {
|
||||
const c = continuation, entry = batch.entries[0], overlap = g.pendingArrivalOverlap;
|
||||
requireRule(batch.cause === 'sapho:arrivalOrder' && batch.entries.length === 1 && entry.publicFace &&
|
||||
entry.discardedBy === c.player && entry.card.id === c.card &&
|
||||
richeseCardDefinition(entry.card)?.card.effect === 'juiceOfSapho' &&
|
||||
arrivalOrderWaiting(g) && overlap?.saphoOrder?.frame.event === c.event &&
|
||||
overlap.saphoOrder.frame.revision === c.revision + 1 && overlap.saphoOrder.frame.passed.includes(c.player) &&
|
||||
!c.resume.response && !c.resume.decision && !c.resume.pendingKarama && !c.resume.phaseOpening &&
|
||||
c.stateSignature === saphoArrivalOrderDiscardSignature(g, c),
|
||||
'The used Sapho no longer matches its suspended arrival order.');
|
||||
} 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 &&
|
||||
@@ -9481,6 +9513,16 @@ function semutaOfferSupported(
|
||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
entries: FreshDiscardBatch['entries'],
|
||||
) {
|
||||
if (continuation.kind === 'saphoArrivalOrderDiscard') {
|
||||
const order = g.pendingArrivalOverlap?.saphoOrder;
|
||||
return g.semutaPreview === true && bareBasicSemutaProfile(g) && g.phase === 5 &&
|
||||
arrivalOrderUnopened(g) && !!order && order.frame.event === continuation.event &&
|
||||
order.frame.revision === continuation.revision + 1 && order.frame.passed.includes(continuation.player) &&
|
||||
entries.length === 1 && entries[0].publicFace && entries[0].discardedBy === continuation.player &&
|
||||
entries[0].card.id === continuation.card && entries[0].card.effect === 'juiceOfSapho' &&
|
||||
!continuation.resume.response && !continuation.resume.decision &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening;
|
||||
}
|
||||
if (continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'recruits')
|
||||
return g.semutaRecruitsPreview === true && basicIndependentRecruitsSemutaProfile(g) &&
|
||||
g.status === 'playing' && g.phase === 4 && g.recruits?.turn === g.turn &&
|
||||
@@ -10195,6 +10237,10 @@ 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 === 'saphoArrivalOrderDiscard') {
|
||||
g.response = next.resume.response; g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama; g.phaseOpening = next.resume.phaseOpening;
|
||||
if (g.pendingArrivalOverlap?.saphoOrder?.frame.passed.length === 2) openFirstOrderedArrival(g);
|
||||
} 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;
|
||||
@@ -24199,11 +24245,30 @@ function arrivalOverlapModeSupported(g: Game, revivalArrival?: RevivalEntryParen
|
||||
!g.homeworlds && !g.nexusCards && !g.leaderSkills &&
|
||||
!g.discoveryEnabled && !g.techTokens && !g.strongholdCards);
|
||||
}
|
||||
function arrivalOrderUnopened(g: Game): boolean {
|
||||
const overlap = g.pendingArrivalOverlap;
|
||||
return !!overlap?.saphoOrder && overlap.stage === 'first' &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !overlap.ambassadorEvent;
|
||||
}
|
||||
function arrivalOrderWaiting(g: Game): boolean {
|
||||
return arrivalOrderUnopened(g) &&
|
||||
(g.pendingArrivalOverlap!.saphoOrder!.frame.passed.length < 2 ||
|
||||
g.pendingTreacheryDiscard?.continuation.kind === 'saphoArrivalOrderDiscard');
|
||||
}
|
||||
function arrivalOverlapIntegrity(g: Game) {
|
||||
const record = g.pendingArrivalOverlap;
|
||||
if (!record) return;
|
||||
const ecazIndex = g.order.indexOf(record.ecaz);
|
||||
const moritaniIndex = g.order.indexOf(record.moritani);
|
||||
const originalOwners: [string, string] = ecazIndex < moritaniIndex
|
||||
? [record.ecaz, record.moritani] : [record.moritani, record.ecaz];
|
||||
const selectedFirst = record.first === 'ambassador' ? record.ecaz : record.moritani;
|
||||
if (record.saphoOrder) try {
|
||||
validateSaphoArrivalOrder(record.saphoOrder, record.event, originalOwners, selectedFirst);
|
||||
} catch (error) {
|
||||
if (error instanceof SaphoTruthOrderError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
}
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const ambassador = g.pendingAmbassador ??
|
||||
(continuation?.kind === 'ambassador' ? continuation.entry : null);
|
||||
@@ -24228,7 +24293,7 @@ function arrivalOverlapIntegrity(g: Game) {
|
||||
ecazIndex >= 0 && moritaniIndex >= 0 && ecazIndex !== moritaniIndex &&
|
||||
g.order.lastIndexOf(record.ecaz) === ecazIndex &&
|
||||
g.order.lastIndexOf(record.moritani) === moritaniIndex &&
|
||||
record.first === (ecazIndex < moritaniIndex ? 'ambassador' : 'terror') &&
|
||||
(!!record.saphoOrder || record.first === (ecazIndex < moritaniIndex ? 'ambassador' : 'terror')) &&
|
||||
(record.stage === 'first' || record.stage === 'second') &&
|
||||
Number.isSafeInteger(record.sector) &&
|
||||
TERROR_STRONGHOLDS.includes(record.territory) &&
|
||||
@@ -24241,7 +24306,14 @@ function arrivalOverlapIntegrity(g: Game) {
|
||||
Array.isArray(record.terrorTokens) && record.terrorTokens.length > 0 &&
|
||||
record.terrorTokens.every(id => typeof id === 'string' && id.length > 0) &&
|
||||
new Set(record.terrorTokens).size === record.terrorTokens.length &&
|
||||
(active === 'ambassador'
|
||||
(arrivalOrderWaiting(g) || !record.saphoOrder || record.saphoOrder.frame.passed.length === 2) &&
|
||||
(arrivalOrderWaiting(g)
|
||||
? !g.response && !g.decision && !g.phaseOpening && !g.truthtrance &&
|
||||
!!g.ecazAmbassadors?.tokens.some(token => token.id === record.ambassadorToken && token.zone === 'placed' && token.location === record.territory) &&
|
||||
record.terrorTokens.every(id => g.moritaniTerror?.tokens.some(token => token.id === id && token.status === 'placed' && token.location === record.territory)) &&
|
||||
(record.saphoOrder!.frame.passed.length < 2 ||
|
||||
g.pendingTreacheryDiscard?.continuation.kind === 'saphoArrivalOrderDiscard')
|
||||
: active === 'ambassador'
|
||||
? !!ambassador && (!terror || ambassadorTerrorEntryMatches(g, terror)) &&
|
||||
ambassador.event === record.ambassadorEvent &&
|
||||
ambassador.owner === record.ecaz &&
|
||||
@@ -24316,6 +24388,25 @@ function finishArrivalOverlap(g: Game, overlap: NonNullable<Game['pendingArrival
|
||||
log(g, 'Both optional Ambassador and Terror reactions to the original entry have finished.');
|
||||
if (overlap.resume === 'wormRide') nextWormRide(g);
|
||||
}
|
||||
/** Open the first original reaction once its public ordering window is complete. */
|
||||
function openFirstOrderedArrival(g: Game) {
|
||||
const overlap = g.pendingArrivalOverlap;
|
||||
requireRule(overlap && arrivalOrderUnopened(g) && overlap.saphoOrder?.frame.passed.length === 2 &&
|
||||
!g.pendingTreacheryDiscard, 'Finish the public arrival order before opening its first reaction.');
|
||||
const entrant = getPlayer(g, overlap.entrant), owner = getPlayer(g, overlap.ecaz);
|
||||
const token = g.ecazAmbassadors?.tokens.find(item => item.id === overlap.ambassadorToken &&
|
||||
item.zone === 'placed' && item.location === overlap.territory);
|
||||
requireRule(token, 'The original Ambassador must remain at the arrival.');
|
||||
if (overlap.first === 'terror') {
|
||||
requireRule(openTerrorEntry(g, entrant, overlap.territory, overlap.sector, overlap.amount,
|
||||
overlap.elite, overlap.cause, 'none', undefined, overlap.revivalArrival, overlap.caladanArrival),
|
||||
'The original Terror opportunity must remain available.');
|
||||
} else {
|
||||
openAmbassadorEntry(g, owner, token, entrant, overlap.territory, overlap.sector,
|
||||
'none', overlap.cause, overlap.revivalArrival, overlap.caladanArrival);
|
||||
overlap.ambassadorEvent = g.pendingAmbassador!.event;
|
||||
}
|
||||
}
|
||||
/** The original entry is committed once; eligible overlapping owners act in storm order. */
|
||||
function openTerritoryEntry(
|
||||
g: Game,
|
||||
@@ -24405,8 +24496,18 @@ function openTerritoryEntry(
|
||||
...(revivalArrival ? {revivalArrival:structuredClone(revivalArrival)} : {}),
|
||||
...(caladanArrival ? {caladanArrival:structuredClone(caladanArrival)} : {}),
|
||||
};
|
||||
const saphoArrival = g.advanced === false && g.phase === 5 && g.semutaPreview === true &&
|
||||
bareBasicSemutaProfile(g) && resume === 'none' && (cause === 'shipment' || cause === 'movement') &&
|
||||
!g.pendingArrivalReaction?.length && !g.pendingChoamMove && !g.pendingIxMove && !g.pendingFremenMove &&
|
||||
!g.ornithopter && !g.summonedWorm && g.wormRides.length === 0 && !revivalArrival && !caladanArrival;
|
||||
if (saphoArrival) overlap.saphoOrder = createSaphoArrivalOrder(overlap.event,
|
||||
first === 'ambassador' ? owner.id : moritani.id, first === 'ambassador' ? moritani.id : owner.id);
|
||||
overlap.signature = arrivalOverlapSignature(overlap);
|
||||
g.pendingArrivalOverlap = overlap;
|
||||
if (overlap.saphoOrder) {
|
||||
log(g, `${owner.name} and ${moritani.name} may adjust their provisional reaction order before either acts.`);
|
||||
return true;
|
||||
}
|
||||
log(g, `${owner.name} and ${moritani.name} both have optional reactions to ${entrant.name}'s entry in ${territory(to).name}; ${first === 'ambassador' ? owner.name : moritani.name} acts first in storm order.`);
|
||||
if (first === 'terror')
|
||||
return openTerrorEntry(g, entrant, to, sector, amount, elite, cause, 'none', undefined, revivalArrival, caladanArrival);
|
||||
@@ -28465,6 +28566,81 @@ function saphoBattleCardReserved(g: Game, player: string, card: string): boolean
|
||||
return !!battle && (!!plan && [plan.weapon, plan.defense, plan.leader].includes(card) ||
|
||||
committedPlanElements(battle, player).some(element => element.value === card));
|
||||
}
|
||||
function saphoArrivalOrderChoices(g: Game, p: Player): {card: string; mode: 'first' | 'last'}[] {
|
||||
const overlap = g.pendingArrivalOverlap, order = overlap?.saphoOrder;
|
||||
if (!arrivalOrderWaiting(g) || !order || g.pendingTreacheryDiscard ||
|
||||
printedRolePlayBlock(g, p.id, 'juiceOfSapho')) return [];
|
||||
const card = p.hand.find(item => item.id === 'richese-juice-of-sapho' && item.effect === 'juiceOfSapho');
|
||||
if (!card || !richeseCardDefinition(card) || p.hand.filter(item => item.id === card.id).length !== 1 ||
|
||||
transferCardBlock(g, p, card) || giftReserved(g, p.id, card.id) || saphoBattleCardReserved(g, p.id, card.id)) return [];
|
||||
try {
|
||||
return quoteSaphoArrivalOrder(order, p.id).modes.filter(mode => {
|
||||
const trial = structuredClone(g), actor = getPlayer(trial, p.id), parent = trial.pendingArrivalOverlap!;
|
||||
const result = reorderSaphoArrivalOrder(parent.saphoOrder!, {
|
||||
event: order.frame.event, revision: order.frame.revision, player: p.id, mode,
|
||||
});
|
||||
parent.saphoOrder = result.order;
|
||||
parent.first = result.order.queue[0].player === parent.ecaz ? 'ambassador' : 'terror';
|
||||
parent.signature = arrivalOverlapSignature(parent);
|
||||
recordNativePrintedCardPlay(trial, p.id, 'juiceOfSapho');
|
||||
const used = discard(trial, actor, card.id);
|
||||
guardTurnActionPrintedCardCost(trial, actor, used);
|
||||
const continuation: SaphoArrivalOrderDiscardContinuation = {
|
||||
kind: 'saphoArrivalOrderDiscard', player: p.id, card: used.id, event: order.frame.event,
|
||||
revision: order.frame.revision, mode,
|
||||
resume: { response: trial.response, decision: trial.decision, pendingKarama: trial.pendingKarama, phaseOpening: trial.phaseOpening },
|
||||
stateSignature: '',
|
||||
};
|
||||
return semutaOfferSupported(trial, continuation, [{card: used, discardedBy: p.id, publicFace: true}]);
|
||||
}).map(mode => ({card: card.id, mode}));
|
||||
} catch (error) {
|
||||
if (error instanceof RuleError || error instanceof SaphoTruthOrderError) return [];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function decideSaphoArrivalOrder(g: Game, p: Player, action: Action) {
|
||||
const overlap = g.pendingArrivalOverlap, order = overlap?.saphoOrder;
|
||||
requireRule(arrivalOrderWaiting(g) && overlap && order && !g.pendingTreacheryDiscard,
|
||||
'This arrival-order opportunity is no longer open.');
|
||||
const request = { event: action.event as string, revision: action.revision as number, player: p.id };
|
||||
let result;
|
||||
try {
|
||||
if (action.type === 'arrivalOrderPass') {
|
||||
requireRule(Object.keys(action).sort().join(',') === 'event,revision,type', 'Choose only the current arrival order.');
|
||||
result = passSaphoArrivalOrder(order, request);
|
||||
} else {
|
||||
requireRule(action.type === 'saphoArrivalOrder' && Object.keys(action).sort().join(',') === 'card,event,mode,revision,type' &&
|
||||
saphoArrivalOrderChoices(g, p).some(choice => choice.card === action.card && choice.mode === action.mode),
|
||||
'This Sapho arrival-order choice is unavailable or has expired.');
|
||||
result = reorderSaphoArrivalOrder(order, {...request, mode: action.mode as 'first' | 'last'});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SaphoTruthOrderError) throw new RuleError(error.message);
|
||||
throw error;
|
||||
}
|
||||
overlap.saphoOrder = result.order;
|
||||
overlap.first = result.order.queue[0].player === overlap.ecaz ? 'ambassador' : 'terror';
|
||||
overlap.signature = arrivalOverlapSignature(overlap);
|
||||
if (action.type === 'arrivalOrderPass') {
|
||||
if (result.done) openFirstOrderedArrival(g);
|
||||
return;
|
||||
}
|
||||
recordNativePrintedCardPlay(g, p.id, 'juiceOfSapho');
|
||||
const used = discard(g, p, action.card as string);
|
||||
guardTurnActionPrintedCardCost(g, p, used);
|
||||
log(g, `${p.name} discarded Juice of Sapho to react ${String(action.mode)} in the shared arrival.`,
|
||||
{ faction: p.faction, name: 'Juice of Sapho' });
|
||||
const continuation: SaphoArrivalOrderDiscardContinuation = {
|
||||
kind: 'saphoArrivalOrderDiscard', player: p.id, card: used.id, event: request.event,
|
||||
revision: request.revision, mode: action.mode as 'first' | 'last',
|
||||
resume: { response: g.response, decision: g.decision, pendingKarama: g.pendingKarama, phaseOpening: g.phaseOpening },
|
||||
stateSignature: '',
|
||||
};
|
||||
const entries = [{card: used, discardedBy: p.id, publicFace: true}];
|
||||
requireRule(semutaOfferSupported(g, continuation, entries), 'This arrival-order discard cannot safely resume its Semuta reaction.');
|
||||
continuation.stateSignature = saphoArrivalOrderDiscardSignature(g, continuation);
|
||||
stageTreacheryDiscard(g, 'sapho:arrivalOrder', entries, continuation);
|
||||
}
|
||||
/** 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. */
|
||||
@@ -41845,6 +42021,15 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
|
||||
);
|
||||
return normalizeAutomaticGame(state);
|
||||
}
|
||||
if (arrivalOrderWaiting(state)) {
|
||||
getPlayer(state, id);
|
||||
if (action?.type === 'advanceBots') return state;
|
||||
requireRule(action?.type === 'arrivalOrderPass' || action?.type === 'saphoArrivalOrder',
|
||||
'Finish the shared arrival order before another action.');
|
||||
const game = structuredClone(state);
|
||||
decideSaphoArrivalOrder(game, getPlayer(game, id), action);
|
||||
return game.pendingTreacheryDiscard?.reaction ? game : normalizeAutomaticGame(game);
|
||||
}
|
||||
if (action.alliancePromiseEvent !== undefined) {
|
||||
const guide = projectedAlliancePromiseGuide(state, id);
|
||||
requireRule(guide && Object.keys(action).sort().join(',') === Object.keys(guide.action).sort().join(',') &&
|
||||
@@ -42113,6 +42298,7 @@ function finishActionContinuations(g: Game) {
|
||||
finishTreacheryDiscard(g);
|
||||
if (g.pendingTreacheryDiscard?.reaction) return;
|
||||
requireRule(!g.pendingTreacheryDiscard, 'The automatic discard chain did not finish.');
|
||||
if (arrivalOrderWaiting(g)) return;
|
||||
if (g.pendingNexusEmperorPayment) return;
|
||||
if (g.pendingNexusEmperorSellerPurchase || g.pendingAmbassador?.nexusPurchase) return;
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
@@ -42170,6 +42356,7 @@ function finishActionContinuations(g: Game) {
|
||||
finishMoritaniPlacement(g);
|
||||
}
|
||||
function settleAutomaticContinuations(g: Game) {
|
||||
if (arrivalOrderWaiting(g)) return;
|
||||
if (g.victoryContinuation) return;
|
||||
if (g.pendingNexusHarkonnenBetrayal) return;
|
||||
if (g.pendingNexusIxianBetrayal) return;
|
||||
@@ -42182,6 +42369,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
if (g.pendingNullentropy) return;
|
||||
for (let iteration = 0; iteration < 128; iteration++) {
|
||||
if (arrivalOrderWaiting(g)) return;
|
||||
if (g.victoryContinuation) return;
|
||||
if (g.pendingNexusHarkonnenBetrayal) return;
|
||||
if (g.pendingNexusIxianBetrayal) return;
|
||||
@@ -47178,6 +47366,13 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
allyAtActivation: g.moritaniAtomics.allyAtActivation }
|
||||
: null,
|
||||
arrivalOverlapMode: arrivalOverlapModeSupported(g),
|
||||
saphoArrivalOrder: arrivalOrderWaiting(g) && !g.pendingTreacheryDiscard
|
||||
? { event: g.pendingArrivalOverlap!.saphoOrder!.frame.event,
|
||||
revision: g.pendingArrivalOverlap!.saphoOrder!.frame.revision,
|
||||
order: g.pendingArrivalOverlap!.saphoOrder!.queue.map(item => item.player),
|
||||
passed: [...g.pendingArrivalOverlap!.saphoOrder!.frame.passed],
|
||||
choices: saphoArrivalOrderChoices(g, me) }
|
||||
: null,
|
||||
arrivalOverlap: g.pendingArrivalOverlap
|
||||
? {
|
||||
first: g.pendingArrivalOverlap.first,
|
||||
|
||||
Reference in new issue
Block a user