Resume worm rides after advanced Sneak arrivals
Verify and publish checkpoint container / container (push) Successful in 10m40s
Verify and publish checkpoint container / container (push) Successful in 10m40s
This commit is contained in:
1 parent
b79e448deb
commit
57321c2c8d
8 files changed
+363
-36
No files matched your search
+83
-10
@@ -2334,7 +2334,18 @@ export type Game = {
|
||||
caladanSneakAdvisor?: true;
|
||||
/** An ordinary Sneak group waits for an existing Ambassador/Terror parent. */
|
||||
nativeSneakAdvisor?: true;
|
||||
/** Binds this physical group to an independently retained worm continuation. */
|
||||
sneakWormEvent?: string;
|
||||
}[] | null;
|
||||
/** The original worm ride is held while a committed Sneak arrival resolves. */
|
||||
pendingSneakWorm?: {
|
||||
event: string;
|
||||
turn: number;
|
||||
phase: number;
|
||||
rider: string;
|
||||
parent: {kind: 'terror' | 'ambassador' | 'overlap'; event: string};
|
||||
stage: 'parent' | 'queued' | 'resolving';
|
||||
} | null;
|
||||
/** One committed entry with two independently optional expansion reactions. */
|
||||
pendingArrivalOverlap?: {
|
||||
victoryEvent?: string;
|
||||
@@ -15103,7 +15114,29 @@ function finishSpicePass(g: Game) {
|
||||
nextPhase(g);
|
||||
}
|
||||
}
|
||||
function holdSneakWormParent(g: Game, kind: 'terror' | 'ambassador' | 'overlap', event: string): boolean {
|
||||
const frame = g.pendingSneakWorm;
|
||||
if (!frame || frame.parent.kind !== kind || frame.parent.event !== event) return false;
|
||||
requireRule(frame.stage === 'parent' && frame.turn === g.turn && frame.phase === g.phase,
|
||||
'The Sneak arrival lost its original worm parent.');
|
||||
frame.stage = 'queued';
|
||||
return true;
|
||||
}
|
||||
function sneakWormIntegrity(g: Game): void {
|
||||
const frame = g.pendingSneakWorm;
|
||||
if (!frame) return;
|
||||
const queued = (g.pendingArrivalReaction ?? []).filter(row => row.sneakWormEvent === frame.event);
|
||||
requireRule(g.status === 'playing' && g.advanced && frame.turn === g.turn && frame.phase === g.phase && g.phase === 1 &&
|
||||
typeof frame.event === 'string' && frame.event.length > 0 &&
|
||||
g.players.some(player => player.id === frame.rider && player.faction === 'fremen') &&
|
||||
!!frame.parent && ['terror','ambassador','overlap'].includes(frame.parent.kind) &&
|
||||
typeof frame.parent.event === 'string' && frame.parent.event.length > 0 &&
|
||||
(frame.stage === 'parent' || frame.stage === 'queued' ? queued.length === 1 :
|
||||
frame.stage === 'resolving' && queued.length === 0),
|
||||
'The saved Sneak arrival lost its original worm continuation.');
|
||||
}
|
||||
function nextWormRide(g: Game) {
|
||||
requireRule(!g.pendingSneakWorm, 'Finish the committed Sneak arrival before resuming another worm continuation.');
|
||||
if (g.greatMaker?.stage === 'arrival') { completeGreatMaker(g); return; }
|
||||
if (g.summonedNexusBeforeRides) {
|
||||
g.summonedNexusBeforeRides = false;
|
||||
@@ -25277,7 +25310,7 @@ function finishArrivalOverlap(g: Game, overlap: NonNullable<Game['pendingArrival
|
||||
'Both reactions must finish before resuming the original arrival.');
|
||||
g.pendingArrivalOverlap = null;
|
||||
log(g, 'Both optional Ambassador and Terror reactions to the original entry have finished.');
|
||||
if (overlap.resume === 'wormRide') nextWormRide(g);
|
||||
if (overlap.resume === 'wormRide' && !holdSneakWormParent(g, 'overlap', overlap.event)) nextWormRide(g);
|
||||
}
|
||||
/** Open the first original reaction once its public ordering window is complete. */
|
||||
function openFirstOrderedArrival(g: Game) {
|
||||
@@ -25460,7 +25493,7 @@ function finishAmbassador(g: Game, deferResume = false) {
|
||||
finishArrivalOverlap(g, overlap);
|
||||
return;
|
||||
}
|
||||
if (!deferResume && pending.resume === 'wormRide') nextWormRide(g);
|
||||
if (!deferResume && pending.resume === 'wormRide' && !holdSneakWormParent(g, 'ambassador', pending.event)) nextWormRide(g);
|
||||
}
|
||||
function currentFremenAmbassador(g: Game, event?: string, completed = false) {
|
||||
const entry = g.pendingAmbassador;
|
||||
@@ -27831,7 +27864,7 @@ function finishTerrorEntry(g: Game) {
|
||||
finishArrivalOverlap(g, overlap);
|
||||
return;
|
||||
}
|
||||
if (pending.resume === 'wormRide') nextWormRide(g);
|
||||
if (pending.resume === 'wormRide' && !holdSneakWormParent(g, 'terror', pending.entrySignature ?? terrorEntrySignature(pending))) nextWormRide(g);
|
||||
else if (pending.resume === 'ambassador')
|
||||
continueAmbassadorArrival(g, pending.ambassadorEvent!);
|
||||
}
|
||||
@@ -27887,9 +27920,8 @@ function sneakAttackOptions(
|
||||
let blocked: string | null = null;
|
||||
try {
|
||||
allowedEntry(g, owner, entry.territory, entry.sector);
|
||||
if (g.advanced && (entry.resume === 'wormRide' || g.pendingArrivalOverlap?.resume === 'wormRide' ||
|
||||
g.pendingAmbassador?.resume === 'wormRide'))
|
||||
blocked = 'Sneak Attack during a worm ride needs its original ride continuation; send no forces or leave the token hidden.';
|
||||
if (g.advanced && g.greatMaker?.stage === 'arrival')
|
||||
blocked = 'Sneak Attack during a Great Maker arrival needs its original continuation; send no forces or leave the token hidden.';
|
||||
} catch (error) {
|
||||
if (!(error instanceof RuleError)) throw error;
|
||||
blocked = error.message;
|
||||
@@ -28171,10 +28203,26 @@ function decideTerror(g: Game, p: Player, action: Action) {
|
||||
const revivalArrival: RevivalEntryParent | undefined = amount > 0 && entry.revivalArrival
|
||||
? {event:entry.revivalArrival.event,terror:{event:entry.entrySignature!,owner:p.id,
|
||||
territory:entry.territory,sector:entry.sector,amount,elite:0}} : undefined;
|
||||
// Worm continuations can advance the phase synchronously; their child
|
||||
// arrival needs a separate retained worm cursor before it can be queued.
|
||||
const wormParent = g.pendingArrivalOverlap?.resume === 'wormRide'
|
||||
? {kind:'overlap' as const, event:g.pendingArrivalOverlap.event}
|
||||
: entry.resume === 'ambassador' && g.pendingAmbassador?.resume === 'wormRide'
|
||||
? {kind:'ambassador' as const, event:g.pendingAmbassador.event}
|
||||
: entry.resume === 'wormRide'
|
||||
? {kind:'terror' as const, event:entry.entrySignature ?? terrorEntrySignature(entry)}
|
||||
: undefined;
|
||||
const advancedNativeArrival = g.advanced && entry.resume !== 'wormRide' &&
|
||||
g.pendingArrivalOverlap?.resume !== 'wormRide' && g.pendingAmbassador?.resume !== 'wormRide';
|
||||
const wormSneak = amount > 0 && g.advanced && !!wormParent && !caladan && !revivalArrival;
|
||||
if (wormSneak) {
|
||||
const rider = byFaction(g, 'fremen');
|
||||
requireRule(rider && !g.pendingSneakWorm && g.phase === 1,
|
||||
'This Sneak arrival cannot retain its original worm rider.');
|
||||
const event = crypto.randomUUID();
|
||||
g.pendingSneakWorm = {event, turn:g.turn, phase:g.phase, rider:rider.id, parent:wormParent!, stage:'parent'};
|
||||
(g.pendingArrivalReaction ??= []).push({entrant:p.id,territory:entry.territory,
|
||||
sector:entry.sector,amount,elite:0,cause:'shipment',resume:'none',turn:g.turn,
|
||||
phase:g.phase,nativeSneakAdvisor:true,sneakWormEvent:event});
|
||||
}
|
||||
if (caladan) (g.homeworldVictoryReinforcement!.terrorArrivals ??= []).push(caladan.receipt);
|
||||
finishTerrorEntry(g);
|
||||
if (caladan) {
|
||||
@@ -28191,6 +28239,7 @@ function decideTerror(g: Game, p: Player, action: Action) {
|
||||
phase:g.phase,revivalArrival,revivalSneakAdvisor:true});
|
||||
return;
|
||||
}
|
||||
if (wormSneak) return;
|
||||
if (amount > 0 && (advancedNativeArrival || !g.advanced &&
|
||||
(g.pendingArrivalOverlap || g.pendingAmbassador || g.pendingTerrorEntry || g.response || g.decision))) {
|
||||
// The child may have opened the original Terror decision. Let it finish
|
||||
@@ -43860,8 +43909,9 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (g.pendingHomeworldTupileCleanup || tupileSourceBlock(g)) return;
|
||||
if (g.truthtrance || g.phaseOpening || g.status === 'finished') return;
|
||||
if (!g.response && !g.decision && g.pendingAutomaticAdvisorFlips?.length) openAutomaticAdvisorFlip(g);
|
||||
if (g.pendingArrivalReaction?.length && !g.response && !g.decision &&
|
||||
!g.pendingTerrorEntry && !g.pendingAmbassador && !g.pendingArrivalOverlap && !g.pendingSpiritualAdvisor) {
|
||||
if (g.pendingArrivalReaction?.length && !g.response && !g.decision && !g.summonedWorm && !g.pendingKull &&
|
||||
!g.pendingTerrorEntry && !g.pendingAmbassador && !g.pendingArrivalOverlap && !g.pendingSpiritualAdvisor &&
|
||||
!g.pendingTreacheryDiscard) {
|
||||
const deferred = g.pendingArrivalReaction.shift()!;
|
||||
if (!g.pendingArrivalReaction.length) g.pendingArrivalReaction = null;
|
||||
if (deferred.victoryEvent !== undefined || deferred.cause === 'caladanReinforcement')
|
||||
@@ -43895,6 +43945,14 @@ function settleAutomaticContinuations(g: Game) {
|
||||
deferred.cause === 'shipment' && deferred.resume === 'none' &&
|
||||
entrant!.faction === 'moritani' && deferred.amount > 0 && deferred.elite === 0,
|
||||
'The queued Sneak reactions have lost their committed native entry.');
|
||||
if (deferred.sneakWormEvent) {
|
||||
const frame = g.pendingSneakWorm;
|
||||
requireRule(frame && frame.event === deferred.sneakWormEvent && frame.stage === 'queued' &&
|
||||
frame.turn === g.turn && frame.phase === g.phase &&
|
||||
g.players.some(player => player.id === frame.rider && player.faction === 'fremen'),
|
||||
'The queued Sneak group lost its original worm continuation.');
|
||||
frame.stage = 'resolving';
|
||||
}
|
||||
const bg = byFaction(g, 'beneGesserit');
|
||||
const followup = bg && bg.id !== entrant!.id && spiritualAdvisorMaximum(g, bg.id) > 0
|
||||
? {shipment:entrant!.id,destination:location(deferred.territory,deferred.sector)} : undefined;
|
||||
@@ -43906,6 +43964,19 @@ function settleAutomaticContinuations(g: Game) {
|
||||
if (deferred.revivalArrival) resumeHomeworldRevivalReturn(g);
|
||||
continue;
|
||||
}
|
||||
if (g.pendingSneakWorm?.stage === 'resolving' && !g.response && !g.decision &&
|
||||
!g.pendingTerrorEntry && !g.pendingAmbassador && !g.pendingArrivalOverlap &&
|
||||
!g.pendingArrivalReaction?.length && !g.pendingSpiritualAdvisor &&
|
||||
!g.pendingAutomaticAdvisorFlips?.length && !g.pendingTreacheryDiscard && !g.pendingKarama &&
|
||||
!g.summonedWorm && !g.pendingKull) {
|
||||
const frame = g.pendingSneakWorm;
|
||||
requireRule(frame.turn === g.turn && frame.phase === g.phase && g.phase === 1 &&
|
||||
g.players.some(player => player.id === frame.rider && player.faction === 'fremen'),
|
||||
'The completed Sneak arrival lost its original worm rider.');
|
||||
g.pendingSneakWorm = null;
|
||||
nextWormRide(g);
|
||||
continue;
|
||||
}
|
||||
const response = g.response;
|
||||
const before = JSON.stringify(g);
|
||||
const bonus = g.pendingHomeworldOccupiedBonus;
|
||||
@@ -43980,6 +44051,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
}
|
||||
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
|
||||
export function normalizeAutomaticGame(state: Game): Game {
|
||||
sneakWormIntegrity(state);
|
||||
emperorPhaseOpeningGiftIntegrity(state);
|
||||
emperorExtortionGiftIntegrity(state);
|
||||
emperorAuctionGiftIntegrity(state);
|
||||
@@ -44150,6 +44222,7 @@ function applyActionInner(
|
||||
execution: 'live' | 'shipmentPreparation' | 'movementPreparation' = 'live',
|
||||
initiator = id,
|
||||
): Game {
|
||||
sneakWormIntegrity(state);
|
||||
tupileSlotsIntegrity(state);
|
||||
const tupileBlock = tupileSourceBlock(state);
|
||||
requireRule(!tupileBlock, tupileBlock ?? 'The occupied Tupile source is unresolved.');
|
||||
|
||||
Reference in new issue
Block a user