import assert from 'node:assert/strict'; import test from 'node:test'; import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { DUKE_VIDAL_ID } from '../game/duke-vidal'; import { actDukeAmbassador, advanceDukeAmbassador, dukeAmbassadorClean } from './fixture-duke-ambassador'; /** Genuine Advanced setup, placement and shipment. Only unopened Spice cards * and the original scalar lotteries are controlled to avoid a worm detour. */ function nativeArrival() { let game = createGame('PLAINECAZHARKONNENDUKE', newPlayer('ecaz', 'Ecaz', 'ecaz'), true, ['ecaz']); joinGame(game, newPlayer('harkonnen', 'Harkonnen', 'harkonnen')); joinGame(game, newPlayer('guild', 'Guild', 'guild')); const actions: { actor:string; action:Action }[] = []; const act = (actor:string, action:Action) => { game = actDukeAmbassador(game, actor, action, actions); }; const reach = (until:(state:Game)=>boolean) => { game = advanceDukeAmbassador(game, until, actions); }; for (const player of game.players) act(player.id, {type:'ready'}); act(game.host, {type:'start', advancedPreview:true}); reach(state => state.status === 'playing'); assert.equal(game.advanced, true); assert.deepEqual(game.expansions, ['ecaz']); assert.equal(game.dukeCaptureUncertain, false); assert.ok(!game.nexusCards && !game.homeworlds && !game.leaderSkills && !game.discoveryEnabled); const token = game.ecazAmbassadors!.tokens.find(item => item.effect === 'ecaz'); assert.ok(token && token.zone === 'supply'); const originalDuke = structuredClone(game.dukeVidal!.leader); for (const position of [0,1]) { const index = game.spiceDeck.findIndex((card,i) => i >= position && 'territory' in card && !card.discovery); assert.ok(index >= position); [game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]]; } reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement'); act('ecaz', {type:'decision', token:token.id, territory:'arrakeen'}); reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement'); act('ecaz', {type:'decision', decline:true}); reach(state => state.phase === 5 && state.active === 'harkonnen' && dukeAmbassadorClean(state)); const beforeSpice = game.players.find(player => player.id === 'harkonnen')!.spice; act('harkonnen', {type:'ship', amount:1, territory:'arrakeen', sector:10}); reach(state => !state.response && state.pendingAmbassador?.stage === 'offer'); assert.equal(game.pendingAmbassador!.token, token.id); return { game, token, originalDuke, beforeSpice, actions }; } void test('plain Advanced Ecaz may acquire the original Duke on a real Harkonnen arrival before any capture', () => { const source = nativeArrival(); let game = source.game; const {token, originalDuke, beforeSpice, actions} = source; const act = (actor:string, action:Action) => { game = actDukeAmbassador(game, actor, action, actions); }; const reach = (until:(state:Game)=>boolean) => { game = advanceDukeAmbassador(game, until, actions); }; const offer = viewGame(game, 'ecaz').ambassadorEntry; assert.ok(offer); assert.equal(offer.dukeAcquisition?.blocked, null); const event = game.pendingAmbassador!.event; act('ecaz', {type:'decision', event, trigger:true, beneficiary:'ecaz', choice:'duke'}); reach(state => dukeAmbassadorClean(state) && !state.pendingAmbassador); assert.deepEqual(game.dukeVidal!.leader, originalDuke); assert.deepEqual([game.dukeVidal!.controller, game.dukeVidal!.source], ['ecaz','ecaz']); assert.equal(game.ecazAmbassadors!.tokens.find(item => item.id === token.id)!.zone, 'supply'); assert.equal(game.players.find(player => player.id === 'harkonnen')!.forces['arrakeen:10'], 1); assert.ok(game.players.find(player => player.id === 'harkonnen')!.spice < beforeSpice); assert.equal(game.active, 'harkonnen', 'The original shipment and movement turn resumes'); act('harkonnen', {type:'endMovement'}); assert.notEqual(game.active, 'harkonnen'); assert.ok(game.players.every(player => player.leaders.every(leader => leader.id !== DUKE_VIDAL_ID))); }); void test('plain Advanced Ecaz alliance automatically lends the original Duke on Harkonnen acceptance', () => { const source = nativeArrival(); let game = source.game; const {token, originalDuke, actions} = source; const act = (actor:string, action:Action) => { game = actDukeAmbassador(game, actor, action, actions); }; const reach = (until:(state:Game)=>boolean) => { game = advanceDukeAmbassador(game, until, actions); }; const event = game.pendingAmbassador!.event; act('ecaz', {type:'decision', event, trigger:true, beneficiary:'ecaz', choice:'alliance'}); assert.equal(game.pendingAmbassador?.stage, 'allianceReply'); act('harkonnen', {type:'decision', event, accept:true}); reach(state => dukeAmbassadorClean(state) && !state.pendingAmbassador); assert.equal(game.players.find(player => player.id === 'ecaz')!.ally, 'harkonnen'); assert.equal(game.players.find(player => player.id === 'harkonnen')!.ally, 'ecaz'); assert.deepEqual(game.dukeVidal!.leader, originalDuke); assert.deepEqual([game.dukeVidal!.controller, game.dukeVidal!.source], ['harkonnen','ally']); assert.equal(game.ecazAmbassadors!.tokens.find(item => item.id === token.id)!.zone, 'supply'); assert.equal(game.players.find(player => player.id === 'harkonnen')!.forces['arrakeen:10'], 1); assert.equal(game.active, 'harkonnen', 'The original movement turn resumes without a loan acknowledgement'); assert.ok(game.players.every(player => player.leaders.every(leader => leader.id !== DUKE_VIDAL_ID))); });