Resolve Sneak arrivals during Great Maker rides
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 19:59:10 +11:00
1 parent 57321c2c8d
commit 2ad7f127ec
9 files changed
+290 -13

No files matched your search

+12 -1
View File
@@ -17,12 +17,23 @@ open mode gates.
## Current checkpoint and work
**Sneak during a Great Maker arrival — 29 October. Prototyped / Partial.**
Great Maker now retains its original reserve ride while a positive Moritani
Sneak group resolves its own reactions, then completes once. Its encounter and
arrival remain bound to the saved worm continuation. Twelve related files pass
79 cases, including a real vote and ride, both paired-owner orders and legal
bot choices. Fixtures use audit setup and control the turn, token placement,
Spice order and paired owner order. Wider optional interactions and deployed
verification remain open.
[Scope](MORITANI_ENTRY_TIMING.md#great-maker-sneak-arrivals--29-october-2026).
**Advanced Sneak during a worm ride — 29 October. Prototyped / Partial.**
A separate saved continuation retains the Fremen rider while a positive
Moritani Sneak group resolves its own arrival and reactions. It resumes the
original worm once, including the next ride or a summoned-worm Nexus. Ten
related files pass 75 cases, including existing bot choices; the worm fixtures
stage their starting positions. Great Maker, wider optional-module combinations,
stage their starting positions. The later Great Maker checkpoint above extends
that continuation; wider optional-module combinations,
full-game and deployed verification remain open.
[Scope](MORITANI_ENTRY_TIMING.md#advanced-sneak-on-a-worm-ride--29-october-2026).
+9
View File
@@ -1,5 +1,14 @@
# Discovery functional prototype
## Great Maker and Sneak Attack — 29 October 2026
**Prototyped / Partial.** Great Maker's signed reserve arrival can wait for a
positive Moritani Sneak group and its own arrival reactions before completing.
This retains the original Fremen rider and does not spend another movement or
shipment. The original paired Ecaz/Moritani entry is also recognized. The
provisional ordering, controlled fixture staging and remaining combinations are
recorded in [the entry contract](MORITANI_ENTRY_TIMING.md#great-maker-sneak-arrivals--29-october-2026).
## Sapho at Ecological Testing Station — 29 October 2026
**Prototyped / Partial.** Before either of two original occupants chooses,
+24 -6
View File
@@ -2,6 +2,25 @@
Reviewed 2026-09-06. This supplements `MORITANI_TERROR_RULES.md`; it does not enable combined-faction play or certify an implementation order.
## Great Maker Sneak arrivals — 29 October 2026
**Prototyped / Partial.** An Advanced Great Maker reserve arrival now retains
its original encounter and ride while a positive Moritani Sneak group resolves
its own eligible reactions, then completes the Great Maker once. Its existing
encounter signature binds the saved continuation, and the original paired
Ambassador/Terror entry is also recognized while its owners act. Zero forces
retain the normal original continuation. Ordering is the provisional child
composition below, not a publisher priority ruling.
Twelve related files pass 79 cases. The focused fixture uses audit setup and
controls the later turn, physical token placements, unplayed Spice order and
paired owner order; actual Great Maker votes and the reserve ride are actions.
Tests cover both original owner orders, Bene Gesserit, the child Ambassador,
zero and legal bot choices. A summon during a held ordinary Sneak child also
retains its Nexus before the next ride. Great Maker with a non-immediate summoned
worm and other deeply nested or optional combinations remain open, as do
full-game and deployed verification.
## Advanced Sneak on a worm ride — 29 October 2026
**Prototyped / Partial.** During an ordinary Advanced worm arrival, a positive
@@ -16,9 +35,8 @@ Ten related files pass 75 cases. Staged Advanced worm positions exercise direct
and paired parents, both owner orders, BG accompaniment, a Guild Ambassador
child, zero, next/final ride and a summoned-worm interruption; existing bot
choices remain legal. These are not full-game or deployed acceptance. Great
Maker arrivals retain a temporary positive-entry guard because their separate
saved arrival witness is not yet connected to this continuation. Other optional
module combinations remain unfinished.
Maker arrivals are extended by the later checkpoint above. Other optional module
combinations remain unfinished.
## Direct Advanced Sneak arrivals — 29 October 2026
@@ -32,8 +50,8 @@ publisher priority ruling. Caladan and revival source receipts retain their
dedicated paths.
The later ordinary-worm checkpoint above retains the rider separately from
Moritani's child. Great Maker and broader modules remain open; deployed
verification remains open.
Moritani's child; the later checkpoint above also extends Great Maker. Broader
modules and deployed verification remain open.
Ten related files pass 73 cases. Direct runtime fixtures use genuine Advanced
setup and physical token selection/placement under control, including BG and
@@ -55,7 +73,7 @@ described in GF9 Ecaz & Moritani rules pp. 8 and 15.
Fremen destinations combining Advanced fighter Intrusion with Terror retain
their existing restriction, and simultaneous Ambassador/Terror on Guild
accompaniment remains separate. Other optional modules, Great Maker,
accompaniment remains separate. Other optional modules, further Great Maker interactions,
complete rules and deployed verification remain open.
Eight related files pass 52 cases. Real Advanced setup covers both owner orders,
+8 -1
View File
@@ -1,10 +1,17 @@
# Rule decision index
## Great Maker Sneak arrival — 29 October 2026
Great Maker retains its signed reserve arrival while the separately committed
Sneak group finishes its reactions, then continues once. The owner/child order
is provisional; deeper optional interactions remain open.
[Scope](MORITANI_ENTRY_TIMING.md#great-maker-sneak-arrivals--29-october-2026).
## Advanced Sneak on a worm ride — 29 October 2026
An ordinary worm rider's continuation waits for the separately committed Sneak
group and its reactions; the child does not inherit the Fremen rider's identity.
The ordering is provisional. Great Maker retains its separate guard.
The ordering is provisional. The later Great Maker checkpoint above extends it.
[Scope](MORITANI_ENTRY_TIMING.md#advanced-sneak-on-a-worm-ride--29-october-2026).
## Direct Advanced Sneak arrival — 29 October 2026
+17 -3
View File
@@ -2345,6 +2345,8 @@ export type Game = {
rider: string;
parent: {kind: 'terror' | 'ambassador' | 'overlap'; event: string};
stage: 'parent' | 'queued' | 'resolving';
/** Bind the held ride to the exact Great Maker encounter when applicable. */
greatMaker?: {event: string; signature: string};
} | null;
/** One committed entry with two independently optional expansion reactions. */
pendingArrivalOverlap?: {
@@ -14578,6 +14580,7 @@ function afterWorm(g: Game) {
g.pendingAmbassador?.resume === 'wormRide' ||
g.pendingTerrorEntry?.resume === 'wormRide' ||
g.pendingArrivalOverlap?.resume === 'wormRide' ||
!!g.pendingSneakWorm ||
(g.pendingKarama?.use.kind === 'cancel' &&
g.pendingKarama.use.response.kind === 'advisorFlip' &&
g.pendingKarama.use.response.advisorResume === 'wormRide') ||
@@ -14890,6 +14893,7 @@ function stampGreatMaker(g: Game) {
g.greatMaker!.signature = greatMakerSignature(g.greatMaker!);
}
function greatMakerIntegrity(g: Game) {
sneakWormIntegrity(g);
const frame = g.greatMaker, continuation = g.pendingTreacheryDiscard?.continuation;
const controls = [g, g.pendingExchange, g.pendingNullentropy?.resume, g.pendingRicheseGift?.resume,
g.pendingRichesePurchaseIncome?.resume, continuation && 'resume' in continuation ? continuation.resume : null,
@@ -14935,6 +14939,12 @@ function greatMakerIntegrity(g: Game) {
controls.some(control => control?.decision?.kind === 'intrusion' && control.decision.wormRide && control.decision.player === byFaction(g,'beneGesserit')?.id && control.decision.territory === frame.arrival!.territory) ||
responses.some(response => response?.kind === 'advisorFlip' && response.advisorResume === 'wormRide' && response.owner === byFaction(g,'beneGesserit')?.id && response.location === frame.arrival!.territory) ||
(g.pendingAmbassador?.resume === 'wormRide' && g.pendingAmbassador.territory === frame.arrival!.territory && g.pendingAmbassador.sector === frame.arrival!.sector) ||
(g.pendingArrivalOverlap?.resume === 'wormRide' && g.pendingArrivalOverlap.cause === 'wormRide' &&
g.pendingArrivalOverlap.entrant === byFaction(g, 'fremen')?.id &&
g.pendingArrivalOverlap.territory === frame.arrival!.territory && g.pendingArrivalOverlap.sector === frame.arrival!.sector) ||
(g.pendingSneakWorm?.greatMaker?.event === frame.event && g.pendingSneakWorm.greatMaker.signature === frame.signature &&
g.pendingSneakWorm.rider === byFaction(g, 'fremen')?.id &&
g.pendingSneakWorm.turn === g.turn && g.pendingSneakWorm.phase === g.phase) ||
(g.pendingTerrorEntry?.resume === 'wormRide' && g.pendingTerrorEntry.territory === frame.arrival!.territory && g.pendingTerrorEntry.sector === frame.arrival!.sector),
'The Great Maker has lost its reserve arrival interaction.');
}
@@ -15131,6 +15141,9 @@ function sneakWormIntegrity(g: Game): void {
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.greatMaker === undefined || !!frame.greatMaker && g.greatMaker?.stage === 'arrival' &&
frame.greatMaker.event === g.greatMaker.event && frame.greatMaker.signature === g.greatMaker.signature) &&
(g.greatMaker?.stage !== 'arrival' || frame.greatMaker !== undefined) &&
(frame.stage === 'parent' || frame.stage === 'queued' ? queued.length === 1 :
frame.stage === 'resolving' && queued.length === 0),
'The saved Sneak arrival lost its original worm continuation.');
@@ -27920,8 +27933,6 @@ function sneakAttackOptions(
let blocked: string | null = null;
try {
allowedEntry(g, owner, entry.territory, entry.sector);
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;
@@ -28218,7 +28229,10 @@ function decideTerror(g: Game, p: Player, action: Action) {
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'};
const greatMaker = g.greatMaker?.stage === 'arrival'
? {event:g.greatMaker.event, signature:g.greatMaker.signature} : undefined;
g.pendingSneakWorm = {event, turn:g.turn, phase:g.phase, rider:rider.id, parent:wormParent!, stage:'parent',
...(greatMaker ? {greatMaker} : {})};
(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});
+2 -2
View File
@@ -4227,7 +4227,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Sabotage: randomly draw and discard a Treachery Card from the entrant if possible. Then you may give that player a card of your choice from your own hand. Giving a card is optional. If the gift puts the recipient above their hand limit, the recipient provisionally chooses one held card to discard, including the gift; the original entry waits for that fresh discard and any supported Semuta reaction.',
'Sneak Attack: send up to five of your reserve forces into the triggered territory at no cost, respecting storm and occupancy restrictions. This particular entry is allowed even when Atomics Aftermath is present.',
'Before revealing Sneak Attack, inspect its private available maximum and any reason positive entry is blocked. You may keep it hidden. Revealing spends the token even if you then choose Send no forces. For a supported entry, select zero through the displayed maximum; forces enter the arrival’s territory and sector without consuming your ordinary shipment or movement turn. Zero remains available when positive entry is blocked. Aftermath’s printed exception allows this reserve entry even after Atomics resolves. A positive entry during Shipment and Movement is treated as an off-planet shipment for the Heighliners token, which accrues its once-per-turn income for its owner, and the committed entry finishes before Bene Gesserit may answer it with an Intrusion choice or a free spiritual-advisor shipment.',
'In Advanced play the separate Sneak group can also trigger an eligible Ecaz Ambassador after its Bene Gesserit reactions. During an ordinary worm ride the original Fremen continuation waits for the child and resumes once. A positive group during a Great Maker arrival remains temporarily unavailable; leaving the token hidden or sending zero remains possible.',
'In Advanced play the separate Sneak group can also trigger an eligible Ecaz Ambassador after its Bene Gesserit reactions. During an ordinary worm ride or Great Maker reserve arrival, the original Fremen continuation waits for the child and resumes once. Deeper optional interactions remain provisional.',
'The random Assassination token is separate from the advanced Assassinate Leaders advantage. Neither ability supplies the other’s selection or timing rules.',
],
checklist: [
@@ -4235,7 +4235,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Original actual390px Reveal Assassination resolves a captive Duke death6, displays Tanks inspection and resumes native Harkonnen movement. No selectable victim or projected pool; no overflow/browser errors.', evidence: ['components/game-table.tsx', 'components/duke-vidal.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four policies make accepted original decisions: Easy reveals, Medium/Hard/Brutal form an accepted Enemy of My Enemy alliance in the actual native position. Legal participation only; no calibrated strategy claim.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Effective-control exceptional pool, public victim and spent-empty behavior are labelled provisional and separate from Advanced battle-loss Assassinate Leaders. Native printed values and no foreign-Ghola Duke remain binding.', evidence: ['docs/MORITANI_ASSASSINATION_SNEAK.md', 'docs/MORITANI_TERROR_RULES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Advanced worm Sneak and related arrivals pass ten files and 75 cases, using staged worm positions and legal bot choices. Earlier direct Sneak tests use genuine setup with controlled physical token placements. Earlier affected22-file195/195 and types/lint/build pass for Assassination. Pure custody/identity/value boundaries plus original capture-to-Terror runtime and real table death6 pass. Empty boundary resumes genuine Guild entry but stages prior native deaths. Full combinations/deployed acceptance open.', evidence: ['tests/advanced-sneak-worm-runtime.test.ts', 'tests/advanced-sneak-direct-runtime.test.ts', 'tests/ambassador-terror-overlap-worm.test.ts', 'tests/moritani-terror-assassination.test.ts', 'tests/moritani-terror-assassination-runtime.test.ts', 'tests/moritani-force-leader.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Great Maker Sneak and related arrivals pass twelve files and 79 cases, using audit setup, controlled token/turn/order staging, real votes and rides, and legal bot choices. Earlier worm tests stage positions and direct Sneak uses setup with controlled physical token placements. Earlier affected22-file195/195 and types/lint/build pass for Assassination. Pure custody/identity/value boundaries plus original capture-to-Terror runtime and real table death6 pass. Empty boundary resumes genuine Guild entry but stages prior native deaths. Full combinations/deployed acceptance open.', evidence: ['tests/great-maker-sneak-runtime.test.ts', 'tests/advanced-sneak-worm-runtime.test.ts', 'tests/advanced-sneak-direct-runtime.test.ts', 'tests/ambassador-terror-overlap-worm.test.ts', 'tests/moritani-terror-assassination.test.ts', 'tests/moritani-terror-assassination-runtime.test.ts', 'tests/moritani-force-leader.test.ts'] },
],
related: [
'moritani-terror',
@@ -102,6 +102,25 @@ void test('a summoned worm during a held Sneak arrival keeps its Nexus before th
assert.equal(state.decision ?? null, null);
});
void test('a summoned worm during the Sneak child preserves the Nexus before the next ride', () => {
const { g } = table('sneakAttack');
const karama = g.deck.find(card => card.effect === 'karama')!;
g.deck = g.deck.filter(card => card.id !== karama.id);
seat(g, 'in').hand = [karama];
let state = wormRide(g);
state = action(state, 'm', {type:'decision', reveal:true});
state = action(state, 'm', {type:'decision', amount:1});
state = action(state, 'ec', {type:'decision', event:state.pendingAmbassador!.event, decline:true});
assert.equal(state.pendingAmbassador?.entrant, 'm');
state = action(state, 'in', {type:'card', mode:'special', card:karama.id, territory:'the_great_flat'});
assert.equal(state.summonedNexusBeforeRides, true);
state = action(state, 'ec', {type:'decision', event:state.pendingAmbassador!.event, decline:true});
assert.equal(state.pendingSneakWorm ?? null, null);
assert.equal(state.nexus, true);
assert.deepEqual(state.wormRides, ['hagga_basin']);
assert.equal(state.decision ?? null, null);
});
void test('Terror first keeps Robbery overflow custody and the ordered arrival intact', () => {
const { g, terror } = table('robbery');
seat(g, 'm').hand = g.deck.splice(0, 4);
+100
View File
@@ -0,0 +1,100 @@
import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeDiscoveryGameForAudit, initializeEcazTreacheryGameForAudit, joinGame, newPlayer,
type Action, type Game,
} from '../game/engine';
import { placeAmbassador, validateAmbassadors } from '../game/ecaz-ambassadors';
import { placeTerror } from '../game/moritani-terror';
import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
import { nextSpiceBankerIncomeNativeStep } from './fixture-spice-banker-income';
export const act = (game: Game, actor: string, action: Action): Game =>
applyAction(structuredClone(game), actor, action);
export function player(game: Game, id: string) {
const seat = game.players.find(row => row.id === id); assert.ok(seat); return seat;
}
/** Original Advanced native setup, actual Great Maker votes and reserve ride.
* The test controls a later turn/phase boundary, the existing unplayed Spice
* order and original physical Terror/Ambassador placement. It does not stage
* forces, player purses, a Great Maker encounter, a pending reaction or receipt.
* Ecaz additionally selects the independent printed card variant to enter the
* existing paired E3 Discovery profile. Paired tests control the public order
* before the real Great Maker votes to exercise both original owner orders. */
export function greatMakerSneakFixture(withBG = false, withEcaz = false,
ambassadorEffect: 'fremen' | 'emperor' = 'fremen', first: 'terror' | 'ambassador' = 'terror') {
let game = createGame('MAKERSNEAK', newPlayer('moritani','Moritani','moritani'), true, ['ecaz']);
for (const faction of ['fremen','atreides', ...(withBG ? ['beneGesserit'] as const : []),
...(withEcaz ? ['ecaz'] as const : [])] as const)
joinGame(game, newPlayer(faction,faction,faction));
game.discoveryEnabled = true;
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
game = withEcaz ? initializeEcazTreacheryGameForAudit(game) : initializeDiscoveryGameForAudit(game);
for (let count = 0; game.status === 'setup' && count < 250; count++) {
if (game.setupStage === 'forces' && player(game,'fremen').reserves === 20) {
game = act(game,'fremen',{type:'fremenSetup',placements:{sietch_tabr:10}});
continue;
}
const next = withEcaz ? nextBasicSemutaAmbassadorStep(game) : nextSpiceBankerIncomeNativeStep(game);
assert.ok(next, 'Native Discovery setup needs an existing legal choice');
game = act(game, next.actor, next.action);
}
assert.equal(game.status, 'playing');
assert.ok(game.advanced && game.discoveryEnabled && game.discoveries);
const sneak = game.moritaniTerror!.tokens.find(row => row.kind === 'sneakAttack'); assert.ok(sneak);
game.moritaniTerror = placeTerror(game.moritaniTerror!, sneak.id, 'carthag', 1);
let ambassador: string | undefined;
if (withEcaz) {
const inventory = game.ecazAmbassadors!;
const token = inventory.tokens.find(row => row.effect === ambassadorEffect); assert.ok(token);
if (token.zone === 'pool') {
const replacement = inventory.tokens.find(row => row.zone === 'supply' && row.effect !== 'ecaz');
assert.ok(replacement);
const index = inventory.cohort.indexOf(replacement.id); assert.ok(index >= 0);
inventory.cohort[index] = token.id; replacement.zone = 'pool'; token.zone = 'supply';
}
assert.equal(token.zone, 'supply'); validateAmbassadors(inventory);
game.ecazAmbassadors = placeAmbassador(inventory, token.id, {turn:1,
availableSpice:player(game,'ecaz').spice,
destination:{id:'carthag',stronghold:true,inStorm:false,allowed:true}}).state;
ambassador = token.id;
}
const take = (matches: (card: Game['spiceDeck'][number]) => boolean) => {
const index = game.spiceDeck.findIndex(matches); assert.ok(index >= 0);
return game.spiceDeck.splice(index,1)[0];
};
const maker = take(card => 'worm' in card && card.greatMaker === true);
const source = take(card => 'territory' in card && card.territory === 'broken_land' && !card.discovery);
const replacement = take(card => 'territory' in card && !card.discovery);
Object.assign(game, {turn:2, phase:1, storm:18, nexus:false, summonedBeforeBlow:false,
wormRides:[], spiceSequence:null, spiceResolution:null, spiceWindow:null,
response:null, decision:null, phaseOpening:null, ready:[]});
game.spiceDiscard = [[source], []];
game.spiceDeck = [maker, replacement, ...game.spiceDeck];
if (withEcaz && ambassadorEffect !== 'fremen') {
const firstOwner = first === 'terror' ? 'moritani' : 'ecaz';
const secondOwner = first === 'terror' ? 'ecaz' : 'moritani';
game.order = [firstOwner, secondOwner, ...game.order.filter(id => id !== firstOwner && id !== secondOwner)];
}
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
assert.equal(game.greatMaker?.stage, 'vote');
while (game.decision?.kind === 'greatMakerVote') {
const decision = game.decision;
game = act(game, decision.player, {type:'decision',event:decision.event,yes:false});
}
assert.equal(game.decision?.kind, 'greatMakerRide');
const event = game.decision.event;
const originalReserves = player(game,'fremen').reserves;
game = act(game,'fremen',{type:'decision',event,accept:true,
territory:'carthag',sector:11,amount:1,elite:0});
assert.equal(game.greatMaker?.stage, 'arrival');
if (withEcaz && ambassadorEffect !== 'fremen' && first === 'ambassador')
assert.equal(game.pendingAmbassador?.token, ambassador);
else assert.equal(game.pendingTerrorEntry?.token, sneak.id);
assert.equal(player(game,'fremen').reserves, originalReserves - 1);
return {game, event, sneak:sneak.id, ambassador, withBG, withEcaz, first, originalReserves,
destination:'carthag', sector:11};
}
+99
View File
@@ -0,0 +1,99 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { DIFFICULTIES } from '../game/bot-profiles';
import { botActions } from '../game/bots';
import { act, greatMakerSneakFixture, player } from './fixture-great-maker-sneak';
for (const withBG of [false,true]) void test(
`Advanced Great Maker Sneak resolves its own arrival ${withBG ? 'after BG' : 'without BG'} before completing the Maker`, () => {
const f = greatMakerSneakFixture(withBG, withBG);
let game = f.game;
assert.equal(viewGame(game,'moritani').terrorEntry?.sneakAttack?.blocked, null);
game = act(game,'moritani',{type:'decision',reveal:true});
assert.equal(game.pendingTerrorEntry?.stage,'sneakAttack');
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game,'moritani');
view.players.find(row => row.id === 'moritani')!.bot = difficulty;
const choice = botActions(view)[0]; assert.ok(choice, difficulty);
assert.doesNotThrow(() => act(game,'moritani',choice), difficulty);
}
const reserves = player(game,'moritani').reserves;
const spice = player(game,'moritani').spice;
game = act(game,'moritani',{type:'decision',amount:1});
assert.equal(player(game,'moritani').reserves,reserves-1);
assert.equal(player(game,'moritani').forces[`${f.destination}:${f.sector}`],1);
assert.equal(player(game,'moritani').spice,spice);
assert.equal(player(game,'fremen').reserves,f.originalReserves-1);
if (withBG) {
assert.equal(game.greatMaker?.stage,'arrival');
assert.equal(game.decision?.kind,'advisor');
assert.equal(game.decision?.player,'beneGesserit');
game = act(game,'beneGesserit',{type:'decision',accept:false});
const child = game.pendingAmbassador; assert.ok(child);
assert.equal(child.token,f.ambassador);
assert.equal(child.entrant,'moritani');
assert.equal(game.greatMaker?.stage,'arrival');
game = act(game,'ecaz',{type:'decision',event:child.event,decline:true});
}
assert.equal(game.pendingSneakWorm ?? null,null);
assert.equal(game.pendingTerrorEntry ?? null,null);
assert.equal(game.pendingAmbassador ?? null,null);
assert.equal(game.greatMaker?.event,f.event);
assert.equal(game.greatMaker?.stage,'complete');
assert.equal(player(game,'fremen').reserves,f.originalReserves-1);
assert.equal(player(game,'moritani').reserves,reserves-1);
assert.equal(player(game,'moritani').forces[`${f.destination}:${f.sector}`],1);
});
void test('Advanced Great Maker Sneak choosing zero finishes without BG or a child entry', () => {
const f = greatMakerSneakFixture(true, true);
let game = act(f.game,'moritani',{type:'decision',reveal:true});
const reserves = player(game,'moritani').reserves;
game = act(game,'moritani',{type:'decision',amount:0});
assert.equal(player(game,'moritani').reserves,reserves);
assert.equal(game.pendingSneakWorm ?? null,null);
assert.equal(game.pendingTerrorEntry ?? null,null);
assert.equal(game.pendingAmbassador ?? null,null);
assert.notEqual(game.decision?.kind,'advisor');
assert.equal(game.greatMaker?.event,f.event);
assert.equal(game.greatMaker?.stage,'complete');
});
for (const first of ['terror','ambassador'] as const) void test(
`Advanced Great Maker ${first}-first overlap finishes both original owners before the Sneak child`, () => {
const f = greatMakerSneakFixture(false, true, 'emperor', first);
let game = f.game;
assert.equal(game.pendingArrivalOverlap?.first, first);
assert.equal(game.pendingArrivalOverlap?.resume, 'wormRide');
assert.equal(game.greatMaker?.stage, 'arrival');
if (first === 'ambassador') {
const original = game.pendingAmbassador; assert.ok(original);
assert.equal(original.entrant, 'fremen');
game = act(game,'ecaz',{type:'decision',event:original.event,decline:true});
}
assert.equal(game.pendingTerrorEntry?.token, f.sneak);
assert.equal(viewGame(game,'moritani').terrorEntry?.sneakAttack?.blocked, null);
game = act(game,'moritani',{type:'decision',reveal:true});
const reserves = player(game,'moritani').reserves;
game = act(game,'moritani',{type:'decision',amount:1});
assert.equal(game.greatMaker?.stage,'arrival');
if (first === 'terror') {
const original = game.pendingAmbassador; assert.ok(original);
assert.equal(original.entrant, 'fremen');
game = act(game,'ecaz',{type:'decision',event:original.event,decline:true});
}
const child = game.pendingAmbassador; assert.ok(child);
assert.equal(child.token, f.ambassador);
assert.equal(child.entrant, 'moritani');
assert.equal(game.greatMaker?.stage,'arrival');
game = act(game,'ecaz',{type:'decision',event:child.event,decline:true});
assert.equal(game.pendingSneakWorm ?? null,null);
assert.equal(game.pendingArrivalOverlap ?? null,null);
assert.equal(game.pendingAmbassador ?? null,null);
assert.equal(game.greatMaker?.event,f.event);
assert.equal(game.greatMaker?.stage,'complete');
assert.equal(player(game,'moritani').reserves,reserves-1);
assert.equal(player(game,'moritani').forces[`${f.destination}:${f.sector}`],1);
assert.equal(player(game,'fremen').reserves,f.originalReserves-1);
});