112 lines
5.9 KiB
TypeScript
112 lines
5.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { baseDeck } from '../game/cards';
|
|
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
|
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { CoalitionNexus, coalitionNexusOptions } from '../components/coalition-nexus';
|
|
|
|
const send = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
|
|
const seat = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
|
|
|
/** The quiet Nexus, wallet and physical Truthtrance are staged. Pair/group
|
|
* formation, question, answer, departure and payment are actual actions. */
|
|
function group(): Game {
|
|
let game = createGame('GROUPBRIBE', newPlayer('a', 'Atreides', 'atreides'), false);
|
|
game.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'),
|
|
newPlayer('d', 'Harkonnen', 'harkonnen'), newPlayer('e', 'Guild', 'guild'));
|
|
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['a','b','c','d','e'],
|
|
ready:[], deck:baseDeck()});
|
|
game.coalitionState = emptyCoalitionState('unlimited');
|
|
seat(game, 'a').spice = 8;
|
|
game = send(game, 'a', {type:'alliance', target:'b'});
|
|
game = send(game, 'b', {type:'alliance', target:'a'});
|
|
const pair = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(pair);
|
|
game = send(game, 'a', {type:'coalitionPropose', groupId:pair.id, entrant:'c',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
for (const id of ['b','c']) {
|
|
const offer = game.coalitionState!.proposal!;
|
|
game = send(game, id, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus,
|
|
turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0);
|
|
const [card] = game.deck.splice(index, 1); seat(game, 'e').hand.push(card);
|
|
return game;
|
|
}
|
|
function ask(answer: 'yes' | 'no', expression = false): Game {
|
|
let game = group();
|
|
const card = seat(game, 'e').hand.find(row => row.effect === 'truthtrance')!;
|
|
game = send(game, 'e', {type:'card', card:card.id});
|
|
while (game.truthtrance?.stage === 'priority') {
|
|
const passer = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(passer);
|
|
game = send(game, passer.id, {type:'truthPass'});
|
|
}
|
|
const question = expression
|
|
? {kind:'bribeExpression', target:'a', turn:game.turn, claim:{kind:'and', terms:[
|
|
{kind:'payment', recipient:'d', minimum:2}, {kind:'payment', recipient:'e', minimum:1}]}}
|
|
: {kind:'bribe', target:'a', turn:game.turn, recipient:'d', minimum:2};
|
|
game = send(game, 'e', {type:'truthAsk', question});
|
|
const view = viewGame(game, 'a');
|
|
const answers = expression ? view.truthBribeExpressionAnswers : view.truthBribeAnswers;
|
|
assert.ok(answers?.includes(answer));
|
|
return send(game, 'a', {type:'truthAnswer', answer});
|
|
}
|
|
function leave(game:Game, actor:string):Game {
|
|
return send(game, actor, {type:'coalitionLeave', expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
function blockedProposal(game:Game) {
|
|
const alliance = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(alliance);
|
|
const before = structuredClone(game);
|
|
assert.throws(() => send(game, 'a', {type:'coalitionPropose', groupId:alliance.id,
|
|
entrant:'d', expectedRevision:game.coalitionState!.revision}));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
|
|
void test('a positive bribe answer allows owned group departure and remains payable', () => {
|
|
let game = ask('yes');
|
|
const projected = viewGame(game, 'a').coalitionNexus; assert.ok(projected);
|
|
const options = coalitionNexusOptions(projected);
|
|
assert.equal(options.mayLeave, true);
|
|
assert.ok(options.invitations.every(invitation => invitation.entrant !== 'd'));
|
|
const markup = renderToStaticMarkup(createElement(CoalitionNexus, {view:projected, onIntent:()=>{}}));
|
|
assert.match(markup, /Leave/);
|
|
blockedProposal(game);
|
|
game = leave(game, 'a');
|
|
assert.equal(coalitionOf(game.players, game.coalitionState, 'a'), null);
|
|
assert.deepEqual(coalitionOf(game.players, game.coalitionState, 'b')!.members, ['b','c']);
|
|
const promise = game.bribePromises!.find(row => row.player === 'a')!;
|
|
assert.equal(promise.released ?? false, false);
|
|
const guide = viewGame(game, 'a').bribePromiseGuide; assert.ok(guide);
|
|
assert.equal(guide.action.type, 'bribe'); assert.equal(guide.action.target, 'd');
|
|
game = send(game, 'a', guide.action);
|
|
assert.equal(game.bribePromises!.find(row => row.event === promise.event)!.fulfilled, true);
|
|
assert.equal(seat(game, 'd').bribes, 2);
|
|
});
|
|
|
|
void test('a negative bribe answer survives another member departing and retains its cap', () => {
|
|
let game = ask('no');
|
|
game = leave(game, 'b');
|
|
assert.deepEqual(coalitionOf(game.players, game.coalitionState, 'a')!.members, ['a','c']);
|
|
const before = structuredClone(game);
|
|
assert.throws(() => send(game, 'a', {type:'bribe', target:'d', amount:2}));
|
|
assert.deepEqual(game, before);
|
|
game = send(game, 'a', {type:'bribe', target:'d', amount:1});
|
|
assert.equal(seat(game, 'd').bribes, 1);
|
|
assert.equal(game.bribePromises!.find(row => row.player === 'a')!.released ?? false, false);
|
|
});
|
|
|
|
void test('a positive bribe expression remains binding and payable after departure', () => {
|
|
let game = ask('yes', true);
|
|
blockedProposal(game);
|
|
game = leave(game, 'a');
|
|
const promise = game.bribeExpressionPromises!.find(row => row.player === 'a')!;
|
|
assert.equal(promise.released ?? false, false);
|
|
for (let step = 0; step < 2; step++) {
|
|
const guide = viewGame(game, 'a').bribePromiseGuide; assert.ok(guide);
|
|
game = send(game, 'a', guide.action);
|
|
}
|
|
assert.equal(game.bribeExpressionPromises!.find(row => row.event === promise.event)!.fulfilled, true);
|
|
assert.equal(seat(game, 'd').bribes, 2); assert.equal(seat(game, 'e').bribes, 1);
|
|
});
|