import assert from 'node:assert/strict'; import test from 'node:test'; import { createGame, newPlayer, type Game } from '../game/engine'; import { acquireDuke, captureDuke, createDukeVidal } from '../game/duke-vidal'; import { ecazDukeAcquisitionBlock, quoteEcazDukeAcquisition } from '../game/ecaz-duke-acquisition'; import { DUKE_CAPTURE_UNCERTAIN_BLOCK, dukeCaptureUncertaintyBlock, plainAdvancedEcazDukeProfile } from '../game/ecaz-duke-capture-policy'; function table(): Game { const game = createGame('DUKEPOLICY', newPlayer('e', 'Ecaz', 'ecaz'), true, ['ecaz']); game.players.push(newPlayer('h', 'Harkonnen', 'harkonnen'), newPlayer('g', 'Guild', 'guild')); game.status = 'playing'; game.turn = 2; game.dukeVidal = createDukeVidal(); game.dukeCaptureUncertain = false; return game; } void test('a known-safe plain Advanced table can acquire the original living Duke', () => { const game = table(), before = structuredClone(game); assert.equal(plainAdvancedEcazDukeProfile(game), true); assert.equal(dukeCaptureUncertaintyBlock(game), null); const next = quoteEcazDukeAcquisition(game, 'e'); assert.equal(next.controller, 'e'); assert.equal(next.leader.id, game.dukeVidal!.leader.id); assert.deepEqual(game, before, 'A quote does not change custody.'); const ecaz = game.players.find(player => player.id === 'e')!; const guild = game.players.find(player => player.id === 'g')!; ecaz.ally = 'g'; guild.ally = 'e'; assert.equal(quoteEcazDukeAcquisition(game, 'e', 'g').controller, 'g'); }); void test('uncertain and legacy Advanced tables block all hidden custody identically', () => { const free = table(); free.dukeVidal = acquireDuke(free.dukeVidal!, 'g', free.turn, 'ecaz'); const captured = structuredClone(free); captured.dukeVidal = captureDuke(captured.dukeVidal!, 'h', 'g', captured.turn, 'arrakeen'); for (const uncertain of [true, undefined] as const) { for (const game of [free, captured]) { if (uncertain === undefined) delete game.dukeCaptureUncertain; else game.dukeCaptureUncertain = uncertain; const before = structuredClone(game); assert.equal(ecazDukeAcquisitionBlock(game, 'e'), DUKE_CAPTURE_UNCERTAIN_BLOCK); assert.throws(() => quoteEcazDukeAcquisition(game, 'e')); assert.deepEqual(game, before); } } }); void test('uncertainty does not change Basic or tables without Harkonnen', () => { const basic = table(); basic.advanced = false; delete basic.dukeCaptureUncertain; assert.equal(dukeCaptureUncertaintyBlock(basic), null); assert.equal(quoteEcazDukeAcquisition(basic, 'e').controller, 'e'); const noHarkonnen = table(); noHarkonnen.players = noHarkonnen.players.filter(player => player.id !== 'h'); delete noHarkonnen.dukeCaptureUncertain; assert.equal(dukeCaptureUncertaintyBlock(noHarkonnen), null); assert.equal(quoteEcazDukeAcquisition(noHarkonnen, 'e').controller, 'e'); }); void test('the plain profile keeps optional modules separate and validates the selected roster', () => { const game = table(); assert.equal(plainAdvancedEcazDukeProfile(game), true); const mixed = structuredClone(game); mixed.expansions.push('ix'); mixed.players.push(newPlayer('i', 'Ixians', 'ixians')); assert.equal(plainAdvancedEcazDukeProfile(mixed), true); mixed.sandtrout = true; assert.equal(plainAdvancedEcazDukeProfile(mixed), true, 'Active native Sandtrout is not an optional selection.'); mixed.leaderSkills = {} as NonNullable; assert.equal(plainAdvancedEcazDukeProfile(mixed), false); assert.match(ecazDukeAcquisitionBlock(mixed, 'e')!, /Advanced Harkonnen/); delete mixed.leaderSkills; mixed.expansions = ['ecaz']; assert.equal(plainAdvancedEcazDukeProfile(mixed), false, 'An unselected expansion cannot seat Ixians.'); });