Protect Sapho priority at the Testing Station
Verify and publish checkpoint container / container (push) Successful in 11m16s
Verify and publish checkpoint container / container (push) Successful in 11m16s
This commit is contained in:
1 parent
f73a6e0a9c
commit
a6be17fc5d
12 files changed
+391
-9
No files matched your search
@@ -1250,6 +1250,12 @@ function policyActions(g: GameView): Action[] {
|
||||
? [{ type: 'victoryContinuation', event: g.victoryContinuation.event, mode: 'finish' }]
|
||||
: [];
|
||||
if (g.homeworldTupileBlocked) return [];
|
||||
if (g.saphoStationPriority) {
|
||||
const own = g.players.find(player => player.id === g.me), priority = g.saphoStationPriority;
|
||||
return (own?.bot || own?.autopilot) && g.status === 'playing' && priority.canPass &&
|
||||
!g.roomControl?.paused && !g.roomControl?.closed && !g.automaticContinuationPending
|
||||
? [{type:'saphoStationPass', event:priority.event}] : [];
|
||||
}
|
||||
const dukeGrant = g.homeworldEcazDuke;
|
||||
if (dukeGrant && dukeGrant.occupier === me.id && !dukeGrant.blocked && dukeGrant.event && dukeGrant.recipients.length)
|
||||
return [{ type: 'homeworldDuke', event: dukeGrant.event, recipient: dukeGrant.recipients[0].id }];
|
||||
|
||||
+44
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -512,7 +512,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'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 Semuta claim/pass restores the same question queue, including the selected Homeworld, Leader Skills and Discovery profiles. Clean ordinary auction lots also preserve their standing bid and remaining clock. Richese cache Once Around and Silent lots preserve their original bidding and funding; Black Market, other special auctions 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.',
|
||||
'Before either of two Ecological Testing Station occupants chooses, use Sapho to take your adjustment first or last if that changes the order. Each occupant still makes one choice against the cumulative storm distance. Physical storm order and its original source stay unchanged. In supported Semuta games, claim or pass resumes the same next choice. The current occupant can act before another player intervenes, including an automated seat. Replay of a recovered Sapho remains unfinished; this online priority is provisional.',
|
||||
'Before either of two Ecological Testing Station occupants chooses, use Sapho to take your adjustment first or last if that changes the order. Fresh choices with Richese seated give both occupants a shared opportunity to use an available order change or continue; an automated occupant cannot advance the station choice while another player is deciding. Each occupant still adjusts the cumulative storm distance once. Physical storm order and its original source stay unchanged. In supported Semuta games, claim or pass resumes the same next choice. Replay of a recovered Sapho remains unfinished; this online priority is provisional.',
|
||||
'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. At a clean boundary, the holder may pause its own claimed turn and resume last without repeating the claim or payment. Completed turns stay closed and 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 another player has started, provisional queued timing lets that player finish unchanged. At a clean boundary, you may instead pause your own started turn and resume last, retaining used allowances. Pending requests must finish first. Completed turns and already-held positions cannot be repeated. Basic and older concurrent Revival phases have no ordered scope.',
|
||||
'When two or more players declare simultaneous Truthtrances in a quiet unrevealed battle, their public question-order window can use Sapho to move the holder’s group first or last. The current battle and committed plans remain in place. In the supported Basic Semuta preview, the discard and any exchange finish before the reordered questions resume.',
|
||||
|
||||
+2
-1
@@ -12,12 +12,13 @@ type TurnView = Pick<
|
||||
| 'phaseOpening'
|
||||
| 'automaticContinuationPending'
|
||||
> &
|
||||
Partial<Pick<GameView, 'biddingEnd' | 'kullReaction' | 'richeseBetrayalReaction' | 'guildBetrayalReaction' | 'nexusIxianReplacement' | 'nexusIxianBetrayalReaction' | 'nexusHarkonnenBetrayalReaction'>>;
|
||||
Partial<Pick<GameView, 'saphoStationPriority' | 'biddingEnd' | 'kullReaction' | 'richeseBetrayalReaction' | 'guildBetrayalReaction' | 'nexusIxianReplacement' | 'nexusIxianBetrayalReaction' | 'nexusHarkonnenBetrayalReaction'>>;
|
||||
|
||||
/** The single current decision owner for the table banner and seat highlight.
|
||||
* Shared response windows have no single acting seat. Only public view fields
|
||||
* participate; this does not decide which gameplay actions are legal. */
|
||||
export function tableActionOwner(g: TurnView): string | null {
|
||||
if (g.saphoStationPriority) return null;
|
||||
if (g.richeseBetrayalReaction || g.guildBetrayalReaction) return null;
|
||||
if (g.kullReaction && g.status === 'playing')
|
||||
return g.players.some(p => p.id === g.kullReaction!.player)
|
||||
|
||||
Reference in new issue
Block a user