Connect original selected-deck Discovery setup, optional Nexus/World/Skills/phase modules and physical card consumers through shared public capabilities. Retain Cunning revealed-location arrival, Sandtrout suppression, paid nested R/Harass and actual later Recruits revival. Preserve native trained-capture and unrelated-preview boundaries. Validation: frozen 15-file118/118, types/lint/build and six finite original-action/rendered-control programmes; four minimal legal policies. External smoke corrections changed no application source. Existing persistent notices also rechecked23/23. Full combinations, assurance and deployed acceptance remain open; no saved-game, restart or deployment operation.
561 lines
19 KiB
TypeScript
561 lines
19 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, createGame, newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { FACTIONS, type FactionId } from '../game/catalog';
|
|
import {
|
|
createDukeVidal,
|
|
acquireDuke,
|
|
consumeDuke,
|
|
DUKE_VIDAL_ID,
|
|
} from '../game/duke-vidal';
|
|
import { newRevivalRules, revivalDiscount } from '../game/revival';
|
|
import {
|
|
resolveEcazDukeRevival,
|
|
quoteEcazDukeRevival,
|
|
ecazDukeRevivalBlock,
|
|
EcazDukeRevivalError,
|
|
DUKE_REVIVAL_NORMAL_COST,
|
|
} from '../game/ecaz-duke-revival';
|
|
import { baseDeck } from '../game/cards';
|
|
import { quoteRevivalCancellation } from '../game/revival-cancellation';
|
|
import { botActions } from '../game/bots';
|
|
const p = (g: Game, id: string) => g.players.find((p) => p.id === id)!;
|
|
function fixture(advanced = false) {
|
|
const g = createGame(
|
|
'DUKEREVIVAL',
|
|
newPlayer('ec', 'Ecaz', 'ecaz'),
|
|
advanced,
|
|
['ecaz'],
|
|
);
|
|
g.players.push(newPlayer('m', 'Moritani', 'moritani'));
|
|
Object.assign(g, {
|
|
status: 'playing',
|
|
phase: 4,
|
|
turn: 3,
|
|
order: ['ec', 'm'],
|
|
dukeVidal: createDukeVidal(),
|
|
revivalRules: newRevivalRules(),
|
|
});
|
|
g.dukeVidal!.leader.dead = true;
|
|
g.dukeVidal!.leader.deaths = 1;
|
|
return g;
|
|
}
|
|
void test('independent Discovery without Nexus admits the untrained Advanced Ecaz/Harkonnen revival quote', () => {
|
|
const g = fixture(true);
|
|
g.players[1] = newPlayer('h', 'Harkonnen', 'harkonnen');
|
|
g.ecazTreachery = true;
|
|
g.discoveryEnabled = true;
|
|
assert.equal(g.nexusCards, undefined);
|
|
assert.equal(quoteEcazDukeRevival(g, 'ec', false).cost, 5);
|
|
const excluded = { ...g, leaderSkills: {} };
|
|
assert.throws(() => quoteEcazDukeRevival(excluded as Game, 'ec', false), EcazDukeRevivalError);
|
|
});
|
|
void test('canonical dead shared Duke resolves without manufacturing or reviving it and always has printed normal price five', () => {
|
|
for (const advanced of [false, true]) {
|
|
const g = fixture(advanced);
|
|
g.dukeVidal!.leader.deaths = 4;
|
|
g.dukeVidal!.leader.usedAt = 'carthag';
|
|
const before = structuredClone(g),
|
|
oldRandom = Math.random;
|
|
let q;
|
|
try {
|
|
Math.random = () => {
|
|
throw new Error('shared identity quote must not sample');
|
|
};
|
|
q = resolveEcazDukeRevival(g, 'ec');
|
|
} finally {
|
|
Math.random = oldRandom;
|
|
}
|
|
assert.deepEqual(q, {
|
|
source: 'sharedDuke',
|
|
player: 'ec',
|
|
leader: DUKE_VIDAL_ID,
|
|
normalCost: 5,
|
|
duke: g.dukeVidal,
|
|
});
|
|
assert.equal(q.duke.leader.dead, true);
|
|
assert.equal(q.duke.controller, null);
|
|
assert.equal(q.duke.leader.strength, 6);
|
|
assert.equal(DUKE_REVIVAL_NORMAL_COST, 5);
|
|
assert.deepEqual(g, before);
|
|
q.duke.leader.deaths = 99;
|
|
q.duke.controller = 'ec';
|
|
assert.deepEqual(g, before);
|
|
}
|
|
});
|
|
void test('all eleven non-Ecaz factions are rejected, including an old Moritani controller and Tleilaxu foreign-ghola actor', () => {
|
|
for (const faction of FACTIONS.filter((f) => f.id !== 'ecaz')) {
|
|
const g = fixture();
|
|
g.players[1] = newPlayer('other', faction.name, faction.id as FactionId);
|
|
if (faction.id === 'moritani') {
|
|
g.dukeVidal = acquireDuke(createDukeVidal(), 'other', 1, 'moritani');
|
|
g.dukeVidal.leader.dead = true;
|
|
g.dukeVidal.leader.deaths = 1;
|
|
}
|
|
const before = structuredClone(g);
|
|
assert.throws(
|
|
() => resolveEcazDukeRevival(g, 'other'),
|
|
EcazDukeRevivalError,
|
|
faction.id,
|
|
);
|
|
assert.throws(
|
|
() => quoteEcazDukeRevival(g, 'other', false),
|
|
EcazDukeRevivalError,
|
|
faction.id,
|
|
);
|
|
assert.deepEqual(g, before);
|
|
}
|
|
const absent = fixture();
|
|
absent.players = absent.players.filter((p) => p.id !== 'ec');
|
|
assert.throws(
|
|
() => resolveEcazDukeRevival(absent, 'ec'),
|
|
EcazDukeRevivalError,
|
|
);
|
|
});
|
|
void test('existing native death count, phase, ordinary slot, prevention and private resource fields are outside identity resolution', () => {
|
|
const g = fixture();
|
|
for (const phase of [0, 4, 6, 8])
|
|
for (const deadCount of [0, 4, 5]) {
|
|
g.phase = phase;
|
|
p(g, 'ec').leaderRevived = true;
|
|
p(g, 'ec').revivalCycle = 7;
|
|
g.revivalPrevention = { turn: g.turn, player: 'ec' };
|
|
p(g, 'ec').leaders.forEach((l, i) => {
|
|
l.dead = i < deadCount;
|
|
l.deaths = l.dead ? 1 : 0;
|
|
});
|
|
const before = structuredClone(g);
|
|
assert.equal(resolveEcazDukeRevival(g, 'ec').normalCost, 5);
|
|
assert.deepEqual(g, before);
|
|
}
|
|
// Getters establish that the identity guard does not inspect denial/price
|
|
// eligibility or hidden native histories to infer whether it may revive.
|
|
for (const key of ['phase', 'revivalPrevention', 'revivalRules'] as const)
|
|
Object.defineProperty(g, key, {
|
|
get() {
|
|
throw new Error('not an identity prerequisite');
|
|
},
|
|
});
|
|
for (const seat of g.players) {
|
|
for (const key of [
|
|
'spice',
|
|
'hand',
|
|
'traitors',
|
|
'leaderRevived',
|
|
'revivalCycle',
|
|
'ally',
|
|
] as const)
|
|
Object.defineProperty(seat, key, {
|
|
get() {
|
|
throw new Error('not an identity prerequisite');
|
|
},
|
|
});
|
|
for (const leader of seat.leaders)
|
|
for (const key of ['dead', 'deaths', 'capturedBy', 'gholaBy'] as const)
|
|
Object.defineProperty(leader, key, {
|
|
get() {
|
|
throw new Error('private native history');
|
|
},
|
|
});
|
|
}
|
|
assert.equal(resolveEcazDukeRevival(g, 'ec').leader, DUKE_VIDAL_ID);
|
|
});
|
|
void test('declared discount composes the existing Tleilaxu rule to three spice without changing strength or the normal five-spice quote', () => {
|
|
const g = fixture(true);
|
|
g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
|
|
p(g, 't').ally = 'ec';
|
|
p(g, 'ec').ally = 't';
|
|
g.revivalRules!.allyDiscount = 'ec';
|
|
const discounted = quoteEcazDukeRevival(
|
|
g,
|
|
'ec',
|
|
!!revivalDiscount(g, p(g, 'ec')),
|
|
);
|
|
assert.equal(discounted.cost, 3);
|
|
assert.equal(discounted.normalCost, 5);
|
|
assert.equal(discounted.duke.leader.strength, 6);
|
|
g.revivalRules!.discountBlocked = true;
|
|
const full = quoteEcazDukeRevival(g, 'ec', !!revivalDiscount(g, p(g, 'ec')));
|
|
assert.equal(full.cost, 5);
|
|
assert.equal(full.normalCost, 5);
|
|
assert.equal(
|
|
discounted.cost,
|
|
3,
|
|
'a prior detached quote is not repriced by reading current entitlement',
|
|
);
|
|
assert.equal(resolveEcazDukeRevival(g, 'ec').normalCost, 5);
|
|
assert.throws(
|
|
() => quoteEcazDukeRevival(g, 'ec', undefined as never),
|
|
EcazDukeRevivalError,
|
|
);
|
|
assert.throws(
|
|
() => quoteEcazDukeRevival(g, 'ec', 1 as never),
|
|
EcazDukeRevivalError,
|
|
);
|
|
});
|
|
void test('missing, duplicate, forged and malformed shared physical history are rejected with one generic unavailable reason', () => {
|
|
const changes: [string, (g: Game) => void][] = [
|
|
[
|
|
'missing',
|
|
(g) => {
|
|
delete g.dukeVidal;
|
|
},
|
|
],
|
|
[
|
|
'native duplicate',
|
|
(g) => {
|
|
p(g, 'ec').leaders.push(structuredClone(g.dukeVidal!.leader));
|
|
},
|
|
],
|
|
[
|
|
'foreign duplicate',
|
|
(g) => {
|
|
p(g, 'm').leaders.push(structuredClone(g.dukeVidal!.leader));
|
|
},
|
|
],
|
|
[
|
|
'wrong ID',
|
|
(g) => {
|
|
g.dukeVidal!.leader.id = 'ecaz-1';
|
|
},
|
|
],
|
|
[
|
|
'wrong faction',
|
|
(g) => {
|
|
g.dukeVidal!.leader.faction = 'moritani';
|
|
},
|
|
],
|
|
[
|
|
'wrong strength',
|
|
(g) => {
|
|
g.dukeVidal!.leader.strength = 5;
|
|
},
|
|
],
|
|
[
|
|
'alive',
|
|
(g) => {
|
|
g.dukeVidal!.leader.dead = false;
|
|
},
|
|
],
|
|
[
|
|
'zero deaths',
|
|
(g) => {
|
|
g.dukeVidal!.leader.deaths = 0;
|
|
},
|
|
],
|
|
[
|
|
'negative deaths',
|
|
(g) => {
|
|
g.dukeVidal!.leader.deaths = -1;
|
|
},
|
|
],
|
|
[
|
|
'unsafe deaths',
|
|
(g) => {
|
|
g.dukeVidal!.leader.deaths = Number.MAX_SAFE_INTEGER + 1;
|
|
},
|
|
],
|
|
[
|
|
'fractional deaths',
|
|
(g) => {
|
|
g.dukeVidal!.leader.deaths = 1.5;
|
|
},
|
|
],
|
|
[
|
|
'malformed usedAt',
|
|
(g) => {
|
|
g.dukeVidal!.leader.usedAt = 5 as never;
|
|
},
|
|
],
|
|
[
|
|
'unknown controller',
|
|
(g) => {
|
|
g.dukeVidal!.controller = 'absent';
|
|
},
|
|
],
|
|
[
|
|
'missing source',
|
|
(g) => {
|
|
g.dukeVidal!.controller = 'm';
|
|
g.dukeVidal!.acquiredTurn = 1;
|
|
},
|
|
],
|
|
[
|
|
'orphan turn',
|
|
(g) => {
|
|
g.dukeVidal!.acquiredTurn = 1;
|
|
},
|
|
],
|
|
[
|
|
'future custody',
|
|
(g) => {
|
|
g.dukeVidal!.controller = 'm';
|
|
g.dukeVidal!.source = 'moritani';
|
|
g.dukeVidal!.acquiredTurn = 4;
|
|
},
|
|
],
|
|
];
|
|
for (const [label, change] of changes) {
|
|
const g = fixture();
|
|
change(g);
|
|
const before = structuredClone(g);
|
|
assert.throws(
|
|
() => resolveEcazDukeRevival(g, 'ec'),
|
|
EcazDukeRevivalError,
|
|
label,
|
|
);
|
|
assert.equal(
|
|
ecazDukeRevivalBlock(g, 'ec'),
|
|
'Duke Vidal is unavailable for this revival.',
|
|
label,
|
|
);
|
|
assert.deepEqual(g, before);
|
|
}
|
|
});
|
|
void test('captured, ghola and concealed records give identical reasons independent of hidden dead/capture variants', () => {
|
|
const descriptors = new Set<string | null>();
|
|
for (const marker of ['capturedBy', 'gholaBy', 'concealed'] as const)
|
|
for (const dead of [false, true]) {
|
|
const g = fixture();
|
|
g.dukeVidal!.leader.dead = dead;
|
|
if (marker === 'concealed')
|
|
g.dukeVidal!.leader.concealed = {
|
|
captor: 'm',
|
|
controller: 'ec',
|
|
dead: true,
|
|
deaths: 1,
|
|
};
|
|
else g.dukeVidal!.leader[marker] = 'm';
|
|
const before = structuredClone(g);
|
|
descriptors.add(ecazDukeRevivalBlock(g, 'ec'));
|
|
assert.throws(
|
|
() => resolveEcazDukeRevival(g, 'ec'),
|
|
EcazDukeRevivalError,
|
|
);
|
|
assert.deepEqual(g, before);
|
|
}
|
|
assert.deepEqual(
|
|
[...descriptors],
|
|
['Duke Vidal is unavailable for this revival.'],
|
|
);
|
|
});
|
|
void test('Advanced Harkonnen retains its uniform public boundary while Basic and other Advanced tables resolve normally', () => {
|
|
const g = fixture();
|
|
g.players.push(newPlayer('h', 'Harkonnen', 'harkonnen'));
|
|
assert.equal(ecazDukeRevivalBlock(g, 'ec'), null);
|
|
g.advanced = true;
|
|
const expected = ecazDukeRevivalBlock(g, 'ec');
|
|
assert.match(expected!, /Advanced Harkonnen/);
|
|
for (const dead of [false, true])
|
|
for (const captured of [false, true]) {
|
|
g.dukeVidal!.leader.dead = dead;
|
|
if (captured) g.dukeVidal!.leader.capturedBy = 'h';
|
|
else delete g.dukeVidal!.leader.capturedBy;
|
|
assert.equal(ecazDukeRevivalBlock(g, 'ec'), expected);
|
|
}
|
|
});
|
|
void test('set-aside battle-death identity and valid retained legacy custody resolve to the same reviver without selecting future control', () => {
|
|
const g = fixture();
|
|
g.dukeVidal = acquireDuke(createDukeVidal(), 'm', 2, 'moritani');
|
|
g.dukeVidal.leader.dead = true;
|
|
g.dukeVidal.leader.deaths = 2;
|
|
g.dukeVidal.leader.usedAt = 'arrakeen';
|
|
const retained = resolveEcazDukeRevival(g, 'ec');
|
|
assert.equal(retained.duke.controller, 'm');
|
|
g.dukeVidal = consumeDuke(g.dukeVidal);
|
|
const released = resolveEcazDukeRevival(g, 'ec');
|
|
assert.equal(released.duke.controller, null);
|
|
assert.equal(retained.player, released.player);
|
|
assert.equal(released.player, 'ec');
|
|
assert.deepEqual(retained.duke.leader, released.duke.leader);
|
|
assert.deepEqual(
|
|
resolveEcazDukeRevival(JSON.parse(JSON.stringify(g)), 'ec'),
|
|
released,
|
|
);
|
|
});
|
|
void test('a dead Duke with prior Ecaz Nexus custody resolves to the same physical return', () => {
|
|
const g = fixture();
|
|
g.dukeVidal = acquireDuke(createDukeVidal(), 'ec', 2, 'ecazNexus');
|
|
g.dukeVidal.leader.dead = true;
|
|
g.dukeVidal.leader.deaths = 1;
|
|
const quoted = resolveEcazDukeRevival(g, 'ec');
|
|
assert.equal(quoted.duke.source, 'ecazNexus');
|
|
assert.equal(quoted.normalCost, 5);
|
|
});
|
|
void test('invalid source context and duplicate Ecaz owners fail independently from paid revival timing', () => {
|
|
for (const change of [
|
|
(g: Game) => {
|
|
g.status = 'setup';
|
|
},
|
|
(g: Game) => {
|
|
g.turn = 0;
|
|
},
|
|
(g: Game) => {
|
|
g.turn = NaN;
|
|
},
|
|
(g: Game) => {
|
|
g.players.push(newPlayer('another', 'Second Ecaz', 'ecaz'));
|
|
},
|
|
(g: Game) => {
|
|
g.players.push(newPlayer('ec', 'Duplicate ID', 'moritani'));
|
|
},
|
|
(g: Game) => {
|
|
p(g, 'ec').leaders = null as never;
|
|
},
|
|
]) {
|
|
const g = fixture();
|
|
change(g);
|
|
const before = structuredClone(g);
|
|
assert.throws(() => resolveEcazDukeRevival(g, 'ec'), EcazDukeRevivalError);
|
|
assert.deepEqual(g, before);
|
|
}
|
|
});
|
|
void test('four native deaths plus Duke open Ecaz first cohort, which stays open after a paid return', () => {
|
|
let g = fixture();
|
|
p(g, 'ec').leaders.slice(0, 4).forEach((l) => {
|
|
l.dead = true;
|
|
l.deaths = 1;
|
|
});
|
|
p(g, 'ec').spice = 20;
|
|
const initial = viewGame(g, 'ec').revival.leaders;
|
|
assert.equal(initial.filter((l) => l.id !== DUKE_VIDAL_ID).length, 4);
|
|
assert.equal(initial.find((l) => l.id === DUKE_VIDAL_ID)?.early, false);
|
|
g = applyAction(g, 'ec', { type: 'reviveLeader', leader: DUKE_VIDAL_ID });
|
|
assert.equal(p(g, 'ec').revivalCycle, 1);
|
|
assert.equal(p(g, 'ec').spice, 15);
|
|
p(g, 'ec').leaderRevived = false;
|
|
g.turn++;
|
|
assert.equal(viewGame(g, 'ec').revival.leaders.length, 4);
|
|
const native = p(g, 'ec').leaders[0].id;
|
|
g = applyAction(g, 'ec', { type: 'reviveLeader', leader: native });
|
|
assert.equal(p(g, 'ec').spice, 15 - p(g, 'ec').leaders[0].strength);
|
|
assert.equal(p(g, 'ec').leaders[4].dead, false);
|
|
assert.equal(g.dukeVidal!.controller, null);
|
|
});
|
|
void test('saved discounted Duke request can be canceled back to five and paid once with income', () => {
|
|
let g = fixture(true);
|
|
g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
|
|
g.players.push(newPlayer('k', 'Karama', 'emperor'));
|
|
g.order = g.players.map((seat) => seat.id);
|
|
g.deck = baseDeck();
|
|
const karamaIndex = g.deck.findIndex((card) => card.effect === 'karama');
|
|
p(g, 'k').hand.push(g.deck.splice(karamaIndex, 1)[0]);
|
|
p(g, 'ec').spice = 8;
|
|
p(g, 't').ally = 'ec';
|
|
p(g, 'ec').ally = 't';
|
|
p(g, 't').specialKaramaUsed = true;
|
|
g = applyAction(g, 't', { type: 'tleilaxuAllyDiscount' });
|
|
assert.equal(viewGame(g, 'ec').revival.leaders.find((l) => l.id === DUKE_VIDAL_ID)?.cost, 3);
|
|
g = applyAction(g, 'ec', { type: 'reviveLeader', leader: DUKE_VIDAL_ID });
|
|
assert.deepEqual(g.pendingRevival?.checks, []);
|
|
assert.equal(g.response?.kind, 'revivalDiscount');
|
|
assert.equal(g.pendingRevival?.normalCost, 5);
|
|
assert.equal(g.pendingRevival?.cost, 3);
|
|
const saved = JSON.parse(JSON.stringify(g)) as Game;
|
|
const before = structuredClone(saved);
|
|
const quote = quoteRevivalCancellation(saved, saved.response!);
|
|
assert.equal(quote.pending?.cost, 5);
|
|
assert.deepEqual(saved, before);
|
|
g = applyAction(saved, 'k', {
|
|
type: 'card', card: p(saved, 'k').hand[0].id, mode: 'cancel',
|
|
});
|
|
assert.equal(p(g, 'ec').spice, 3);
|
|
assert.equal(g.dukeVidal!.leader.dead, false);
|
|
assert.equal(g.dukeVidal!.controller, null);
|
|
assert.equal(p(g, 'ec').leaderRevived, true);
|
|
assert.equal(g.pendingRevival, null);
|
|
assert.equal(g.response, null);
|
|
assert.equal(p(g, 't').spice, 5, 'the automatic paid revival income settles once');
|
|
});
|
|
void test('Advanced Tleilaxu stop decision survives JSON and cannot charge Duke before approval', () => {
|
|
let g = fixture(true);
|
|
g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
|
|
g.order = g.players.map((seat) => seat.id);
|
|
p(g, 'ec').spice = 7;
|
|
g = applyAction(g, 'ec', { type: 'reviveLeader', leader: DUKE_VIDAL_ID });
|
|
assert.equal(g.decision?.kind, 'revivalStop');
|
|
assert.equal(g.pendingRevival?.normalCost, 5);
|
|
assert.equal(p(g, 'ec').spice, 7);
|
|
assert.equal(g.dukeVidal!.leader.dead, true);
|
|
g = JSON.parse(JSON.stringify(g)) as Game;
|
|
g = applyAction(g, 't', { type: 'decision', decline: true });
|
|
assert.equal(p(g, 'ec').spice, 2);
|
|
assert.equal(p(g, 't').spice, 5);
|
|
assert.equal(g.dukeVidal!.leader.dead, false);
|
|
assert.equal(g.dukeVidal!.controller, null);
|
|
assert.equal(g.pendingRevival, null);
|
|
});
|
|
void test('all four Ecaz AI profiles can choose the authoritative five-spice Duke return', () => {
|
|
const g = fixture();
|
|
p(g, 'ec').spice = 10;
|
|
for (const profile of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
p(g, 'ec').bot = profile;
|
|
const view = viewGame(g, 'ec');
|
|
const selected = botActions(view).find(
|
|
(action) => action.type === 'reviveLeader' && action.leader === DUKE_VIDAL_ID,
|
|
);
|
|
assert.ok(selected, profile);
|
|
const done = applyAction(g, 'ec', selected);
|
|
assert.equal(done.dukeVidal!.leader.dead, false);
|
|
assert.equal(done.dukeVidal!.controller, null);
|
|
assert.equal(done.players.find((seat) => seat.id === 'ec')!.spice, 5);
|
|
}
|
|
});
|
|
void test('corrupt saved pending Duke custody rejects continuation without changing spice or the request', () => {
|
|
let g = fixture(true);
|
|
g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
|
|
g.order = g.players.map((seat) => seat.id);
|
|
p(g, 'ec').spice = 9;
|
|
g = applyAction(g, 'ec', { type: 'reviveLeader', leader: DUKE_VIDAL_ID });
|
|
assert.equal(g.decision?.kind, 'revivalStop');
|
|
g = JSON.parse(JSON.stringify(g)) as Game;
|
|
p(g, 'ec').leaders.push(structuredClone(g.dukeVidal!.leader));
|
|
const before = structuredClone(g);
|
|
assert.throws(() => applyAction(g, 't', { type: 'decision', decline: true }));
|
|
assert.deepEqual(g, before);
|
|
assert.equal(p(g, 'ec').spice, 9);
|
|
});
|
|
void test('five native Ecaz leaders in Tanks open the ordinary first cohort even while Duke lives', () => {
|
|
const g = fixture();
|
|
g.dukeVidal!.leader.dead = false;
|
|
g.dukeVidal!.leader.deaths = 0;
|
|
p(g, 'ec').leaders.forEach((l) => { l.dead = true; l.deaths = 1; });
|
|
p(g, 'ec').spice = 20;
|
|
const options = viewGame(g, 'ec').revival.leaders;
|
|
assert.equal(options.length, 5);
|
|
assert.equal(options.some((l) => l.id === DUKE_VIDAL_ID), false);
|
|
});
|
|
void test('unavailable captured native leaders retain the ordinary no-survivor exception', () => {
|
|
const g = fixture();
|
|
g.dukeVidal!.leader.dead = false;
|
|
g.dukeVidal!.leader.deaths = 0;
|
|
p(g, 'ec').leaders.forEach((l, i) => {
|
|
l.dead = i === 0;
|
|
l.deaths = i === 0 ? 1 : 0;
|
|
if (i !== 0) l.capturedBy = 'm';
|
|
});
|
|
p(g, 'ec').spice = 20;
|
|
assert.deepEqual(viewGame(g, 'ec').revival.leaders.map((l) => l.id),
|
|
[p(g, 'ec').leaders[0].id]);
|
|
});
|
|
void test('concealed foreign-ghola execution does not reveal Ecaz revival cohort history', () => {
|
|
const g = fixture(true);
|
|
g.expansions = ['ecaz', 'ix'];
|
|
g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
|
|
g.players.push(newPlayer('h', 'Harkonnen', 'harkonnen'));
|
|
const leader = p(g, 'ec').leaders[0];
|
|
leader.dead = false;
|
|
leader.deaths = 1;
|
|
leader.gholaBy = 't';
|
|
leader.capturedBy = 'h';
|
|
leader.concealed = { captor: 'h', controller: 't', dead: false, deaths: 1 };
|
|
const before = viewGame(g, 'ec');
|
|
leader.dead = true;
|
|
leader.deaths = 2;
|
|
delete leader.capturedBy;
|
|
const after = viewGame(g, 'ec');
|
|
assert.deepEqual(after.players.find((seat) => seat.id === 'ec')?.leaders,
|
|
before.players.find((seat) => seat.id === 'ec')?.leaders);
|
|
assert.deepEqual(after.revival, before.revival);
|
|
});
|