Resolve direct advanced Sneak arrivals
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 19:48:35 +11:00
1 parent 8a39f5a172
commit b79e448deb
8 files changed
+213 -8

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { guildAmbassadorShipments } from '../game/guild-ambassador';
import { reserveShipmentCost } from '../game/shipment-price';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
import { act, player } from './fixture-advanced-overlap-child';
import { advancedDirectSneakFixture } from './fixture-advanced-sneak-direct';
function advanceTo(game: Game, until: (state: Game) => boolean): Game {
for (let i = 0; i < 60 && !until(game); i++) {
if (game.decision?.kind === 'advisor') {
game = act(game, game.decision.player, {type:'decision', accept:false});
continue;
}
if (game.decision?.kind === 'guildShipment') {
game = act(game, game.decision.player, {type:'decision', allow:true});
continue;
}
const next = nextBasicSemutaAmbassadorStep(game);
assert.ok(next, `The original arrival must continue: ${game.decision?.kind ?? game.response?.kind}`);
game = act(game, next.actor, next.action);
}
assert.ok(until(game), 'The requested native arrival reaction is reached');
return game;
}
function revealSneak(withBG: boolean) {
const f = advancedDirectSneakFixture(withBG);
const originalSpice = player(f.game, f.entrant).spice;
const price = reserveShipmentCost({faction:'guild', halfRate:true}, 'stronghold', 1);
let game = act(f.game, f.entrant, f.shipment);
game = advanceTo(game, state => !!state.pendingTerrorEntry);
assert.equal(game.pendingArrivalOverlap ?? null, null, 'Guild cannot trigger its own Ambassador');
assert.equal(game.pendingTerrorEntry?.token, f.sneak);
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
game = act(game, f.moritani, {type:'decision', reveal:true});
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
return {f, game, originalSpice, price};
}
for (const withBG of [false, true]) void test(
`Advanced direct Sneak opens the Moritani Ambassador entry ${withBG ? 'after BG accompaniment' : 'without BG'}`, () => {
const {f, game: before, originalSpice, price} = revealSneak(withBG);
const reserves = player(before, f.moritani).reserves;
let game = act(before, f.moritani, {type:'decision', amount:1});
if (withBG) {
assert.equal(game.decision?.kind, 'advisor');
assert.equal(game.decision?.player, 'beneGesserit');
game = act(game, 'beneGesserit', {type:'decision', accept:false});
}
game = advanceTo(game, state => !!state.pendingAmbassador);
const entry = game.pendingAmbassador; assert.ok(entry);
assert.equal(entry.token, f.ambassador);
assert.equal(entry.entrant, f.moritani);
assert.equal(player(game, f.moritani).reserves, reserves - 1);
assert.equal(player(game, f.moritani).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, 'ecaz');
view.players.find(p => p.id === 'ecaz')!.bot = difficulty;
const action = botActions(view)[0]; assert.ok(action, difficulty);
assert.doesNotThrow(() => act(game, 'ecaz', action), difficulty);
}
game = act(game, 'ecaz', {type:'decision', event:entry.event, decline:true});
assert.equal(game.pendingAmbassador, null);
assert.equal(game.pendingTerrorEntry, null);
assert.equal(game.pendingArrivalOverlap ?? null, null);
assert.equal(player(game, f.moritani).reserves, reserves - 1);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
});
void test('Advanced direct Sneak sending zero opens no separate entry or accompaniment', () => {
const {f, game: before, originalSpice, price} = revealSneak(true);
const reserves = player(before, f.moritani).reserves;
const game = act(before, f.moritani, {type:'decision', amount:0});
assert.equal(player(game, f.moritani).reserves, reserves);
assert.equal(game.pendingAmbassador ?? null, null);
assert.equal(game.pendingTerrorEntry, null);
assert.notEqual(game.decision?.kind, 'advisor');
assert.equal(player(game, f.entrant).spice, originalSpice - price);
});
void test('Advanced direct Sneak can trigger the retained Guild Ambassador exactly once', () => {
const {f, game: before, originalSpice, price} = revealSneak(false);
const reserves = player(before, f.owner).reserves, spice = player(before, f.owner).spice;
let game = act(before, f.moritani, {type:'decision', amount:1});
game = advanceTo(game, state => !!state.pendingAmbassador);
const entry = game.pendingAmbassador; assert.ok(entry);
game = act(game, f.owner, {type:'decision', event:entry.event, trigger:true, beneficiary:f.owner});
assert.equal(game.pendingAmbassador?.stage, 'ship');
const target = guildAmbassadorShipments(game, f.owner).destinations.find(row => row.territory === 'polar_sink');
assert.ok(target);
game = act(game, f.owner, {type:'decision', event:entry.event, amount:1, elite:0,
territory:target.territory, sector:target.sector});
game = advanceTo(game, state => !state.pendingAmbassador);
assert.equal(game.pendingAmbassador, null);
assert.equal(game.pendingTerrorEntry, null);
assert.equal(player(game, f.owner).reserves, reserves - 1);
assert.equal(player(game, f.owner).spice, spice);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(game.ecazAmbassadors?.tokens.find(token => token.id === f.ambassador)?.zone, 'used');
});
@@ -69,6 +69,20 @@ function ready(state: Game) {
}
const queuedRide = { kind: 'wormRide', player: 'in', territory: 'hagga_basin' } as const;
void test('Advanced worm Sneak shows the temporary positive-entry guard and preserves the ride on zero', () => {
const { g } = table('sneakAttack');
let state = wormRide(g);
assert.match(viewGame(state, 'm').terrorEntry?.sneakAttack?.blocked ?? '', /worm ride/);
state = action(state, 'm', {type:'decision', reveal:true});
const before = structuredClone(state);
assert.throws(() => action(state, 'm', {type:'decision', amount:1}), /worm ride/);
assert.deepEqual(state, before);
state = action(state, 'm', {type:'decision', amount:0});
assert.equal(state.pendingArrivalOverlap?.stage, 'second');
state = action(state, 'ec', {type:'decision', event:state.pendingAmbassador!.event, decline:true});
assert.deepEqual(state.decision, queuedRide);
});
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);
+34
View File
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import { validateAmbassadors } from '../game/ecaz-ambassadors';
import { advancedOverlapChildFixture } from './fixture-advanced-overlap-child';
/** Genuine Advanced native lobby/setup and Guild shipment from the shared
* fixture. Select the existing Guild Ambassador and Sneak Attack markers at
* the earned clean turn boundary; no forces, spice or continuation are staged.
* The Guild entrant cannot trigger its own Ambassador, but Moritani can. */
export function advancedDirectSneakFixture(withBG: boolean) {
const f = advancedOverlapChildFixture('fremen', 'terror', withBG);
const game = structuredClone(f.game);
const ambassadors = game.ecazAmbassadors!;
const old = ambassadors.tokens.find(token => token.id === f.token);
const guild = ambassadors.tokens.find(token => token.effect === 'guild');
assert.ok(old && guild && old.zone === 'placed');
old.zone = 'supply'; old.location = null;
if (guild.zone === 'pool') {
const index = ambassadors.cohort.indexOf(old.id); assert.ok(index >= 0);
ambassadors.cohort[index] = guild.id;
old.zone = 'pool'; guild.zone = 'supply';
}
assert.equal(guild.zone, 'supply');
guild.zone = 'placed'; guild.location = f.destination;
validateAmbassadors(ambassadors);
const robbery = game.moritaniTerror!.tokens.find(token => token.id === f.terror);
const sneak = game.moritaniTerror!.tokens.find(token => token.kind === 'sneakAttack');
assert.ok(robbery && sneak);
robbery.status = 'available'; robbery.location = null;
// The six-seat shared fixture may already have staged Sneak at a second
// site. Move that same physical token rather than creating another one.
assert.ok(sneak.status === 'available' || sneak.status === 'placed');
sneak.status = 'placed'; sneak.location = f.destination;
return { ...f, game, ambassador:guild.id, sneak:sneak.id };
}