138 lines
7.9 KiB
TypeScript
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);
|
|
}
|
|
});
|