Files
Dune/tests/ecaz-duke-ambassador-privacy.test.ts
T
Quintine 22b463a395
Verify and publish checkpoint container / container (push) Successful in 10m31s
Restore safe Duke grants in early legacy games
2026-10-29 19:35:55 +11:00

138 lines
7.9 KiB
TypeScript

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 = <V extends ArrayBufferView | null>(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);
}
});