135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { registerHooks } from 'node:module';
|
||
import { createElement } from 'react';
|
||
import { renderToStaticMarkup } from 'react-dom/server';
|
||
import { viewGame, type GameView } from '../game/engine';
|
||
import {
|
||
nexusTraitorDrawAction,
|
||
nexusTraitorReturnAction,
|
||
} from '../game/nexus-traitor-options';
|
||
import {
|
||
nexusTraitorFixture,
|
||
nexusTraitorDraw,
|
||
} from './fixture-nexus-traitors';
|
||
|
||
const aliases = registerHooks({
|
||
resolve(specifier, context, next) {
|
||
return next(
|
||
specifier === 'next/image' ? 'vinext/shims/image' : specifier,
|
||
context,
|
||
);
|
||
},
|
||
});
|
||
const { NexusTraitors } = await import('../components/nexus-traitors');
|
||
aliases.deregister();
|
||
|
||
function html(game: GameView, busy = false) {
|
||
return renderToStaticMarkup(
|
||
createElement(NexusTraitors, { game, busy, act() {} }),
|
||
);
|
||
}
|
||
|
||
void test('Nexus draw control uses the private current offer and keeps human Cunning available over preserved controls', () => {
|
||
const v = viewGame(nexusTraitorFixture(), 'p');
|
||
const offer = v.nexusTraitors!.offer!;
|
||
const expected = {
|
||
type: 'nexusTraitorDraw',
|
||
event: offer.event,
|
||
mode: 'cunning',
|
||
};
|
||
assert.deepEqual(nexusTraitorDrawAction(v, offer.event, 'cunning'), expected);
|
||
v.decision = { kind: 'nullentropy', player: 'p' };
|
||
v.automaticContinuationPending = true;
|
||
assert.deepEqual(nexusTraitorDrawAction(v, offer.event, 'cunning'), expected);
|
||
assert.match(html(v), /Draw 1 Traitor card/);
|
||
assert.match(html(v, true), /disabled/);
|
||
assert.equal(nexusTraitorDrawAction(v, 'stale', 'cunning'), null);
|
||
assert.equal(nexusTraitorDrawAction(v, offer.event, 'secretAlly'), null);
|
||
offer.blocked = 'Finish the current Nexus card draw.';
|
||
assert.equal(nexusTraitorDrawAction(v, offer.event, 'cunning'), null);
|
||
assert.match(html(v), /Finish the current Nexus card draw/);
|
||
offer.blocked = null;
|
||
v.nexusCards!.card = 'atreides';
|
||
assert.equal(nexusTraitorDrawAction(v, offer.event, 'cunning'), null);
|
||
});
|
||
|
||
void test('owner exchange renders real identities and explicit choice without automatic confirmation', () => {
|
||
const v = viewGame(nexusTraitorDraw(nexusTraitorFixture()), 'p');
|
||
const pending = v.nexusTraitors!.pending!;
|
||
const rendered = html(v);
|
||
assert.equal(
|
||
(rendered.match(/type="checkbox"/g) ?? []).length,
|
||
pending.choices.length,
|
||
);
|
||
assert.doesNotMatch(rendered, /checked=""/);
|
||
assert.match(rendered, /0 of 1 selected/);
|
||
assert.match(rendered, /previous game action is paused/);
|
||
assert.match(rendered, /disabled=""[^>]*>Return selected card/);
|
||
for (const choice of pending.choices) {
|
||
const identity = v.allLeaders.find((leader) => leader.id === choice.id)!;
|
||
assert.ok(rendered.includes(identity.name));
|
||
assert.ok(rendered.includes(`Inspect traitor: ${identity.name}`));
|
||
assert.ok(rendered.includes(`Strength ${identity.strength}`));
|
||
}
|
||
const drawn = pending.choices.find((choice) => choice.drawn)!.id;
|
||
assert.deepEqual(nexusTraitorReturnAction(v, pending.event, [drawn]), {
|
||
type: 'nexusTraitorReturn',
|
||
event: pending.event,
|
||
cards: [drawn],
|
||
});
|
||
});
|
||
|
||
void test('Face Dancer exchange requires exactly two projected choices and excludes previously revealed dancers', () => {
|
||
const g = nexusTraitorFixture({ ownerFaction: 'tleilaxu' });
|
||
g.players[0].faceDancers![0].revealed = true;
|
||
const excluded = g.players[0].faceDancers![0].leader;
|
||
const v = viewGame(nexusTraitorDraw(g), 'p');
|
||
const pending = v.nexusTraitors!.pending!;
|
||
const selected = pending.choices.slice(0, 2).map((choice) => choice.id);
|
||
assert.equal(pending.count, 2);
|
||
assert.ok(!pending.choices.some((choice) => choice.id === excluded));
|
||
assert.match(html(v), /Inspect Face Dancer:/);
|
||
assert.match(html(v), /Previously revealed Face Dancers are not available/);
|
||
assert.ok(nexusTraitorReturnAction(v, pending.event, selected));
|
||
for (const cards of [
|
||
[],
|
||
selected.slice(0, 1),
|
||
[selected[0], selected[0]],
|
||
[selected[0], excluded],
|
||
[...selected, excluded],
|
||
])
|
||
assert.equal(nexusTraitorReturnAction(v, pending.event, cards), null);
|
||
assert.equal(nexusTraitorReturnAction(v, 'old-exchange', selected), null);
|
||
v.decision = { kind: 'nullentropy', player: 'p' };
|
||
v.automaticContinuationPending = true;
|
||
assert.ok(nexusTraitorReturnAction(v, pending.event, selected));
|
||
v.truthtrance = { stage: 'priority', queue: [], passed: [], question: null };
|
||
assert.equal(nexusTraitorReturnAction(v, pending.event, selected), null);
|
||
assert.match(html(v), /Finish Truthtrance before returning/);
|
||
});
|
||
|
||
void test('observer exchange controls never read private card choices', () => {
|
||
const g = nexusTraitorDraw(nexusTraitorFixture());
|
||
for (const id of ['q', 'r']) {
|
||
const v = viewGame(g, id);
|
||
const pending = v.nexusTraitors!.pending!;
|
||
assert.deepEqual(pending.choices, []);
|
||
Object.defineProperty(pending, 'choices', {
|
||
get() {
|
||
throw new Error('Read another seat’s choices');
|
||
},
|
||
});
|
||
const rendered = html(v);
|
||
assert.match(rendered, /Waiting for Owner to return 1 card privately/);
|
||
assert.doesNotMatch(
|
||
rendered,
|
||
/checkbox|Inspect traitor|Strength|Newly drawn/,
|
||
);
|
||
assert.equal(
|
||
nexusTraitorReturnAction(v, pending.event, ['anything']),
|
||
null,
|
||
);
|
||
}
|
||
});
|