66 lines
3.5 KiB
TypeScript
66 lines
3.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { TurnActionFields, turnActionInputError } from '../components/turn-action-fields';
|
|
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
|
import { applyAction, createGame, newPlayer, viewGame, type GameView } from '../game/engine';
|
|
import type { MixedActionExpression } from '../game/mixed-action-question';
|
|
import type { TurnActionLeaf } from '../game/turn-action-promises';
|
|
|
|
const payment = (recipient: string): MixedActionExpression<TurnActionLeaf> =>
|
|
({kind:'action', action:{kind:'payment', recipient, minimum:1}});
|
|
|
|
/** Controlled quiet Nexus and preview ledger, with native pair and triple
|
|
* formation. The editor consumes the resulting public projection. */
|
|
function projected(triple: boolean): GameView {
|
|
let game = createGame('TURNPAYGROUP', newPlayer('a', 'Atreides', 'atreides'), false);
|
|
game.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'),
|
|
newPlayer('d', 'Harkonnen', 'harkonnen'));
|
|
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['a','b','c','d'], ready:[]});
|
|
game.coalitionState = emptyCoalitionState('three-short');
|
|
game = applyAction(game, 'a', {type:'alliance', target:'b'});
|
|
game = applyAction(game, 'b', {type:'alliance', target:'a'});
|
|
if (triple) {
|
|
const pair = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(pair);
|
|
game = applyAction(game, 'a', {type:'coalitionPropose', groupId:pair.id, entrant:'c',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
for (const actor of ['b','c']) {
|
|
const offer = game.coalitionState!.proposal!;
|
|
game = applyAction(game, actor, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus,
|
|
turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
}
|
|
return viewGame(game, 'd');
|
|
}
|
|
|
|
function html(game: GameView, recipient: string) {
|
|
return renderToStaticMarkup(createElement(TurnActionFields, {
|
|
game, target:'a', value:payment(recipient), disabled:false, onChange() {},
|
|
}));
|
|
}
|
|
|
|
void test('whole-turn payment editor excludes every group ally and accepts an outsider', () => {
|
|
const game = projected(true);
|
|
assert.equal(game.players.find(player => player.id === 'a')!.ally, null,
|
|
'Triple membership must come from the projected coalition ledger.');
|
|
for (const id of ['a','b','c']) {
|
|
assert.match(turnActionInputError(payment(id), game, 'a') ?? '', /non-allied recipient/);
|
|
assert.doesNotMatch(html(game, 'd'), new RegExp(`<option value="${id}"`));
|
|
}
|
|
assert.equal(turnActionInputError(payment('d'), game, 'a'), null);
|
|
assert.match(html(game, 'd'), /<option value="d" selected="">Harkonnen<\/option>/);
|
|
});
|
|
|
|
void test('whole-turn payment editor preserves reciprocal pairs and unallied choices', () => {
|
|
const game = projected(false);
|
|
assert.match(turnActionInputError(payment('b'), game, 'a') ?? '', /non-allied recipient/);
|
|
for (const id of ['c','d']) assert.equal(turnActionInputError(payment(id), game, 'a'), null);
|
|
const legacy = structuredClone(game);
|
|
delete legacy.coalitionState;
|
|
assert.match(turnActionInputError(payment('b'), legacy, 'a') ?? '', /non-allied recipient/);
|
|
for (const id of ['c','d']) assert.equal(turnActionInputError(payment(id), legacy, 'a'), null);
|
|
assert.match(html(legacy, 'c'), /<option value="c" selected="">Fremen<\/option>/);
|
|
assert.doesNotMatch(html(legacy, 'c'), /<option value="b"/);
|
|
});
|