Protect Sapho priority at the Testing Station
Verify and publish checkpoint container / container (push) Successful in 11m16s

This commit is contained in:
Quintine committed 2026-10-30 17:35:55 +11:00
1 parent f73a6e0a9c
commit a6be17fc5d
12 files changed
+391 -9

No files matched your search

+44 -1
View File
@@ -1674,6 +1674,8 @@ export type Game = {
} | null;
stormMovementSource?: StormMovementSource;
ecologicalStorm?: DiscoveryStorm;
/** Neutral opportunity before either of two Station occupants chooses. */
saphoStationPriority?: { event: string; passed: string[] };
discoveryEnabled?: boolean;
discoveries?: DiscoveryState;
discoveryStash?: DiscoveryStash;
@@ -14119,6 +14121,9 @@ function startStormMovement(g: Game, n: number) {
if (offer && !offer.blocked) {
g.ecologicalStorm = nexusRule(() => createDiscoveryStorm(g,crypto.randomUUID()));
g.decision = {kind:'ecologicalStorm',player:g.ecologicalStorm.owner,event:g.ecologicalStorm.event};
if (g.ecologicalStorm.queue?.order.length === 2 && byFaction(g, 'richese') &&
!g.semutaAdvancedOrdinaryPreview && !g.semutaRecruitsPreview)
g.saphoStationPriority = {event:g.ecologicalStorm.event, passed:[]};
} else moveStorm(g,n);
}
function choamStormOptions(g: Game) {
@@ -14653,6 +14658,16 @@ function discoveryIntegrity(g: Game) {
}
function ecologicalStormIntegrity(g: Game) {
const frame = g.ecologicalStorm, continuation = g.pendingTreacheryDiscard?.continuation;
if (g.saphoStationPriority) {
const priority = g.saphoStationPriority;
requireRule(frame?.stage === 'choose' && frame.event === priority.event &&
frame.queue?.order.length === 2 && frame.queue.choices.length === 0 &&
g.decision?.kind === 'ecologicalStorm' && g.decision.event === priority.event &&
Array.isArray(priority.passed) && priority.passed.length < 2 &&
new Set(priority.passed).size === priority.passed.length &&
priority.passed.every(id => frame.queue!.order.includes(id)),
'The Testing Station order opportunity has lost its original occupants.');
}
requireRule(!(g.status === 'playing' && g.phase === 0 && g.advanced && g.turn > 1 &&
g.stormDialers.length === 0 && g.stormPending !== null) || !!g.stormMovementSource,
'The revealed Advanced storm has lost its original movement source.');
@@ -29849,6 +29864,8 @@ function saphoOptions(g: Game, p: Player): SaphoOption[] {
const options: SaphoOption[] = [];
if (saphoTestingStationDecision(g)) {
const frame = g.ecologicalStorm!;
if (g.saphoStationPriority &&
(!frame.queue?.order.includes(p.id) || g.saphoStationPriority.passed.includes(p.id))) return options;
for (const mode of nexusRule(() => discoveryStormSaphoModes(g, frame, p.id)))
options.push({scope: 'testingStation', event: frame.event, mode});
return options;
@@ -29966,6 +29983,7 @@ function playSapho(g: Game, p: Player, action: Action) {
holder: p.id, event: option.event, mode: option.mode, nextEvent: crypto.randomUUID(),
}));
const used = discard(g, p, 'richese-juice-of-sapho');
delete g.saphoStationPriority;
g.ecologicalStorm = frame;
g.decision = {kind:'ecologicalStorm', player:frame.owner, event:frame.event};
log(g, `${p.name} discarded Juice of Sapho to take the Testing Station choice ${option.mode}. Both occupants retain one adjustment and the original storm movement remains.`,
@@ -43504,6 +43522,27 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
decideSaphoArrivalOrder(game, getPlayer(game, id), action);
return game.pendingTreacheryDiscard?.reaction ? game : normalizeAutomaticGame(game);
}
if (state.saphoStationPriority) {
const priority = state.saphoStationPriority, station = state.ecologicalStorm;
ecologicalStormIntegrity(state);
getPlayer(state, id);
if (action?.type === 'advanceBots') return state;
requireRule(station?.stage === 'choose' && station.event === priority.event &&
station.queue?.order.includes(id) && !priority.passed.includes(id),
'This Testing Station priority is not available to you.');
if (action.type === 'saphoStationPass') {
requireRule(Object.keys(action).sort().join(',') === 'event,type' && action.event === priority.event,
'Continue only the current Testing Station order opportunity.');
const game = structuredClone(state);
game.saphoStationPriority!.passed.push(id);
if (game.saphoStationPriority!.passed.length === station.queue!.order.length)
delete game.saphoStationPriority;
return game;
}
requireRule(action.type === 'card' && action.card === 'richese-juice-of-sapho' &&
action.scope === 'testingStation' && action.event === priority.event,
'Finish the Testing Station priority before choosing storm movement.');
}
if (action.alliancePromiseEvent !== undefined) {
const guide = projectedAlliancePromiseGuide(state, id);
requireRule(guide && Object.keys(action).sort().join(',') === Object.keys(guide.action).sort().join(',') &&
@@ -49391,7 +49430,11 @@ export function viewGame(state: Game, id: string, now?: number) {
? {event:g.battle.jacurutuDial.event,owner:g.battle.jacurutuDial.owner,winner:g.battle.jacurutuDial.winner,
choices:structuredClone(g.battle.jacurutuDial.choices)} : null,
ecologicalStorm: g.decision?.kind === 'ecologicalStorm' && g.decision.player === id && g.ecologicalStorm?.stage === 'choose'
? {...nexusRule(() => discoveryStormOffer(g,g.ecologicalStorm))!,event:g.ecologicalStorm.event} : null,
? {...nexusRule(() => discoveryStormOffer(g,g.ecologicalStorm))!,event:g.ecologicalStorm.event,
...(g.saphoStationPriority ? {blocked:'Finish the Testing Station order opportunity first.'} : {})} : null,
saphoStationPriority: g.saphoStationPriority ? {event:g.saphoStationPriority.event,
canPass:!!g.ecologicalStorm?.queue?.order.includes(id) && !g.saphoStationPriority.passed.includes(id),
waiting:g.saphoStationPriority.passed.includes(id) || !g.ecologicalStorm?.queue?.order.includes(id)} : null,
discoveryEntry: g.discoveryEntry?.stage === 'choose' && g.decision?.kind === 'discoveryEntry' && g.decision.player === id
? {...nexusRule(() => quoteDiscoveryEntry({...g,discoveries:g.discoveries!},g.discoveryEntry!,id)),event:g.decision.event} : null,
greatMaker: g.greatMaker ? { event:g.greatMaker.event, turn:g.greatMaker.turn, stage:g.greatMaker.stage, votes:structuredClone(g.greatMaker.votes), order:[...g.greatMaker.order], ride:greatMakerRideOptions(g,id) } : null,