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

@@ -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);
});