import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement, type ReactElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { MovementPromises } from '../components/movement-promises'; import { NullentropySearch } from '../components/nullentropy-box'; import { viewGame, type Action, type Game } from '../game/engine'; import { createTruthMovementGholaFixture, createTruthMovementBoxGholaFixture, actTruthMovementGhola as act, advanceTruthMovementGhola as advance, truthMovementGholaClean as clean, truthMovementGholaPlayer as player, type TruthMovementGholaFixture, } from './fixture-truth-movement-ghola'; type Props = Parameters[0]; type Control = { children?: unknown; disabled?: boolean; onClick: () => void }; function controls(node: unknown): Control[] { if (Array.isArray(node)) return node.flatMap(controls); if (!node || typeof node !== 'object' || !('props' in node)) return []; const props = (node as ReactElement<{ children?: unknown; disabled?: boolean; onClick?: () => void }>).props; return [...(props.onClick ? [{ ...props, onClick: props.onClick }] : []), ...controls(props.children)]; } const markup = (props: Props) => renderToStaticMarkup(createElement(MovementPromises, props)); type Source = Pick; /** Actual declaration, answer and native discard controls; no installed policy. */ function bind(f: Source): Game { let game = act(f.game, f.asker, f.truthStep.action); while (game.truthtrance?.stage === 'priority') { const p = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(p); game = act(game, p.id, { type: 'truthPass' }); } game = act(game, f.asker, f.askStep.action); assert.ok(viewGame(game, f.target).truthMovementAnswers?.includes('yes')); game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' }); return advance(game, clean); } function clickCurrent(game: Game, actor: string, label: string): Action { const sent: Action[] = [], props = { game: viewGame(game, actor), busy: false, act: (action: Action) => { sent.push(action); } }; const available = controls(MovementPromises(props)); assert.equal(available.length, 1); assert.equal(available[0].children, label); assert.equal(available[0].disabled, false); assert.match(markup(props), /Yes: you must make a ground movement of at least 20 physical forces to Carthag/); assert.match(markup(props), /a shipment can prepare that move but does not fulfill it/); const busy = controls(MovementPromises({ ...props, busy: true })); assert.equal(busy.length, 1); assert.equal(busy[0].disabled, true); busy[0].onClick(); assert.deepEqual(sent, []); assert.match(markup({ ...props, busy: true }), /Your ground-movement promises/); available[0].onClick(); assert.deepEqual(sent, props.game.movementPromisePolicy); assert.equal(sent[0], props.game.movementPromisePolicy![0]); return sent[0]; } function assertWaitingForNativeControl(game: Game, actor: string) { const props = { game: viewGame(game, actor), busy: false, act() { assert.fail('A suspended native response must not submit movement'); } }; assert.equal(props.game.movementPromisePolicy, null); assert.equal(controls(MovementPromises(props)).length, 0); assert.match(markup(props), /Your ground-movement promises/); assert.doesNotMatch(markup(props), /