96 lines
5.0 KiB
TypeScript
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);
|
|
});
|