Support group Fremen worm protection
Verify and publish checkpoint container / container (push) Successful in 10m58s

This commit is contained in:
Quintine committed 2026-10-29 00:05:12 +11:00
1 parent 6e26ceb216
commit 40c9651b80
12 files changed
+479 -24

No files matched your search

+77 -13
View File
@@ -754,6 +754,9 @@ import { isAlliedShipmentVisit, makeBasicAlliedShipment, quoteBasicAlliedShipmen
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionAllies, coalitionGroups, coalitionOf,
leaveCoalition, CoalitionAllianceError, type CoalitionState, type CoalitionTransition } from './coalition-alliance';
import { coalitionBattleBeneficiary } from './coalition-battle-preparation';
import { createCoalitionWormProtection, currentCoalitionWormAlly, decideCoalitionWormProtection,
resolveCoalitionWormProtection, protectedCoalitionWormAllies, validateCoalitionWormProtection,
CoalitionWormProtectionError, type CoalitionWormProtectionFrame } from './coalition-worm-protection';
import { reconcileCoalitionEscrow, setCoalitionPledge, type CoalitionEscrowMembership,
type CoalitionEscrowState } from './coalition-escrow';
import { CoalitionFundingError, quoteCoalitionFunding, settleCoalitionFunding,
@@ -1187,7 +1190,7 @@ export type Decision =
resume: 'storm' | 'shipment';
}
| { kind: 'wormPlacement'; player: string }
| { kind: 'wormProtection'; player: string; territory: string; ally: string }
| { kind: 'wormProtection'; player: string; territory: string; ally: string; event?: string }
| { kind: 'nexusFremenProtection'; player: string; event: string }
| { kind: 'nexusFremenRideBetrayal'; player: string; event: string }
| { kind: 'discoveryDiscard'; player: string; event: string }
@@ -1247,6 +1250,7 @@ export type ResponseWindow = {
/** Saved exact ordinary Guild-rate response; not the independent Karama rate. */
guildRateEvent?: string;
guildTransportEvent?: string;
coalitionWormEvent?: string;
advisorResume?: 'preparation' | 'wormRide' | 'declaration' | 'ambassador' | 'discoveryEntry' | 'planetologistGather';
advisorGatherMove?: { turn: number; move: number; order: MovementOrder };
discoveryEntry?: DiscoveryEntryArrivalChild;
@@ -1446,6 +1450,7 @@ export type Game = {
/** Unexposed group-alliance integration state; absent in existing games. */
coalitionState?: CoalitionState;
coalitionEscrow?: CoalitionEscrowState;
coalitionWormProtection?: CoalitionWormProtectionFrame;
/** Public written agreements. Their prose is honored by players, not parsed. */
deals?: PublicDeal[];
/** A human-to-human lobby offer, pending the recipient's consent. */
@@ -13706,7 +13711,7 @@ function drawSpice(g: Game) {
function devour(
g: Game,
t: string,
protectedAlly?: string,
protectedAlly?: string | readonly string[],
protectFremen = true,
) {
disasterPreflight(() =>
@@ -13722,6 +13727,27 @@ function devour(
g.wormRides.push(t);
log(g, `Shai-Hulud appeared in ${territory(t).name}.`);
}
function coalitionWormRule<T>(operation: () => T): T {
try { return operation(); }
catch (error) { if (error instanceof CoalitionWormProtectionError) throw new RuleError(error.message); throw error; }
}
function advanceCoalitionWormProtection(g: Game) {
const frame = g.coalitionWormProtection;
requireRule(frame, 'The original group worm protection is missing.');
const ally = coalitionWormRule(() => currentCoalitionWormAlly(g, frame));
if (ally) {
requireRule(frame.stage === 'decision', 'Finish the current worm protection response first.');
g.decision = {kind:'wormProtection', player:frame.owner, territory:frame.territory, ally, event:frame.event};
} else wormSurvival(g, frame.territory);
}
function finishCoalitionWorm(g: Game, t: string, protectFremen: boolean) {
const frame = g.coalitionWormProtection;
requireRule(frame && frame.territory === t, 'The original group worm occurrence changed.');
const protectedAllies = coalitionWormRule(() => protectedCoalitionWormAllies(g, frame));
devour(g, t, protectedAllies, protectFremen);
delete g.coalitionWormProtection;
afterWorm(g);
}
function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summoned' = 'natural') {
const fremen = byFaction(g, 'fremen');
if (!fremen && g.nexusCards?.cards && g.players.some(p =>
@@ -13773,7 +13799,12 @@ function beginWorm(g: Game, t: string, origin: 'natural' | 'additional' | 'summo
return;
}
}
if (fremen?.ally && at(getPlayer(g, fremen.ally), t)) {
const group = fremen && coalitionWormRule(() => createCoalitionWormProtection(g, fremen.id, t, crypto.randomUUID()));
if (group) {
requireRule(!g.coalitionWormProtection, 'Finish the previous group worm before another appearance.');
g.coalitionWormProtection = group;
advanceCoalitionWormProtection(g);
} else if (fremen?.ally && at(getPlayer(g, fremen.ally), t)) {
g.decision = {
kind: 'wormProtection',
player: fremen.id,
@@ -13892,11 +13923,12 @@ function wormSurvival(g: Game, t: string, protectedAlly?: string) {
owner: fremen.id,
location: t,
recipient: protectedAlly,
...(g.coalitionWormProtection ? {coalitionWormEvent:g.coalitionWormProtection.event} : {}),
passed: [],
};
} else {
devour(g, t, protectedAlly);
afterWorm(g);
if (g.coalitionWormProtection) finishCoalitionWorm(g, t, true);
else { devour(g, t, protectedAlly); afterWorm(g); }
}
}
function afterWorm(g: Game) {
@@ -30893,6 +30925,22 @@ function atomicsIntegrity(g: Game) {
'Atomics Aftermath has lost its revealed token, original targets or hand-limit custody.');
}
function marketGholaIntegrity(g: Game) {
const groupWorm = g.coalitionWormProtection;
if (groupWorm) {
coalitionWormRule(() => validateCoalitionWormProtection(g, groupWorm));
const pending = g.pendingKarama?.use.kind === 'cancel' ? g.pendingKarama.use.response : null;
const response = g.response ?? pending;
if (groupWorm.stage === 'decision') {
const decision = g.decision;
requireRule(decision?.kind === 'wormProtection' && decision.event === groupWorm.event &&
decision.player === groupWorm.owner && decision.territory === groupWorm.territory &&
decision.ally === coalitionWormRule(() => currentCoalitionWormAlly(g, groupWorm)),
'The group worm has lost its current Fremen decision.');
} else requireRule(response?.coalitionWormEvent === groupWorm.event && response.owner === groupWorm.owner &&
response.location === groupWorm.territory && (groupWorm.stage === 'response'
? response.kind === 'wormAllyProtection' && response.recipient === coalitionWormRule(() => currentCoalitionWormAlly(g, groupWorm))
: response.kind === 'wormSurvival'), 'The group worm has lost its original response.');
}
saphoCyclicIntegrity(g);
battleOrderIntegrity(g);
saphoAggressorIntegrity(g);
@@ -37059,14 +37107,20 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
continueSpice(g);
} else beginWorm(g, response.location!, 'additional');
} else if (response.kind === 'wormAllyProtection') {
wormSurvival(
g,
response.location!,
canceled ? undefined : response.recipient,
);
if (response.coalitionWormEvent) {
const frame = g.coalitionWormProtection;
requireRule(frame && response.location === frame.territory && response.recipient,
'The original group worm response is missing its ally.');
g.coalitionWormProtection = coalitionWormRule(() => resolveCoalitionWormProtection(g, frame,
response.owner, response.coalitionWormEvent!, response.recipient!, canceled));
advanceCoalitionWormProtection(g);
} else wormSurvival(g, response.location!, canceled ? undefined : response.recipient);
} else if (response.kind === 'wormSurvival') {
devour(g, response.location!, response.recipient, !canceled);
afterWorm(g);
if (response.coalitionWormEvent) {
requireRule(g.coalitionWormProtection?.event === response.coalitionWormEvent,
'The original group worm survival response is missing.');
finishCoalitionWorm(g, response.location!, !canceled);
} else { devour(g, response.location!, response.recipient, !canceled); afterWorm(g); }
} else if (response.kind === 'stormProtection') {
if (response.resume === 'storm') {
g.stormResolution!.protected = !canceled;
@@ -43640,7 +43694,17 @@ function applyActionInner(
typeof action.accept === 'boolean',
'Choose whether to protect your ally.',
);
if (action.accept) {
if (decision.event) {
const frame = g.coalitionWormProtection;
requireRule(frame && frame.territory === decision.territory && action.event === decision.event &&
action.ally === decision.ally && Object.keys(action).sort().join(',') === 'accept,ally,event,type',
'Choose the original group worm event and threatened ally.');
g.coalitionWormProtection = coalitionWormRule(() => decideCoalitionWormProtection(g, frame, id,
decision.event!, decision.ally, action.accept as boolean));
if (action.accept) g.response = {kind:'wormAllyProtection', owner:id, location:decision.territory,
recipient:decision.ally, coalitionWormEvent:decision.event, passed:[]};
else advanceCoalitionWormProtection(g);
} else if (action.accept) {
g.response = {
kind: 'wormAllyProtection',
owner: id,