109 lines
5.4 KiB
TypeScript
109 lines
5.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { baseDeck } from '../game/cards';
|
|
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
|
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
|
|
const send = (game: Game, id: string, action: Action) => applyAction(game, id, action);
|
|
const seat = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
|
|
|
/** The lobby cannot start this variant yet. Stage a Basic Nexus; the pair,
|
|
* both admissions and every later decision are actual authoritative actions. */
|
|
function fixture(phase = 4) {
|
|
let game = createGame('UNLIMITEMP', newPlayer('e', 'Emperor', 'emperor'), false);
|
|
game.players.push(newPlayer('a', 'Atreides', 'atreides'), newPlayer('h', 'Harkonnen', 'harkonnen'),
|
|
newPlayer('g', 'Guild', 'guild'), newPlayer('f', 'Fremen', 'fremen'));
|
|
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true,
|
|
order:['e','a','h','g','f'], ready:[], storm:18, deck:baseDeck(),
|
|
response:null, decision:null, phaseOpening:null});
|
|
game.coalitionState = emptyCoalitionState('unlimited');
|
|
game = send(game, 'e', {type:'alliance', target:'a'});
|
|
game = send(game, 'a', {type:'alliance', target:'e'});
|
|
for (const entrant of ['h', 'g']) {
|
|
const group = coalitionOf(game.players, game.coalitionState, 'e'); assert.ok(group);
|
|
game = send(game, 'e', {type:'coalitionPropose', groupId:group.id, entrant,
|
|
expectedRevision:game.coalitionState!.revision});
|
|
for (const id of [...group.members.filter(member => member !== 'e'), entrant]) {
|
|
const proposal = game.coalitionState!.proposal!;
|
|
game = send(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, 'e')!.members, ['a','e','g','h']);
|
|
Object.assign(game, {phase, nexus:false, ready:[], response:null, decision:null});
|
|
for (const player of game.players) {
|
|
player.forces = {}; player.reserves = 20; player.tanks = 0; player.spice = 10;
|
|
player.hand = []; player.traitors = []; player.traitorChoices = [];
|
|
}
|
|
seat(game, 'e').spice = 20;
|
|
for (const id of ['a','g','h']) { seat(game,id).reserves = 16; seat(game,id).tanks = 4; }
|
|
const index = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
|
|
const [karama] = game.deck.splice(index, 1); seat(game, 'f').hand.push(karama);
|
|
return {game, karama};
|
|
}
|
|
function passResponses(game: Game): Game {
|
|
for (let count = 0; game.response && count < 30; count++) {
|
|
const next = game.players.find(player => !game.response!.passed.includes(player.id));
|
|
assert.ok(next);
|
|
game = send(game, next.id, {type:'passResponse'});
|
|
}
|
|
assert.equal(game.response ?? null, null);
|
|
return game;
|
|
}
|
|
function conserved(game: Game) {
|
|
for (const player of game.players)
|
|
assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((a,b) => a+b,0), 20);
|
|
}
|
|
|
|
void test('four-member Emperor gifts reach every selected ally through the native response', () => {
|
|
let {game} = fixture(3);
|
|
assert.deepEqual(viewGame(game, 'e').coalitionEmperor?.allies.sort(), ['a','g','h']);
|
|
for (const target of ['a','h','g']) {
|
|
const pending = send(game, 'e', {type:'emperorGift', target, amount:2});
|
|
assert.equal(pending.response?.kind, 'emperorGift');
|
|
assert.equal(pending.response?.recipient, target);
|
|
game = passResponses(pending);
|
|
assert.equal(seat(game,target).spice, 12);
|
|
}
|
|
assert.equal(seat(game,'e').spice,14);
|
|
const before = structuredClone(game);
|
|
assert.throws(() => send(game,'e',{type:'emperorGift', target:'f', amount:1}));
|
|
assert.deepEqual(game,before);
|
|
conserved(game);
|
|
});
|
|
|
|
for (const type of ['emperorGift','emperorRevival'] as const)
|
|
void test(`Karama cancels a four-member Emperor ${type} for the fourth member`, () => {
|
|
const initial = fixture();
|
|
let game = send(initial.game, 'e', {type, target:'g', amount:2, elite:0});
|
|
assert.equal(game.response?.kind, type);
|
|
assert.equal(game.response?.recipient,'g');
|
|
game = send(game,'f',{type:'card',mode:'cancel',card:initial.karama.id});
|
|
game = passResponses(game);
|
|
assert.equal(seat(game,'e').spice,20);
|
|
assert.equal(seat(game,'g').spice,10);
|
|
assert.equal(seat(game,'g').tanks,4);
|
|
assert.equal(game.emperorExtra.g ?? 0,0);
|
|
assert.equal(game.discard.filter(card => card.id === initial.karama.id).length,1);
|
|
// Cancellation must not remove the Emperor's ability to aid other members.
|
|
game = passResponses(send(game,'e',{type, target:'h', amount:2, elite:0}));
|
|
assert.equal(seat(game,'e').spice,type === 'emperorGift' ? 18 : 16);
|
|
if (type === 'emperorGift') assert.equal(seat(game,'h').spice,12);
|
|
else { assert.equal(seat(game,'h').tanks,2); assert.equal(game.emperorExtra.h,2); }
|
|
conserved(game);
|
|
});
|
|
|
|
void test('four-member Emperor revivals use separate recipient allowances and the Emperor wallet', () => {
|
|
let {game} = fixture();
|
|
for (const target of ['a','h','g']) {
|
|
const pending = send(game,'e',{type:'emperorRevival',target,amount:3,elite:0});
|
|
assert.equal(pending.response?.recipient,target);
|
|
game = passResponses(pending);
|
|
assert.equal(seat(game,target).tanks,1);
|
|
assert.equal(seat(game,target).reserves,19);
|
|
assert.equal(game.emperorExtra[target],3);
|
|
}
|
|
assert.equal(seat(game,'e').spice,2);
|
|
conserved(game);
|
|
});
|