import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Game } from '../game/engine'; import { acquireDuke, captureDuke, DUKE_VIDAL_ID } from '../game/duke-vidal'; import { createNexusDukeCaptureFixture } from './fixture-nexus-duke-capture'; /** The Ambassador placement and offer are reached by real actions. We stage * two alternate custody outcomes for the same physical disc and parent event: * a living third-party Duke and its legal concealed Harkonnen capture. */ function paired() { const source = createNexusDukeCaptureFixture(); const free = structuredClone(source.arrival.before); const priorController = source.others.find(id => id !== source.captor && id !== source.entrant); assert.ok(priorController, 'A third party owns the staged prior Duke'); const prior = acquireDuke(free.dukeVidal!, priorController, free.turn, 'ecaz'); free.dukeVidal = prior; const captured: Game = structuredClone(free); captured.dukeVidal = captureDuke(prior, source.captor, priorController, free.turn, 'carthag'); // Public history records a possible hidden Duke capture, regardless of // whether this physical disc was the captive actually selected. Object.assign(free, { dukeCaptureUncertain: true }); Object.assign(captured, { dukeCaptureUncertain: true }); assert.equal(captured.pendingAmbassador?.event, free.pendingAmbassador?.event); const freeView = viewGame(free, source.owner), capturedView = viewGame(captured, source.owner); for (const key of Object.keys(freeView) as (keyof typeof freeView)[]) { if (key !== 'ambassadorEntry') assert.deepEqual(capturedView[key], freeView[key], `Public-equivalent ${key}`); } return { source, free, captured }; } void test('Ecaz Ambassador availability must not reveal a concealed Duke captured from another controller', () => { const {source, free, captured} = paired(); const freeEntry = viewGame(free, source.owner).ambassadorEntry; const hiddenEntry = viewGame(captured, source.owner).ambassadorEntry; assert.ok(freeEntry && hiddenEntry); const action = {type:'decision', event:free.pendingAmbassador!.event, trigger:true, beneficiary:source.owner, choice:'duke'}; for (const game of [free, captured]) { const before = structuredClone(game); assert.throws(() => applyAction(game, source.owner, action)); assert.deepEqual(game, before, 'The unavailable action preserves the physical Ambassador and Duke'); } assert.deepEqual(hiddenEntry, freeEntry, 'The same public Duke and parent event must offer identical controls'); assert.ok(freeEntry.dukeAcquisition?.blocked, 'The same public uncertainty uniformly blocks the acquisition'); }); void test('an actual fresh Ambassador grant stays available before public capture uncertainty', () => { const source = createNexusDukeCaptureFixture(), game = structuredClone(source.arrival.before); assert.equal(viewGame(game, source.owner).ambassadorEntry?.dukeAcquisition?.blocked, null); const after = applyAction(game, source.owner, source.arrival.step.action); assert.equal(after.dukeVidal?.controller, source.owner); assert.equal(after.dukeVidal?.source, 'ecaz'); }); void test('a fresh Advanced Ecaz alliance grants the unambiguous Duke without another choice', () => { const source = createNexusDukeCaptureFixture(), game = structuredClone(source.arrival.before); const event = game.pendingAmbassador!.event; const offered = applyAction(game, source.owner, {type:'decision', event, trigger:true, beneficiary:source.owner, choice:'alliance'}); assert.equal(offered.pendingAmbassador?.stage, 'allianceReply'); const accepted = applyAction(offered, source.entrant, {type:'decision', event, accept:true}); assert.equal(accepted.players.find(player => player.id === source.owner)?.ally, source.entrant); assert.equal(accepted.dukeVidal?.controller, source.entrant); assert.equal(accepted.dukeVidal?.source, 'ally'); assert.notEqual(accepted.pendingAmbassador?.stage, 'loan'); }); void test('an Advanced alliance does not reveal uncertain Duke custody through its automatic grant', () => { const {source, free, captured} = paired(); const results: Game[] = []; for (const game of [free, captured]) { const originalDuke = structuredClone(game.dukeVidal); const event = game.pendingAmbassador!.event; const offered = applyAction(game, source.owner, {type:'decision', event, trigger:true, beneficiary:source.owner, choice:'alliance'}); const accepted = applyAction(offered, source.entrant, {type:'decision', event, accept:true}); assert.equal(accepted.players.find(player => player.id === source.owner)?.ally, source.entrant); assert.deepEqual(accepted.dukeVidal, originalDuke, 'No automatic grant changes either actual custody'); assert.notEqual(accepted.pendingAmbassador?.stage, 'loan'); results.push(accepted); } assert.deepEqual(viewGame(results[0], source.owner), viewGame(results[1], source.owner)); assert.deepEqual(viewGame(results[0], source.entrant), viewGame(results[1], source.entrant)); }); void test('a real Duke-possible capture marks uncertainty for either concealed selection', () => { const source = createNexusDukeCaptureFixture({variant:'used'}); assert.ok(source.offer); assert.ok(source.candidates.some(leader => leader.id === DUKE_VIDAL_ID)); assert.ok(source.candidates.some(leader => leader.id !== DUKE_VIDAL_ID)); assert.equal(source.offer.before.dukeCaptureUncertain, false); assert.equal(source.offer.after.decision?.kind, 'capturedLeader'); if (source.offer.after.decision?.kind !== 'capturedLeader') throw Error('Missing capture selection'); assert.equal(source.offer.after.decision.leader, DUKE_VIDAL_ID); assert.equal(source.offer.after.dukeCaptureUncertain, true); assert.equal(source.game.dukeCaptureUncertain, true); // Select a different original candidate solely by controlled shuffle entropy, // replaying the same real public capture and preserving all physical discs. const saved = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues'); const native = crypto.getRandomValues.bind(crypto); try { crypto.getRandomValues = (array: V): V => { if (array instanceof Uint32Array) { array.fill(0); return array; } return Reflect.apply(native, crypto, [array]); }; const different = applyAction(structuredClone(source.offer.before), source.offer.step.actor, source.offer.step.action); assert.equal(different.decision?.kind, 'capturedLeader'); if (different.decision?.kind !== 'capturedLeader') throw Error('Missing capture selection'); assert.notEqual(different.decision.leader, DUKE_VIDAL_ID); assert.equal(different.dukeCaptureUncertain, true, 'The public marker cannot depend on hidden selection'); } finally { if (saved) Object.defineProperty(crypto, 'getRandomValues', saved); else Reflect.deleteProperty(crypto, 'getRandomValues'); } }); void test('legacy games without the capture marker uniformly guard equivalent Duke custody', () => { const {source, free, captured} = paired(); // The paired fixture stages an alternate prior capture. That history cannot // occur before the first Battle, so place both public-equivalent outcomes // at a later turn before removing the legacy marker. free.turn = 2; captured.turn = 2; delete free.dukeCaptureUncertain; delete captured.dukeCaptureUncertain; const freeEntry = viewGame(free, source.owner).ambassadorEntry; const hiddenEntry = viewGame(captured, source.owner).ambassadorEntry; assert.ok(freeEntry && hiddenEntry); assert.deepEqual(hiddenEntry, freeEntry); assert.ok(freeEntry.dukeAcquisition?.blocked, 'Unknown legacy capture history must not be assumed safe'); const action = {type:'decision', event:free.pendingAmbassador!.event, trigger:true, beneficiary:source.owner, choice:'duke'}; for (const game of [free, captured]) { const before = structuredClone(game); assert.throws(() => applyAction(game, source.owner, action)); assert.deepEqual(game, before); } });