92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { Children, createElement, isValidElement, type ReactNode } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { FremenRevivalGrant } from '../components/fremen-revival-grant';
|
|
import { createGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine';
|
|
|
|
/** Projection-only control fixtures; engine tests own the actual alliance and
|
|
* ordered Revival transitions. The ally's private revival inventory is not read. */
|
|
function projected(group = false): GameView {
|
|
const game = createGame('ORDEREDGRANTCONTROLS', newPlayer('f', 'Fremen', 'fremen'), true);
|
|
game.players.push(newPlayer('a', 'Ally', 'atreides'));
|
|
game.players.push(newPlayer('e', 'Emperor', 'emperor'));
|
|
Object.assign(game, { status: 'playing', phase: 4, turn: 3, order: ['f', 'e', 'a'] });
|
|
game.players[0].ally = 'a';
|
|
game.players[1].ally = 'f';
|
|
const view = viewGame(game, 'f');
|
|
view.players.find(player => player.id === 'a')!.autopilot = 'Easy';
|
|
view.players.find(player => player.id === 'e')!.bot = 'Medium';
|
|
view.revivalOrder = { version: 1, turn: 3, original: ['f', 'e', 'a'],
|
|
remaining: ['e', 'a'], completed: ['f'], started: false };
|
|
view.fremenRevivalGrant = { recipient: 'a', granted: false, blocked: null, waitReleased: false };
|
|
if (group) view.fremenGroupRevivalGrant = [
|
|
{ recipient: 'a', granted: false, blocked: null, waitReleased: false },
|
|
{ recipient: 'e', granted: false, blocked: null, waitReleased: false },
|
|
];
|
|
return view;
|
|
}
|
|
function markup(game: GameView, busy = false) {
|
|
return renderToStaticMarkup(createElement(FremenRevivalGrant, { game, busy, act() {} }));
|
|
}
|
|
function click(node: ReactNode, target: string): boolean {
|
|
for (const child of Children.toArray(node)) {
|
|
if (!isValidElement<{ children?: ReactNode; onClick?: () => void; 'data-testid'?: string }>(child)) continue;
|
|
if (child.props['data-testid'] === `fremen-revival-wait-release-${target}` && child.props.onClick) {
|
|
child.props.onClick();
|
|
return true;
|
|
}
|
|
if (click(child.props.children, target)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
function release(game: GameView, target: string, busy = false): Action[] {
|
|
const actions: Action[] = [];
|
|
click(FremenRevivalGrant({ game, busy, act(action) { actions.push(action); } }), target);
|
|
return actions;
|
|
}
|
|
|
|
void test('Fremen can release a later automated ally before its ordered Revival turn', () => {
|
|
const game = projected();
|
|
assert.match(markup(game), /Continue without waiting/);
|
|
assert.match(markup(game), /may still grant later while Revival remains open/);
|
|
assert.deepEqual(release(game, 'a'), [{type:'releaseFremenRevivalWait', target:'a'}]);
|
|
});
|
|
|
|
void test('release is disabled during a busy window or after release without hiding grant', () => {
|
|
const game = projected();
|
|
assert.deepEqual(release(game, 'a', true), []);
|
|
game.fremenRevivalGrant!.waitReleased = true;
|
|
assert.match(markup(game), /Ally can continue without waiting for your grant/);
|
|
assert.match(markup(game), /Grant ally free revival/);
|
|
assert.deepEqual(release(game, 'a'), []);
|
|
game.truthtrance = {} as NonNullable<GameView['truthtrance']>;
|
|
game.fremenRevivalGrant!.waitReleased = false;
|
|
assert.deepEqual(release(game, 'a'), []);
|
|
});
|
|
|
|
void test('group controls release each recipient independently without reading private forces', () => {
|
|
const game = projected(true);
|
|
for (const ally of game.players.filter(player => player.id !== 'f')) {
|
|
for (const field of ['spice', 'tanks', 'deadForces', 'revived', 'freeForcesRevived'])
|
|
Object.defineProperty(ally, field, {get() { throw new Error(`Read private ${field}`); }});
|
|
}
|
|
assert.deepEqual(release(game, 'a'), [{type:'releaseFremenRevivalWait', target:'a'}]);
|
|
assert.deepEqual(release(game, 'e'), [{type:'releaseFremenRevivalWait', target:'e'}]);
|
|
game.fremenGroupRevivalGrant![0].waitReleased = true;
|
|
assert.deepEqual(release(game, 'a'), []);
|
|
assert.deepEqual(release(game, 'e'), [{type:'releaseFremenRevivalWait', target:'e'}]);
|
|
});
|
|
|
|
void test('release is absent for human, finished, blocked or nonordered allies', () => {
|
|
for (const condition of ['human', 'finished', 'blocked', 'basic'] as const) {
|
|
const game = projected();
|
|
if (condition === 'human') game.players.find(player => player.id === 'a')!.autopilot = undefined;
|
|
if (condition === 'finished') game.revivalOrder!.remaining = ['e'];
|
|
if (condition === 'blocked') game.fremenRevivalGrant!.blocked = 'Grant prevented.';
|
|
if (condition === 'basic') game.advanced = false;
|
|
assert.doesNotMatch(markup(game), /Continue without waiting/, condition);
|
|
assert.deepEqual(release(game, 'a'), [], condition);
|
|
}
|
|
});
|