Guard Duke grants using public capture uncertainty
Verify and publish checkpoint container / container (push) Successful in 11m21s
Verify and publish checkpoint container / container (push) Successful in 11m21s
This commit is contained in:
1 parent
5f4eeee4f1
commit
57e7611468
15 files changed
+500
-19
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
|
||||
import { nextDukeCaptureUncertainty as next, type DukeCapturePublicSource } from '../game/duke-capture-uncertainty';
|
||||
|
||||
const known: DukeCapturePublicSource = {
|
||||
publicController:'guild', loser:'guild', revealedLeader:'other-leader', publicPlanKnown:true,
|
||||
};
|
||||
|
||||
void test('publicly possible held or just-used Duke makes the capture uncertain', () => {
|
||||
assert.equal(next(false, known), true);
|
||||
assert.equal(next(false, {...known, publicController:null, revealedLeader:DUKE_VIDAL_ID}), true);
|
||||
assert.equal(next(false, {...known, publicController:'ecaz', revealedLeader:'other-leader'}), false);
|
||||
assert.equal(next(false, {...known, publicController:null, revealedLeader:null}), false);
|
||||
});
|
||||
|
||||
void test('uncertainty persists and unstamped historical games are not rewritten', () => {
|
||||
assert.equal(next(true, {...known, publicController:null}), true);
|
||||
assert.equal(next(undefined, known), undefined);
|
||||
assert.equal(next(undefined, {...known, publicController:undefined}), undefined);
|
||||
});
|
||||
|
||||
void test('incomplete or invalid public sources cannot prove Duke was excluded', () => {
|
||||
for (const source of [
|
||||
{...known, publicController:undefined}, {...known, loser:undefined},
|
||||
{...known, loser:''}, {...known, revealedLeader:undefined},
|
||||
{...known, revealedLeader:''}, {...known, publicPlanKnown:false},
|
||||
]) assert.equal(next(false, source), true);
|
||||
});
|
||||
|
||||
void test('publicly equivalent captures stay equivalent regardless of actual selection', () => {
|
||||
const dukeSelected = {...known, actualSelected:DUKE_VIDAL_ID, concealed:true};
|
||||
const otherSelected = {...known, actualSelected:'other-leader', concealed:false};
|
||||
assert.equal(next(false, dukeSelected), next(false, otherSelected));
|
||||
assert.equal(next(false, dukeSelected), true);
|
||||
});
|
||||
@@ -281,15 +281,26 @@ void test('Advanced new-ally Duke grant transfers available shared custody while
|
||||
assert.deepEqual(done.dukeVidal!.leader, duke!.leader);
|
||||
}
|
||||
}
|
||||
const g = fixture(true);
|
||||
g.players[2] = newPlayer('a', 'Harkonnen', 'harkonnen');
|
||||
const entered = enter(g);
|
||||
assert.ok(viewGame(entered, 'ec').ambassadorEntry!.dukeAcquisition!.blocked);
|
||||
assert.equal(
|
||||
viewGame(entered, 'ec').ambassadorEntry!.allianceOffer!.blocked,
|
||||
null,
|
||||
);
|
||||
assert.equal(seat(answer(propose(entered), true), 'ec').ally, 'in');
|
||||
for (const uncertainty of [false, true, undefined] as const) {
|
||||
const g = fixture(true);
|
||||
g.players[2] = newPlayer('a', 'Harkonnen', 'harkonnen');
|
||||
if (uncertainty === undefined) delete g.dukeCaptureUncertain;
|
||||
else g.dukeCaptureUncertain = uncertainty;
|
||||
const duke = json(g).dukeVidal;
|
||||
const entered = enter(g);
|
||||
const entry = viewGame(entered, 'ec').ambassadorEntry!;
|
||||
if (uncertainty === false) assert.equal(entry.dukeAcquisition!.blocked, null);
|
||||
else assert.match(entry.dukeAcquisition!.blocked!, /Harkonnen capture history may be unresolved/);
|
||||
assert.equal(entry.allianceOffer!.blocked, null);
|
||||
const done = answer(propose(entered), true);
|
||||
assert.equal(seat(done, 'ec').ally, 'in');
|
||||
assert.equal(seat(done, 'in').ally, 'ec');
|
||||
if (uncertainty === false) {
|
||||
assert.equal(done.dukeVidal!.controller, 'in');
|
||||
assert.equal(done.dukeVidal!.source, 'ally');
|
||||
assert.deepEqual(done.dukeVidal!.leader, duke!.leader);
|
||||
} else assert.deepEqual(done.dukeVidal, duke, 'An uncertain loan leaves the physical Duke unchanged.');
|
||||
}
|
||||
});
|
||||
|
||||
void test('changing BG private prediction and unrelated private hands does not change the public alliance offer', () => {
|
||||
|
||||
@@ -192,7 +192,7 @@ void test('pure source/canonical identity/custody checks reject safely, with one
|
||||
EcazDukeAcquisitionError,
|
||||
);
|
||||
});
|
||||
void test('redundant same-owner and Advanced Harkonnen cases keep public reasons independent of private custody', () => {
|
||||
void test('redundant same-owner and uncertain Advanced Harkonnen cases keep public reasons independent of private custody', () => {
|
||||
const own = fixture();
|
||||
own.dukeVidal = acquireDuke(own.dukeVidal!, 'ec', 1, 'ecaz');
|
||||
const reason = ecazDukeAcquisitionBlock(own, 'ec');
|
||||
@@ -200,10 +200,15 @@ void test('redundant same-owner and Advanced Harkonnen cases keep public reasons
|
||||
own.dukeVidal.leader.capturedBy = 'in';
|
||||
assert.equal(ecazDukeAcquisitionBlock(own, 'ec'), reason);
|
||||
const hark = fixture('harkonnen', true);
|
||||
assert.equal(hark.dukeCaptureUncertain, false);
|
||||
assert.equal(ecazDukeAcquisitionBlock(hark, 'ec'), null);
|
||||
hark.dukeCaptureUncertain = true;
|
||||
const blocked = ecazDukeAcquisitionBlock(hark, 'ec');
|
||||
assert.match(blocked!, /Advanced Harkonnen/);
|
||||
assert.match(blocked!, /Harkonnen capture history may be unresolved/);
|
||||
hark.dukeVidal!.leader.capturedBy = 'in';
|
||||
assert.equal(ecazDukeAcquisitionBlock(hark, 'ec'), blocked);
|
||||
delete hark.dukeCaptureUncertain;
|
||||
assert.equal(ecazDukeAcquisitionBlock(hark, 'ec'), blocked);
|
||||
const basic = fixture('harkonnen', false);
|
||||
assert.equal(ecazDukeAcquisitionBlock(basic, 'ec'), null);
|
||||
});
|
||||
@@ -316,6 +321,7 @@ void test('explicit Duke choice/owner/event and eligibility are checked before t
|
||||
(b: Game) => {
|
||||
b.advanced = true;
|
||||
p(b, 'in').faction = 'harkonnen';
|
||||
b.dukeCaptureUncertain = true;
|
||||
},
|
||||
]) {
|
||||
const bad = reload(g);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
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<Game['leaderSkills']>;
|
||||
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.');
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
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)));
|
||||
});
|
||||
Reference in new issue
Block a user