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 { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { advancedSneakWormFixture, act, player, type WormOrder } from './fixture-advanced-sneak-worm'; function reveal(game: Game): Game { assert.equal(game.pendingTerrorEntry?.stage, 'offer'); assert.equal(viewGame(game, 'm').terrorEntry?.sneakAttack?.blocked, null); game = act(game, 'm', {type:'decision', reveal:true}); assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack'); return game; } for (const withBG of [false,true]) void test( `Advanced worm Sneak resolves its own ${withBG ? 'BG and ' : ''}Ambassador before the next ride`, () => { const f = advancedSneakWormFixture('direct', 'terror', withBG); assert.equal(f.game.pendingArrivalOverlap ?? null, null, 'Fremen cannot trigger its own Ambassador'); assert.equal(f.game.pendingTerrorEntry?.resume, 'wormRide'); let game = reveal(f.game); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, 'm'); view.players.find(player => player.id === 'm')!.bot = difficulty; const choice = botActions(view)[0]; assert.ok(choice, difficulty); assert.doesNotThrow(() => act(game, 'm', choice), difficulty); } const beforeReserves = player(game, 'm').reserves, beforeSpice = player(game, 'm').spice; game = act(game, 'm', {type:'decision', amount:1}); assert.equal(game.phase, 1); assert.equal(player(game, 'm').reserves, beforeReserves - 1); assert.equal(player(game, 'm').forces['arrakeen:10'], 1); assert.equal(player(game, 'm').spice, beforeSpice); assert.notDeepEqual(game.decision, f.nextRide, 'The next ride cannot overwrite this arrival'); if (withBG) { assert.equal(game.decision?.kind, 'advisor'); assert.equal(game.decision?.player, 'bg'); game = act(game, 'bg', {type:'decision', accept:false}); } const child = game.pendingAmbassador; assert.ok(child, 'Moritani earns the still-placed Ambassador'); assert.equal(child.token, f.token); assert.equal(child.entrant, 'm'); game = act(game, 'ec', {type:'decision', event:child.event, decline:true}); assert.deepEqual(game.decision, f.nextRide); assert.deepEqual(game.wormRides, []); assert.equal(game.pendingAmbassador ?? null, null); assert.equal(game.pendingTerrorEntry ?? null, null); assert.equal(player(game, 'm').reserves, beforeReserves - 1); assert.equal(player(game, 'in').forces['arrakeen:10'], 2); game = act(game, 'in', {type:'decision', accept:false}); assert.notEqual(game.decision?.kind, 'wormRide'); assert.deepEqual(game.wormRides, []); }); void test('Advanced worm Sneak choosing zero resumes the ride without a child arrival', () => { const f = advancedSneakWormFixture('direct', 'terror', true); let game = reveal(f.game); const beforeReserves = player(game, 'm').reserves; game = act(game, 'm', {type:'decision', amount:0}); assert.equal(player(game, 'm').reserves, beforeReserves); assert.equal(game.pendingAmbassador ?? null, null); assert.equal(game.pendingTerrorEntry ?? null, null); assert.deepEqual(game.decision, f.nextRide); }); void test('Advanced worm Sneak with no child reaction resumes the next ride once', () => { const f = advancedSneakWormFixture('direct'); const source = structuredClone(f.game); const token = source.ecazAmbassadors!.tokens.find(row => row.id === f.token)!; token.zone = 'supply'; token.location = null; let game = reveal(source); game = act(game, 'm', {type:'decision', amount:1}); assert.deepEqual(game.decision, f.nextRide); assert.equal(game.pendingSneakWorm ?? null, null); assert.equal(game.pendingAmbassador ?? null, null); assert.deepEqual(game.wormRides, []); }); void test('Advanced Sneak on the last worm ride settles its child before ending the ride queue', () => { const f = advancedSneakWormFixture('direct'); const source = structuredClone(f.game); // Remove the fixture's staged extra ride to exercise the final-ride path. source.wormRides = []; let game = reveal(source); game = act(game, 'm', {type:'decision', amount:1}); const child = game.pendingAmbassador; assert.ok(child); assert.equal(child.entrant, 'm'); game = act(game, 'ec', {type:'decision', event:child.event, decline:true}); assert.equal(game.pendingAmbassador ?? null, null); assert.equal(game.pendingTerrorEntry ?? null, null); assert.notEqual(game.decision?.kind, 'wormRide'); assert.deepEqual(game.wormRides, []); assert.equal(player(game, 'm').forces['arrakeen:10'], 1); }); for (const first of ['terror','ambassador'] as const satisfies readonly WormOrder[]) void test( `Advanced worm ${first}-first overlap finishes its original owners before the Sneak child and next ride`, () => { const f = advancedSneakWormFixture('overlap', first); let game = f.game; assert.equal(game.pendingArrivalOverlap?.first, first); assert.equal(game.pendingArrivalOverlap?.resume, 'wormRide'); if (first === 'ambassador') { const original = game.pendingAmbassador; assert.ok(original); game = act(game, 'ec', {type:'decision', event:original.event, decline:true}); } game = reveal(game); const beforeReserves = player(game, 'm').reserves; game = act(game, 'm', {type:'decision', amount:1}); if (first === 'terror') { const original = game.pendingAmbassador; assert.ok(original); assert.equal(original.entrant, 'in'); game = act(game, 'ec', {type:'decision', event:original.event, decline:true}); } const child = game.pendingAmbassador; assert.ok(child); assert.equal(child.entrant, 'm'); assert.equal(child.token, f.token); assert.notDeepEqual(game.decision, f.nextRide); game = act(game, 'ec', {type:'decision', event:child.event, decline:true}); assert.deepEqual(game.decision, f.nextRide); assert.equal(game.pendingArrivalOverlap ?? null, null); assert.equal(player(game, 'm').reserves, beforeReserves - 1); assert.equal(player(game, 'm').forces['arrakeen:10'], 1); }); void test('Advanced worm Guild Ambassador child Sneak resolves before the parent rider resumes', () => { const f = advancedSneakWormFixture('ambassador', 'ambassador'); let game = f.game; const entry = game.pendingAmbassador; assert.ok(entry); assert.equal(game.ecazAmbassadors?.tokens.find(token => token.id === entry.token)?.effect, 'guild'); assert.equal(entry.resume, 'wormRide'); const spice = player(game, 'ec').spice, ecReserves = player(game, 'ec').reserves; game = act(game, 'ec', {type:'decision', event:entry.event, trigger:true, beneficiary:'ec'}); assert.equal(game.pendingAmbassador?.stage, 'ship'); assert.ok(guildAmbassadorShipments(game, 'ec').destinations.some(row => row.territory === f.childTerritory && row.sector === f.childSector)); game = act(game, 'ec', {type:'decision', event:entry.event, amount:1, elite:0, territory:f.childTerritory, sector:f.childSector}); assert.equal(game.pendingTerrorEntry?.resume, 'ambassador'); assert.equal(game.pendingTerrorEntry?.token, f.sneak); game = reveal(game); const reserves = player(game, 'm').reserves; game = act(game, 'm', {type:'decision', amount:1}); assert.equal(game.pendingAmbassador ?? null, null); assert.equal(game.pendingTerrorEntry ?? null, null); assert.deepEqual(game.decision, f.nextRide); assert.equal(player(game, 'm').reserves, reserves - 1); assert.equal(player(game, 'm').forces[`${f.childTerritory}:${f.childSector}`], 1); assert.equal(player(game, 'ec').reserves, ecReserves - 1); assert.equal(player(game, 'ec').spice, spice); assert.equal(player(game, 'ec').forces[`${f.childTerritory}:${f.childSector}`], 1); assert.equal(player(game, 'in').forces['arrakeen:10'], 2); });