93 lines
4.8 KiB
TypeScript
93 lines
4.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { AlliancePromiseGuideButton } from '../components/alliance-promises';
|
|
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
|
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game, type GameView } from '../game/engine';
|
|
|
|
const send = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
|
|
|
|
/** Real public pair/group formation with controlled promise and guide
|
|
* projections. These cases exercise client dispatch and stale guards. */
|
|
function projected(kind: 'yes' | 'no' | 'expression' | 'pair'): GameView {
|
|
let game = createGame('GROUPTRUTHCONTROLS', 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('unlimited');
|
|
game = send(game, 'a', {type:'alliance', target:'b'});
|
|
game = send(game, 'b', {type:'alliance', target:'a'});
|
|
if (kind !== 'pair') {
|
|
const original = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(original);
|
|
game = send(game, 'a', {type:'coalitionPropose', groupId:original.id, entrant:'c',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
for (const actor of ['b','c']) {
|
|
const offer = game.coalitionState!.proposal!;
|
|
game = send(game, actor, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus,
|
|
turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
}
|
|
const view = viewGame(game, 'a');
|
|
view.alliancePromiseNexus = {turn:2};
|
|
view.alliancePromises = kind === 'expression' ? [] : [{event:'controlled', player:'a', asker:'d',
|
|
turn:2, partner:'b', answer:kind !== 'no'}];
|
|
view.allianceExpressionPromises = kind !== 'expression' ? [] : [{event:'controlled', player:'a', asker:'d',
|
|
turn:2, claim:{kind:'and', terms:[{kind:'partner',partner:'b'}, {kind:'partner',partner:'c'}]}, answer:true}];
|
|
view.alliancePromiseGuide = {action:kind === 'no'
|
|
? {type:'coalitionLeave', expectedRevision:view.coalitionState!.revision, alliancePromiseEvent:'controlled'}
|
|
: {type:'ready', alliancePromiseEvent:'controlled'}};
|
|
return view;
|
|
}
|
|
|
|
function button(game: GameView, sent: Action[], busy = false) {
|
|
const result = AlliancePromiseGuideButton({game, busy, send(action) { sent.push(action); }});
|
|
assert.ok(result); return result;
|
|
}
|
|
|
|
void test('group Yes and whole-AND guides render an enabled Ready action', () => {
|
|
for (const kind of ['yes', 'expression'] as const) {
|
|
const game = projected(kind), sent:Action[] = [];
|
|
const control = button(game, sent);
|
|
assert.equal(control.props.disabled, false);
|
|
assert.match(renderToStaticMarkup(createElement(AlliancePromiseGuideButton, {game, busy:false, send() {}})),
|
|
/Confirm Ready for this Nexus/);
|
|
control.props.onClick();
|
|
assert.deepEqual(sent, [game.alliancePromiseGuide!.action]);
|
|
}
|
|
});
|
|
|
|
void test('group No guide renders and dispatches its exact event-bound leave action', () => {
|
|
const game = projected('no'), sent:Action[] = [];
|
|
const control = button(game, sent);
|
|
assert.equal(control.props.disabled, false);
|
|
assert.match(renderToStaticMarkup(createElement(AlliancePromiseGuideButton, {game, busy:false, send() {}})),
|
|
/Leave your group alliance while honoring your answers/);
|
|
control.props.onClick();
|
|
assert.deepEqual(sent, [game.alliancePromiseGuide!.action]);
|
|
});
|
|
|
|
void test('group guide refuses stale revision, membership, event, and quiet-window changes', () => {
|
|
const mutations: ((game: GameView) => void)[] = [
|
|
game => { game.coalitionState!.revision += 1; },
|
|
game => { game.coalitionState!.groups[0].members.pop(); },
|
|
game => { game.players = game.players.filter(player => player.id !== 'b'); },
|
|
game => { game.alliancePromises[0].event = 'changed'; },
|
|
game => { game.alliancePromiseGuide!.action.expectedRevision = -1; },
|
|
game => { game.ready.push('a'); },
|
|
game => { game.nexus = false; },
|
|
game => { game.automaticContinuationPending = true; },
|
|
];
|
|
for (const mutate of mutations) {
|
|
const game = projected('no'), sent:Action[] = [], control = button(game, sent);
|
|
mutate(game); control.props.onClick(); assert.deepEqual(sent, []);
|
|
}
|
|
});
|
|
|
|
void test('reciprocal pair guidance remains usable with a staged coalition ledger', () => {
|
|
const game = projected('pair'), sent:Action[] = [];
|
|
const control = button(game, sent);
|
|
assert.equal(control.props.disabled, false); control.props.onClick();
|
|
assert.deepEqual(sent, [game.alliancePromiseGuide!.action]);
|
|
});
|