Respect storm sector for rides into mobile stronghold
Verify and publish checkpoint container / container (push) Failing after 10m28s

This commit is contained in:
Quintine committed 2026-10-29 07:33:59 +11:00
1 parent 0d55333d5d
commit ac1f32c859
11 files changed
+324 -3

No files matched your search

+2 -1
View File
@@ -3,6 +3,7 @@ import { isAlliedShipmentVisit } from './allied-separation';
import { discoveryBotActions } from './discovery-options';
import { normalAuctionHandBlock } from './normal-auction';
import { fremenRevivalGrantBlock } from './fremen-revival-grant';
import { wormRideDestinationBlock } from './worm-ride-destination';
import { atreidesFullPlanAction } from './atreides-full-plan';
import { liveMovementPromises } from './movement-promises';
import { ecazOccupyDialChoices, ecazOccupyOwnProfile, ecazOccupyPolicyActions } from './ecaz-occupy-options';
@@ -3055,7 +3056,7 @@ function policyActions(g: GameView): Action[] {
.filter(
(to) =>
to.t !== d.territory && botEntryAllowed(g, me, to.t, to.s, 'move') &&
!botWormRideArrivalBlock(g, to.t, to.s),
!wormRideDestinationBlock(g, to.t, to.s) && !botWormRideArrivalBlock(g, to.t, to.s),
)
.slice(0, 16)
.map(
+10 -1
View File
@@ -665,6 +665,7 @@ import { choamGholaEvent, choamGholaSkillOfferEvent, choamGholaSkillSignature, c
import { quoteAdvisorPreparationWindow, quoteAdvisorPreparation, validateAdvisorPreparation, projectAdvisorPreparationWindow, AdvisorPreparationError, type AdvisorPreparationWindow, type AdvisorPreparationSource } from './advisor-preparation-window';
import { quoteFremenRevivalGrant, validateFremenRevivalGrant, projectFremenRevivalGrant, projectFremenGroupRevivalGrant, FremenRevivalGrantError, type FremenRevivalGrantSource } from './fremen-revival-grant';
import { richeseRevealInterruptSupported } from './richese-reveal-interruption';
import { wormRideDestinationBlock } from './worm-ride-destination';
import { splitOrdinaryPayments, type OrdinaryPayment } from './ordinary-payment-plan';
import {
charityAmount,
@@ -14655,7 +14656,11 @@ function greatMakerRideOptions(g: Game, owner: string) {
if (frame?.stage !== 'ride' || p.faction !== 'fremen' ||
p.fremenNexusWormBlockedTurn === g.turn) return null;
const destinations = gameTerritories(g).flatMap(t => t.sectors.flatMap(sector => {
try { allowedEntry(g,p,t.id,sector); return [{territory:t.id,name:t.name,sector}]; }
try {
allowedEntry(g,p,t.id,sector);
if (wormRideDestinationBlock(g,t.id,sector)) return [];
return [{territory:t.id,name:t.name,sector}];
}
catch (error) { if (error instanceof RuleError) return []; throw error; }
}));
return {max:p.reserves,eliteMax:p.elites?.reserves ?? 0,destinations};
@@ -14758,6 +14763,8 @@ function decideGreatMakerRide(g: Game, p: Player, action: Action) {
const amount = integer(action.amount,1,p.reserves,'Riding forces');
const elite = eliteChoice(amount,p.reserves,p.elites?.reserves ?? 0,action.elite);
allowedEntry(g,p,to,sector);
const stormBlock = wormRideDestinationBlock(g,to,sector);
requireRule(!stormBlock, stormBlock ?? 'That worm-ride destination is in storm.');
withdrawNativeReserves(g,p,amount,elite);
place(p,to,sector,amount,elite);
frame.arrival = {territory:to,sector,amount,elite};
@@ -45334,6 +45341,8 @@ function applyActionInner(
'Choose another destination territory.',
);
allowedEntry(g, p, to, sector);
const stormBlock = wormRideDestinationBlock(g, to, sector);
requireRule(!stormBlock, stormBlock ?? 'That worm-ride destination is in storm.');
const sandmaster = action.sandmasterCollect
? sandmasterWormCollection(g, p.id, to, sector, decision, action.sandmasterPile as string | undefined) : null;
if (action.sandmasterCollect) {
+3
View File
@@ -3,6 +3,7 @@ import type { Game, GameView, Player } from './engine';
import { presenceAt } from './force-presence';
import { nexusCardMode, validateNexusCards } from './nexus-cards';
import { territoryEntryBlock } from './occupancy';
import { wormRideDestinationBlock } from './worm-ride-destination';
/** The engine allocates a distinct event for each accepted worm appearance, even
* when two worms visit the same territory. Capture before any devouring. */
@@ -137,6 +138,8 @@ export function fremenCunningPublicDestinationBlock(
return 'Ride into the mobile stronghold only from the territory it points at.';
}
if (sector !== 0 && sector === g.storm) return 'That sector is in storm.';
const stormBlock = wormRideDestinationBlock(g, to, sector);
if (stormBlock) return stormBlock;
return territoryEntryBlock(g.players, owner, to);
}
+1
View File
@@ -2135,6 +2135,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Before later storm dials or card reveals, occupied Ixians may keep the current position or declare a route of up to three territories. Use the route selector to name each sector. The destination must be a non-stronghold territory. The route cannot begin, end or pass through the current storm sector. Occupants stay inside throughout relocation.',
'Choose the spice collected in each visited sector, including the starting sector. This preview provisionally permits any whole amount up to the available pile and two per eligible force inside, separately in each sector. Foreign fighters count, a concealed No-Field counts as one, and advisors do not count. Older moves without selected amounts retain their Ixian-only automatic limit. A public Karama response resolves before relocation or collection. The printed rule does not detail partial allocations or all passenger interactions.',
'Forces inside are separate from forces outside. Enter and leave through the pointing sector, counting the stronghold as one more territory of movement. A storm in that pointing sector blocks ordinary entry or exit. Direct shipping inside is restricted to Ixians; advanced Bene Gesserit may accompany their shipment.',
'Fremen may ride a worm directly into the mobile stronghold when its pointing sector is clear of the storm. Its sheltered occupants remain protected while that sector is in storm.',
'Worms and storms do not kill the occupants. Battles wait while the pointing sector is in storm; otherwise normal battles and the two-faction capacity apply inside. The sole controller counts it toward victory, even when that controller is not Ixian. Capturing it does not grant another faction permission to move it.',
'In advanced play, Ixians may spend special Karama once during their own Shipment and Movement turn to relocate up to two territories while their forces occupy it. This preserves their ordinary shipment and troop movement. Setup and bidding technology now have decision controls; expansion starts and remaining interaction audits are unfinished.',
],
+21
View File
@@ -0,0 +1,21 @@
import { MOBILE_STRONGHOLD, splitLocation, validLocation } from './board';
/** The mobile stronghold's interior is encoded as sector 0, but a worm ride
* enters through its actual pointed sector. Ordinary sectors and the Polar
* Sink keep their existing independent entry checks. */
export function wormRideDestinationBlock(
g: {storm: number; mobileStronghold?: {location: string | null} | null},
territory: string,
_sector: number,
): string | null {
if (territory !== MOBILE_STRONGHOLD || !g.mobileStronghold?.location ||
!Number.isSafeInteger(g.storm) || g.storm < 1 || g.storm > 18) return null;
const pointer = g.mobileStronghold.location;
const parts = pointer.split(':');
if (parts.length !== 2) return null;
const at = splitLocation(pointer);
if (!validLocation(at.territory, at.sector) || at.sector < 1) return null;
return at.sector === g.storm
? 'The Hidden Mobile Stronghold points into the storm; a worm ride cannot enter it.'
: null;
}