Files
Dune/tests/coalition-traitor-declarations.test.ts
T
Quintine 478b38f5de
Verify and publish checkpoint container / container (push) Canceled after 0s
Support group Harkonnen traitor calls
2026-10-28 23:46:15 +11:00

96 lines
5.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { applyAction, createGame, newPlayer } from '../game/engine';
import { createTraitorDeclaration, validateTraitorDeclaration, validateTraitorDeclarations,
type TraitorDeclarationContext } from '../game/traitor-declarations';
/** The ledger is staged because the lobby does not yet expose group play.
* Pair formation and unanimous admission use real Nexus actions. The battle
* plans and Harkonnen's known identity are staged for this receipt quote. */
function source(group = true): TraitorDeclarationContext {
let game = createGame('GROUPTRAITOR', newPlayer('h', 'Harkonnen', 'harkonnen'), false);
game.players.push(newPlayer('a', 'Atreides', 'atreides'),
newPlayer('e', 'Emperor', 'emperor'), newPlayer('g', 'Guild', 'guild'));
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true,
order:['h','a','e','g'], ready:[], storm:18, response:null, decision:null, phaseOpening:null});
game.coalitionState = emptyCoalitionState();
game = applyAction(game, 'h', {type:'alliance', target:'a'});
game = applyAction(game, 'a', {type:'alliance', target:'h'});
if (group) {
const pair = coalitionOf(game.players, game.coalitionState, 'h'); assert.ok(pair);
game = applyAction(game, 'h', {type:'coalitionPropose', groupId:pair.id, entrant:'e',
expectedRevision:game.coalitionState!.revision});
for (const id of ['a','e']) {
const proposal = game.coalitionState!.proposal!;
game = applyAction(game, id, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus,
turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision});
}
assert.deepEqual(coalitionOf(game.players, game.coalitionState, 'h')!.members, ['a','e','h']);
}
return {event:'group-battle', attacker:'a', defender:'g',
status:'playing', advanced:false, expansions:[], coalitionState:game.coalitionState,
plans:{a:{leader:'a-leader'}, e:{leader:'e-leader'}, g:{leader:'g-leader'}},
players:game.players.map(player => ({id:player.id, faction:player.faction, ally:player.ally,
...(player.allySinceTurn === undefined ? {} : {allySinceTurn:player.allySinceTurn}),
traitors:player.id === 'h' ? ['g-leader'] : []})), heroLeaderIds:[]};
}
void test('a Harkonnen group declaration binds each actual ally, opponent and identity', () => {
for (const ally of ['a','e']) {
const game = source();
game.attacker = ally;
const before = structuredClone(game);
const receipt = createTraitorDeclaration(game, 'h');
assert.deepEqual({voter:receipt.voter, beneficiary:receipt.beneficiary, target:receipt.target,
leader:receipt.leader, identity:receipt.identity},
{voter:'h', beneficiary:ally, target:'g', leader:'g-leader', identity:'g-leader'});
validateTraitorDeclaration(game, receipt);
validateTraitorDeclarations(game, {h:true}, {h:receipt}, 1);
assert.deepEqual(game, before);
}
});
void test('a group declaration rejects retargeted battle, leader, identity and receipt', () => {
const game = source();
const receipt = createTraitorDeclaration(game, 'h');
for (const mutate of [
(value: TraitorDeclarationContext) => { value.attacker = 'e'; },
(value: TraitorDeclarationContext) => { value.defender = 'e'; },
(value: TraitorDeclarationContext) => { value.plans.g.leader = 'replacement'; },
(value: TraitorDeclarationContext) => { value.plans.g.kwisatz = true; },
(value: TraitorDeclarationContext) => { value.event = 'another-battle'; },
]) {
const changed = structuredClone(game);
mutate(changed);
assert.throws(() => validateTraitorDeclaration(changed, receipt));
}
for (const field of ['beneficiary','target','leader','identity','signature'] as const) {
const changed = {...receipt, [field]:'incorrect'};
assert.throws(() => validateTraitorDeclaration(game, changed));
}
const noIdentity = structuredClone(game);
noIdentity.players.find(player => player.id === 'h')!.traitors = [];
assert.throws(() => createTraitorDeclaration(noIdentity, 'h'));
validateTraitorDeclaration(noIdentity, receipt);
});
void test('group declaration is unavailable in unsupported profiles or ambiguous group battles', () => {
const game = source();
for (const patch of [{advanced:true}, {expansions:['ix']}, {homeworlds:{}},
{nexusCards:{}}, {leaderSkills:{}}, {techTokens:{}}, {strongholdCards:{}},
{ecazTreachery:true}, {discoveryEnabled:true}]) {
assert.throws(() => createTraitorDeclaration({...game, ...patch}, 'h'));
}
const ambiguous = {...game, defender:'e'};
assert.throws(() => createTraitorDeclaration(ambiguous, 'h'));
});
void test('the legacy Harkonnen pair retains implicit allied beneficiary', () => {
const game = source(false);
const receipt = createTraitorDeclaration(game, 'h');
assert.equal(receipt.beneficiary, 'a');
assert.equal(receipt.target, 'g');
validateTraitorDeclaration(game, receipt);
});