Files
Dune/tests/truth-movement-sector-runtime.test.ts
T

122 lines
7.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 { MovementPromises } from '../components/movement-promises';
import { MovementExpressionPromiseGuideButton } from '../components/movement-expression-promises';
import { TravelPromiseGuideButton } from '../components/travel-promises';
import { TurnActionGuideButton } from '../components/turn-action-promises';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { viewGame, type Action, type Game, type GameView } from '../game/engine';
import {
actMovementSector as act, bindMovementSector as bind,
createTruthMovementSectorFixture as fixture, movementSectorInventory as inventory,
movementSectorPlayer as player, settleMovementSector as settle,
type MovementSectorQuestionKind,
} from './fixture-truth-movement-sector';
const kinds: MovementSectorQuestionKind[] = ['movement', 'movementExpression', 'mixedMovement', 'travel', 'turnActions'];
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
}
function row(game: Game, kind: MovementSectorQuestionKind, target: string): {
fulfilled?: boolean; released?: boolean; progress?: { territory: string; maximum: number }[];
sectorProgress?: { territory: string; sector: number; maximum: number }[];
} {
const rows = kind === 'movement' ? game.movementPromises : kind === 'movementExpression' || kind === 'mixedMovement'
? game.movementExpressionPromises : kind === 'travel' ? game.travelPromises : game.turnActionPromises;
const found = rows?.find(item => item.player === target); assert.ok(found); return found;
}
function answers(view: GameView, kind: MovementSectorQuestionKind) {
return kind === 'movement' ? view.truthMovementAnswers : kind === 'movementExpression'
? view.truthMovementExpressionAnswers : kind === 'mixedMovement' ? view.truthMixedMovementAnswers
: kind === 'travel' ? view.truthTravelAnswers : view.truthTurnActionAnswers;
}
function guide(view: GameView, kind: MovementSectorQuestionKind): Action | undefined {
return kind === 'movement' ? view.movementPromisePolicy?.[0] : kind === 'movementExpression' || kind === 'mixedMovement'
? view.movementExpressionPromiseGuide?.action : kind === 'travel'
? view.travelPromiseGuide?.action : view.turnActionGuide?.action;
}
function guideMarkup(view: GameView, kind: MovementSectorQuestionKind): string {
const noop = () => {};
return kind === 'movement' ? renderToStaticMarkup(createElement(MovementPromises, { game: view, busy: false, act: noop }))
: kind === 'movementExpression' || kind === 'mixedMovement' ? renderToStaticMarkup(createElement(MovementExpressionPromiseGuideButton, { game: view, busy: false, send: noop }))
: kind === 'travel' ? renderToStaticMarkup(createElement(TravelPromiseGuideButton, { game: view, busy: false, act: noop }))
: renderToStaticMarkup(createElement(TurnActionGuideButton, { game: view, busy: false, act: noop }));
}
for (const kind of kinds) void test(`${kind}: native Yes binds one exact ground sector and exposes a legal human/bot guide`, () => {
const f = fixture(), bound = bind(f, kind, 'yes'); let game = bound.game;
assert.ok(answers(viewGame(bound.asked, f.target), kind)?.includes('yes'));
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
rejected(game, f.target, { type: 'endMovement' });
const view = viewGame(game, f.target), action = guide(view, kind); assert.ok(action);
assert.equal(action.type, 'move'); assert.equal(action.territory, 'imperial_basin'); assert.equal(action.sector, f.sector);
assert.match(guideMarkup(view, kind), new RegExp(`sector ${f.sector}`, 'i'));
for (const difficulty of DIFFICULTIES) {
const bot = viewGame(game, f.target);
bot.players.find(entry => entry.id === f.target)!.bot = difficulty;
assert.deepEqual(botActions(bot)[0], action);
}
game = settle(act(game, f.target, action));
assert.equal(row(game, kind, f.target).fulfilled, true);
assert.ok((player(game, f.target).forces[`imperial_basin:${f.sector}`] ?? 0) > 0);
assert.equal(f.forceTotal(game), f.originalForces);
assert.deepEqual(inventory(game), f.originalCards);
});
for (const kind of kinds) void test(`${kind}: native No blocks only the named sector`, () => {
const f = fixture(); let game = bind(f, kind, 'no').game;
rejected(game, f.target, f.move(f.sector));
game = settle(act(game, f.target, f.move(f.alternative)));
assert.equal(player(game, f.target).forces[`imperial_basin:${f.alternative}`], 1);
assert.notEqual(row(game, kind, f.target).fulfilled, true);
game = settle(act(game, f.target, { type: 'endMovement' }));
assert.equal(row(game, kind, f.target).fulfilled, true);
assert.equal(f.forceTotal(game), f.originalForces);
assert.deepEqual(inventory(game), f.originalCards);
});
for (const kind of kinds) void test(`${kind}: a different first sector preserves Yes only while actual Hajr remains available`, () => {
const f = fixture(); assert.ok(f.hajr); let game = bind(f, kind, 'yes').game;
game = settle(act(game, f.target, f.move(f.alternative)));
assert.notEqual(row(game, kind, f.target).fulfilled, true);
rejected(game, f.target, { type: 'endMovement' });
game = settle(act(game, f.target, { type: 'card', card: f.hajr }));
game = settle(act(game, f.target, f.move(f.sector)));
assert.equal(row(game, kind, f.target).fulfilled, true);
assert.equal(player(game, f.target).forces[`imperial_basin:${f.sector}`], 1);
assert.equal(player(game, f.target).forces[`imperial_basin:${f.alternative}`], 1);
assert.equal(game.discard.filter(card => card.id === f.hajr).length, 1);
assert.equal(f.forceTotal(game), f.originalForces);
assert.deepEqual(inventory(game), f.originalCards);
});
for (const kind of ['movementExpression', 'mixedMovement', 'travel', 'turnActions'] as const)
void test(`${kind}: two exact-sector conditions use separate native group maxima`, () => {
const f = fixture(); assert.ok(f.hajr); let game = bind(f, kind, 'yes', true).game;
game = settle(act(game, f.target, f.move(f.sector, 2)));
assert.notEqual(row(game, kind, f.target).fulfilled, true);
assert.deepEqual(row(game, kind, f.target).progress, [{ territory: 'imperial_basin', maximum: 2 }]);
game = settle(act(game, f.target, { type: 'card', card: f.hajr }));
game = settle(act(game, f.target, f.move(f.alternative)));
assert.equal(row(game, kind, f.target).fulfilled, true);
assert.deepEqual(row(game, kind, f.target).progress, [{ territory: 'imperial_basin', maximum: 2 }]);
assert.deepEqual(row(game, kind, f.target).sectorProgress, [
{ territory: 'imperial_basin', sector: f.sector, maximum: 2 },
{ territory: 'imperial_basin', sector: f.alternative, maximum: 1 },
]);
assert.equal(f.forceTotal(game), f.originalForces);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('invalid movement sector cannot consume the declared physical Truthtrance', () => {
const f = fixture(), declared = bind(f, 'movement', 'yes');
// Reopen only the actual original declaration returned by the fixture; no state is installed.
rejected(declared.declared, f.asker, { type: 'truthAsk', question: {
kind: 'movement', target: f.target, territory: 'imperial_basin', sector: 0, minimum: 1,
} });
assert.ok(player(declared.declared, f.asker).hand.some(card => card.id === f.truth));
});