Files
Dune/tests/duke-vidal.test.ts

172 lines
6.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
acquireDuke,
consumeDuke,
createDukeVidal,
DUKE_VIDAL_ID,
expireDuke,
type DukeSource,
} from '../game/duke-vidal';
import { leaders } from '../game/cards';
import { traitorDeck } from '../game/traitors';
void test('Duke Vidal is one fresh Ecaz disc of strength six, separate from both native rosters and traitor inventories', () => {
const duke = createDukeVidal();
assert.deepEqual(duke, {
leader: {
id: 'duke-vidal',
name: 'Duke Prad Vidal',
faction: 'ecaz',
strength: 6,
dead: false,
deaths: 0,
},
controller: null,
acquiredTurn: null,
source: null,
});
assert.equal(DUKE_VIDAL_ID, duke.leader.id);
const ecaz = leaders('ecaz');
const moritani = leaders('moritani');
assert.equal(ecaz.length, 5);
assert.equal(moritani.length, 5);
assert.equal(
traitorDeck([{ leaders: ecaz }, { leaders: moritani }], true).includes(
DUKE_VIDAL_ID,
),
false,
);
const changed = createDukeVidal();
changed.leader.dead = true;
changed.controller = 'm';
assert.equal(createDukeVidal().leader.dead, false);
assert.equal(createDukeVidal().controller, null);
});
void test('acquisition changes only explicit custody, preserving permanent identity and leader history', () => {
const original = createDukeVidal();
original.leader.deaths = 2;
original.leader.usedAt = 'arrakeen';
const before = structuredClone(original);
for (const source of ['moritani', 'ecaz', 'ally'] as const) {
const acquired = acquireDuke(original, 'holder-' + source, 3, source);
assert.deepEqual(acquired.leader, original.leader);
assert.notEqual(acquired.leader, original.leader);
assert.equal(acquired.controller, 'holder-' + source);
assert.equal(acquired.acquiredTurn, 3);
assert.equal(acquired.source, source);
acquired.leader.deaths++;
assert.deepEqual(original, before);
}
const first = acquireDuke(original, 'ecaz-seat', 3, 'ecaz');
const taken = acquireDuke(first, 'moritani-seat', 4, 'moritani');
assert.equal(taken.controller, 'moritani-seat');
assert.equal(taken.leader.faction, 'ecaz');
assert.equal(taken.leader.id, DUKE_VIDAL_ID);
assert.equal(first.controller, 'ecaz-seat');
});
void test('dead and unresolved captured or ghola custody reject acquisition without changing the disc', () => {
for (const custody of ['dead', 'capturedBy', 'gholaBy'] as const) {
const state = acquireDuke(createDukeVidal(), 'old-holder', 1, 'ecaz');
if (custody === 'dead') state.leader.dead = true;
else state.leader[custody] = 'other-seat';
const before = structuredClone(state);
for (const source of ['moritani', 'ecaz', 'ally'] as const)
assert.throws(() => acquireDuke(state, 'new-holder', 2, source));
assert.deepEqual(state, before);
}
});
void test('invalid turns, empty controllers and unknown acquisition sources cannot create custody', () => {
const state = createDukeVidal();
const before = structuredClone(state);
for (const turn of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
assert.throws(
() => acquireDuke(state, 'm', turn, 'moritani'),
/valid turn/,
);
assert.throws(() => expireDuke(state, turn), /valid turn/);
}
for (const controller of ['', ' '])
assert.throws(
() => acquireDuke(state, controller, 1, 'moritani'),
/Choose a player/,
);
assert.throws(
() => acquireDuke(state, 'm', 1, 'invented' as DukeSource),
/valid source/,
);
assert.deepEqual(state, before);
});
void test('single-battle consumption releases custody while retaining alive or dead leader history', () => {
for (const dead of [false, true]) {
const state = acquireDuke(createDukeVidal(), 'm', 2, 'moritani');
state.leader.dead = dead;
state.leader.deaths = dead ? 2 : 1;
state.leader.usedAt = 'carthag';
const before = structuredClone(state);
const consumed = consumeDuke(state);
assert.equal(consumed.controller, null);
assert.equal(consumed.acquiredTurn, null);
assert.equal(consumed.source, null);
assert.deepEqual(consumed.leader, state.leader);
assert.deepEqual(state, before);
assert.deepEqual(consumeDuke(consumed), consumed);
}
});
void test('turn end releases Moritani tenure and allied loans while preserving unused Ecaz holdings', () => {
for (const source of ['moritani', 'ally', 'ecaz'] as const) {
const state = acquireDuke(createDukeVidal(), 'holder', 4, source);
const earlier = expireDuke(state, 3);
assert.deepEqual(earlier, state);
for (const turn of [4, 5]) {
const expired = expireDuke(state, turn);
assert.equal(expired.controller, source === 'ecaz' ? 'holder' : null);
assert.equal(expired.acquiredTurn, source === 'ecaz' ? 4 : null);
assert.equal(expired.source, source === 'ecaz' ? source : null);
assert.deepEqual(expired.leader, state.leader);
}
assert.equal(state.controller, 'holder');
}
});
void test('turn end preserves dead, captured and ghola custody rather than inventing a return destination', () => {
for (const custody of ['dead', 'capturedBy', 'gholaBy'] as const) {
const state = acquireDuke(createDukeVidal(), 'm', 1, 'moritani');
if (custody === 'dead') state.leader.dead = true;
else state.leader[custody] = 'other-seat';
state.leader.concealed = {
captor: 'other-seat',
controller: 'm',
dead: false,
deaths: 0,
};
const result = expireDuke(state, 2);
assert.deepEqual(result, state);
result.leader.concealed!.dead = true;
assert.equal(state.leader.concealed.dead, false);
assert.notEqual(result.leader, state.leader);
}
const initial = createDukeVidal();
assert.deepEqual(expireDuke(initial, 1), initial);
});
void test('custody survives JSON persistence and permits a later eligible acquisition without creating another disc', () => {
const held = acquireDuke(createDukeVidal(), 'm', 1, 'moritani');
const released = consumeDuke(JSON.parse(JSON.stringify(held)));
const reacquired = acquireDuke(
JSON.parse(JSON.stringify(released)),
'm',
2,
'moritani',
);
assert.equal(reacquired.leader.id, held.leader.id);
assert.equal(reacquired.acquiredTurn, 2);
assert.equal(released.controller, null);
assert.equal(held.controller, 'm');
});