Files
Dune/tests/coalition-turn-action-payment-controls.test.tsx
T
Quintine 599af2475d
Verify and publish checkpoint container / container (push) Canceled after 0s
Route Advanced Emperor allied auction funding
2026-10-29 02:48:11 +11:00

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"/);
});