Add a structured future single-movement predicate and persistent authoring, feasible-answer and promise controls for bare native Basic active post-shipment turns. Yes requires one matching physical movement while possible; No survives every later Hajr/Ornithopter opportunity until actual turn closure. Matching physical commitment precedes disposal and arrival effects. Preserve compatible promises after opposing loss and reject voluntary destruction of the last owned completion. Reuse the actual movement declaration and physical commitment in a focused owned-action search rather than a strategic shortlist. Retain typed groups, original two-group cohorts, held Hajr, both Ornithopter modes, city access and current Kulon bonus. Deterministic preparation on private clones retains physical cost semantics without inventing response/discard events in reachability. Existing native/optional/Advanced movement retains its actual controls. Verify 17-file218/218, types/lint/build, seven original-action gameplay/control/table-rendering programmes and all four existing legal profiles. Original public classic, paid cache Ornithopter and native allowed-Kulon sources use only unopened positions and scalar lotteries; extra-card/force-loss edges are explicitly controlled. Repair nearby stale fixture continuations to acknowledge Semuta/battle outcomes and put pre-movement Hajr in Basic. Full modes, wider preparation/timing/module coverage, comprehensive assurance and deployed acceptance remain open. No strategic tuning or saved-room/reset/restart/deployment operation.
53 lines
3.5 KiB
TypeScript
53 lines
3.5 KiB
TypeScript
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 { MovementClaimFields } from '../components/movement-claim-fields';
|
||
import { Truthtrance } from '../components/truthtrance';
|
||
import { viewGame, type Action } from '../game/engine';
|
||
import { createTruthMovementFixture, actTruthMovement as act } from './fixture-truth-movement';
|
||
|
||
function question() {
|
||
const f = createTruthMovementFixture(); let game = act(f.game, f.asker, f.truthStep.action);
|
||
for (const p of game.players) if (!game.truthtrance!.passed.includes(p.id)) game = act(game, p.id, { type: 'truthPass' });
|
||
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'movement', target: f.target, territory: 'carthag', minimum: 6 } });
|
||
return { f, game };
|
||
}
|
||
function clickables(node: unknown): { children?: unknown; disabled?: boolean; onClick: () => void }[] {
|
||
if (Array.isArray(node)) return node.flatMap(clickables);
|
||
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 }] : []), ...clickables(props.children)];
|
||
}
|
||
void test('the actual movement question shows truthful feasible answers and its continuing No semantics', () => {
|
||
const { f, game } = question(), view = viewGame(game, f.target);
|
||
const html = renderToStaticMarkup(createElement(Truthtrance, { game: view, act() {}, busy: false }));
|
||
assert.match(html, /before your Shipment and Movement turn ends/);
|
||
assert.match(html, /No forbids every matching move through later Hajr or Ornithopter/);
|
||
assert.match(html, />Answer Yes<\/button>/); assert.match(html, />Answer No<\/button>/);
|
||
assert.doesNotMatch(html, />I don’t know yet<\/button>/);
|
||
assert.equal(viewGame(game, f.asker).truthMovementAnswers, null);
|
||
});
|
||
void test('persistent promise controls submit the actual private witness and respect busy state', () => {
|
||
const { f, game: asked } = question();
|
||
const game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||
const view = viewGame(game, f.target), actions: Action[] = [];
|
||
const props = { game: view, act: (action: Action) => { actions.push(action); }, busy: false };
|
||
const html = renderToStaticMarkup(createElement(MovementPromises, props));
|
||
assert.match(html, /Your ground-movement promises/); assert.match(html, /Move 6 forces to Carthag/);
|
||
clickables(MovementPromises(props))[0].onClick();
|
||
assert.deepEqual(actions, view.movementPromisePolicy);
|
||
const next = act(game, f.target, actions[0]); assert.equal(next.movementPromises![0].fulfilled, true);
|
||
clickables(MovementPromises({ ...props, busy: true }))[0].onClick(); assert.equal(actions.length, 1);
|
||
assert.equal(MovementPromises({ ...props, game: viewGame(game, f.asker) }), null);
|
||
assert.equal(viewGame(game, f.asker).movementPromisePolicy, null);
|
||
});
|
||
void test('movement authoring fields describe ground forces and reject invalid physical minima', () => {
|
||
const html = renderToStaticMarkup(createElement(MovementClaimFields, {
|
||
id: 'movement', value: { territory: 'carthag', minimum: 0 }, onChange() {},
|
||
}));
|
||
assert.match(html, /Ground-movement destination/); assert.match(html, /Minimum physical forces in one ground move/);
|
||
assert.match(html, /aria-invalid="true"/); assert.doesNotMatch(html, /from reserves|shipment conditions/);
|
||
});
|