Implement Harkonnen Nexus exchanges with durable traitor declarations
This commit is contained in:
1 parent
d820c8a414
commit
d5830d260c
20 files changed
+3158
-10
No files matched your search
@@ -0,0 +1,134 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user