Resolve Sneak arrivals during Great Maker rides
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
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);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user