122 lines
7.5 KiB
TypeScript
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));
|
|
});
|