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