import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Game } from '../game/engine'; import { reserveShipmentCost } from '../game/shipment-price'; import { guildAmbassadorShipments } from '../game/guild-ambassador'; import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador'; import { act, player, advancedOverlapChildFixture, type ChildEffect, type First } from './fixture-advanced-overlap-child'; function continueTo(game: Game, until: (state: Game) => boolean): Game { for (let n = 0; n < 100 && !until(game); n++) { 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, `Unanticipated original continuation ${game.decision?.kind ?? game.response?.kind}`); game = act(game, next.actor, next.action); } assert.ok(until(game), 'The native source reached the intended reaction'); return game; } function originalEntry(effect: ChildEffect, first: First, withBG = false) { const f = advancedOverlapChildFixture(effect, first, withBG); const originalSpice = player(f.game, f.entrant).spice; const shipper = player(f.game, f.entrant); const price = reserveShipmentCost({ faction:shipper.faction, halfRate:shipper.faction === 'guild' }, 'stronghold', 1); let game = act(f.game, f.entrant, f.shipment); game = continueTo(game, state => !!state.pendingArrivalOverlap); assert.equal(player(game, f.entrant).spice, originalSpice - price); assert.equal(game.pendingArrivalOverlap?.first, first); if (first === 'terror') { assert.equal(game.pendingTerrorEntry?.entrant, f.entrant); game = act(game, f.moritani, { type:'decision', decline:true }); } const entry = game.pendingAmbassador; assert.ok(entry); assert.equal(entry.token, f.token); assert.equal(viewGame(game, f.owner).ambassadorEntry?.beneficiaries[0].blocked, null); const parentEvent = game.pendingArrivalOverlap!.event; game = act(game, f.owner, { type:'decision', event:entry.event, trigger:true, beneficiary:f.owner }); assert.equal(game.pendingAmbassador?.stage, effect === 'fremen' ? 'move' : 'ship'); return { f, game, parentEvent, entryEvent:entry.event, originalSpice, price }; } for (const effect of ['fremen','guild'] as const) for (const first of ['ambassador','terror'] as const) for (const reveal of [false,true]) void test(`Advanced ${effect} child ${first} first and ${reveal ? 'revealed' : 'declined'} Terror`, () => { const start = originalEntry(effect, first), f = start.f; let game = start.game; const before = player(game, f.owner); const reserves = before.reserves, spice = before.spice; if (effect === 'fremen') { game = act(game, f.owner, { type:'decision', event:start.entryEvent, forces:{ [f.moveFrom]:1 }, territory:f.destination, sector:f.sector }); } else { assert.ok(guildAmbassadorShipments(game, f.owner).destinations.some(option => option.territory === f.destination && option.sector === f.sector)); game = act(game, f.owner, { type:'decision', event:start.entryEvent, amount:1, elite:0, territory:f.destination, sector:f.sector }); } game = continueTo(game, state => state.pendingTerrorEntry?.cause === 'ambassador'); assert.equal(game.pendingTerrorEntry?.entrant, f.owner); assert.equal(game.pendingTerrorEntry?.token, f.terror); assert.equal(game.pendingArrivalOverlap?.event, start.parentEvent); assert.equal(player(game, f.owner).reserves, reserves - (effect === 'guild' ? 1 : 0)); assert.equal(player(game, f.owner).spice, spice); if (reveal) { game = act(game, f.moritani, { type:'decision', reveal:true }); assert.equal(game.pendingTerrorEntry?.stage, 'robbery'); game = act(game, f.moritani, { type:'decision', choice:'spice' }); assert.equal(game.moritaniTerror!.tokens.find(token => token.id === f.terror)?.status, 'removed'); } else game = act(game, f.moritani, { type:'decision', decline:true }); if (first === 'ambassador' && !reveal) { assert.equal(game.pendingTerrorEntry?.entrant, f.entrant); assert.equal(game.pendingTerrorEntry?.token, f.terror); game = act(game, f.moritani, { type:'decision', decline:true }); } assert.equal(game.pendingArrivalOverlap, null); assert.equal(game.pendingAmbassador, null); assert.equal(game.pendingTerrorEntry, null); assert.equal(player(game, f.entrant).spice, start.originalSpice - start.price); assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1); assert.ok((player(game, f.owner).forces[`${f.destination}:${f.sector}`] ?? 0) >= 1); }); void test('Advanced child Sneak and Bene Gesserit accompaniment wait for the original Terror', () => { const start = originalEntry('fremen', 'ambassador', true), f = start.f; assert.ok(f.alternate && f.sneak); const alternate = f.alternate; let game = act(start.game, f.owner, { type:'decision', event:start.entryEvent, forces:{ [alternate.sector.key]:1 }, territory:alternate.destination.territory, sector:alternate.destination.sector }); game = continueTo(game, state => state.pendingTerrorEntry?.token === f.sneak); const reserves = player(game, f.moritani).reserves; game = act(game, f.moritani, { type:'decision', reveal:true }); assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack'); game = act(game, f.moritani, { type:'decision', amount:1 }); assert.equal(player(game, f.moritani).reserves, reserves - 1); assert.equal(game.pendingTerrorEntry?.token, f.terror); assert.equal(game.pendingTerrorEntry?.entrant, f.entrant); assert.equal(game.decision?.kind, 'moritaniTerror'); game = act(game, f.moritani, { type:'decision', decline:true }); assert.equal(game.pendingArrivalOverlap, null); assert.equal(game.decision?.kind, 'advisor'); assert.equal(game.decision?.player, 'beneGesserit'); game = act(game, 'beneGesserit', { type:'decision', accept:false }); assert.equal(game.pendingAmbassador, null); assert.equal(game.pendingTerrorEntry, null); assert.equal(player(game, f.moritani).forces[`${alternate.destination.territory}:${alternate.destination.sector}`], 1); assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1); });