172 lines
6.3 KiB
TypeScript
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');
|
|
});
|