163 lines
5.4 KiB
TypeScript
163 lines
5.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { applyAction, viewGame, type GameView } from '../game/engine';
|
|
import {
|
|
MoritaniPlacement,
|
|
MoritaniTerrorSupply,
|
|
} from '../components/moritani-terror';
|
|
import {
|
|
moritaniPlacementChoices,
|
|
nexusMoritaniAction,
|
|
nexusMoritaniCanAct,
|
|
} from '../game/nexus-moritani-options';
|
|
import {
|
|
nexusMoritaniFixture,
|
|
nexusMoritaniToken,
|
|
nexusMoritaniInventory,
|
|
} from './fixture-nexus-moritani';
|
|
|
|
const html = (game: GameView, busy = false) =>
|
|
renderToStaticMarkup(
|
|
createElement(MoritaniPlacement, { game, busy, act() {} }),
|
|
);
|
|
|
|
void test('genuine Basic and Advanced Mentat keeps native relocation choices and offers explicit Cunning supply placement into Arrakis or a stack', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = nexusMoritaniFixture({ advanced, stack: true });
|
|
const v = viewGame(f.g, f.owner),
|
|
before = structuredClone(v);
|
|
const token = nexusMoritaniToken(f.g, 'robbery').id;
|
|
const native = moritaniPlacementChoices(v, false, token);
|
|
const cunning = moritaniPlacementChoices(v, true, token);
|
|
assert.ok(native.tokens.some((candidate) => candidate.status === 'placed'));
|
|
assert.ok(
|
|
cunning.tokens.every((candidate) => candidate.status === 'available'),
|
|
);
|
|
for (const destination of ['red_chasm', 'polar_sink', 'arrakeen']) {
|
|
assert.equal(native.destinations.includes(destination), false);
|
|
assert.ok(cunning.destinations.includes(destination));
|
|
const action = nexusMoritaniAction(
|
|
v,
|
|
v.nexusMoritani!.event,
|
|
token,
|
|
destination,
|
|
);
|
|
assert.deepEqual(action, {
|
|
type: 'decision',
|
|
token,
|
|
territory: destination,
|
|
nexus: v.nexusMoritani!.event,
|
|
});
|
|
const pending = applyAction(f.g, f.owner, action!);
|
|
assert.equal(pending.response?.kind, 'moritaniPlacement');
|
|
assert.equal(nexusMoritaniToken(pending, 'robbery').status, 'available');
|
|
nexusMoritaniInventory(pending);
|
|
}
|
|
assert.ok(!cunning.destinations.includes('hidden_mobile_stronghold'));
|
|
assert.ok(
|
|
!cunning.destinations.some((destination) =>
|
|
destination.startsWith('homeworld:'),
|
|
),
|
|
);
|
|
const markup = html(v);
|
|
assert.match(markup, /type="checkbox"/);
|
|
assert.doesNotMatch(markup, /checked=""/);
|
|
assert.match(markup, /decision-checkbox min-h-11/);
|
|
assert.match(markup, /Use Moritani Nexus Cunning/);
|
|
assert.match(markup, /Destination stronghold/);
|
|
assert.match(html(v, true), /disabled=""/);
|
|
assert.match(
|
|
renderToStaticMarkup(createElement(MoritaniTerrorSupply, { game: v })),
|
|
/Inspect Robbery/,
|
|
);
|
|
assert.deepEqual(v, before);
|
|
}
|
|
});
|
|
|
|
void test('Moritani shared Cunning action binds current private offer, available token and destination without accepting relocation or stale events', () => {
|
|
const f = nexusMoritaniFixture({ stack: true });
|
|
const v = viewGame(f.g, f.owner),
|
|
offer = v.nexusMoritani!;
|
|
const token = nexusMoritaniToken(f.g, 'robbery').id;
|
|
assert.ok(nexusMoritaniCanAct(v));
|
|
assert.equal(nexusMoritaniAction(v, 'stale', token, 'red_chasm'), null);
|
|
assert.equal(
|
|
nexusMoritaniAction(
|
|
v,
|
|
offer.event,
|
|
nexusMoritaniToken(f.g, 'sabotage').id,
|
|
'red_chasm',
|
|
),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
nexusMoritaniAction(v, offer.event, 'missing', 'red_chasm'),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
nexusMoritaniAction(v, offer.event, token, 'hidden_mobile_stronghold'),
|
|
null,
|
|
);
|
|
for (const edit of [
|
|
(g: GameView) => {
|
|
g.phase = 7;
|
|
},
|
|
(g: GameView) => {
|
|
g.turn++;
|
|
},
|
|
(g: GameView) => {
|
|
g.decision = null;
|
|
},
|
|
(g: GameView) => {
|
|
g.decision = { kind: 'moritaniPlacement', player: f.target };
|
|
},
|
|
(g: GameView) => {
|
|
g.nexusCards!.card = null;
|
|
},
|
|
(g: GameView) => {
|
|
g.players.find((p) => p.id === f.owner)!.ally = f.target;
|
|
},
|
|
(g: GameView) => {
|
|
g.automaticContinuationPending = true;
|
|
},
|
|
(g: GameView) => {
|
|
g.nexusMoritani!.blocked = 'Finish the current interaction.';
|
|
},
|
|
]) {
|
|
const blocked = structuredClone(v);
|
|
edit(blocked);
|
|
assert.equal(
|
|
nexusMoritaniAction(blocked, offer.event, token, 'red_chasm'),
|
|
null,
|
|
);
|
|
}
|
|
const blocked = structuredClone(v);
|
|
blocked.nexusMoritani!.blocked =
|
|
'This combination awaits its placement ruling.';
|
|
assert.match(html(blocked), /combination awaits its placement ruling/);
|
|
});
|
|
|
|
void test('nonowners cannot render or read private Cunning supply even from an accidentally broad view', () => {
|
|
const f = nexusMoritaniFixture({ stack: true });
|
|
for (const id of [f.target, f.observer]) {
|
|
const v = viewGame(f.g, id);
|
|
assert.equal(v.nexusMoritani, null);
|
|
assert.equal(html(v), '');
|
|
Object.defineProperty(v, 'nexusMoritani', {
|
|
get() {
|
|
throw new Error('Opponent Nexus offer read');
|
|
},
|
|
});
|
|
assert.equal(nexusMoritaniCanAct(v), false);
|
|
assert.deepEqual(moritaniPlacementChoices(v, true).tokens, []);
|
|
assert.equal(nexusMoritaniAction(v, 'event', 'token', 'red_chasm'), null);
|
|
const markup = renderToStaticMarkup(
|
|
createElement(MoritaniTerrorSupply, { game: v }),
|
|
);
|
|
assert.match(markup, /Hidden Terror/);
|
|
assert.doesNotMatch(markup, /Inspect Sabotage/);
|
|
}
|
|
});
|