Files
Dune/tests/residual-cunning-controls.test.ts
T

96 lines
5.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { ResidualPoison } from '../components/residual-poison';
import { createGame, joinGame, newPlayer, viewGame, type GameView } from '../game/engine';
import { richeseCards } from '../game/richese-cards';
// Controlled availability projections supplement the genuine Residual/Cunning
// source and reconciliation suite. No played battle, answer, victim, card hand
// or pending Game state is installed. The component owns useId and is always
// entered through React's server renderer, never invoked as a plain function.
function projected(): GameView {
const game = createGame('RESIDUALCUNNINGCONTROLS', newPlayer('a', 'Atreides', 'atreides'), true, ['choam']);
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
joinGame(game, newPlayer('r', 'Richese', 'richese'));
const view = viewGame(game, 'a'), card = richeseCards().find(card => card.effect === 'residualPoison');
assert.ok(card);
view.status = 'playing'; view.phase = 6;
view.residualPoison = { card, blocked: null, event: 'controlled-residual-pre-leader-event', target: 'e' };
return view;
}
function markup(game: GameView, busy = false) {
return renderToStaticMarkup(createElement(ResidualPoison, { game, busy, act() {} }));
}
function playButton(html: string) {
const button = html.match(/<button\b[^>]*>Play Residual Poison against Emperor<\/button>/)?.[0];
assert.ok(button, 'The original direct Residual action remains readable'); return button;
}
function priority(html: string) {
assert.match(html, /aria-label="Provisional inspection priority"/);
assert.match(html, /keep the original nonleader Atreides answer if it can still be honored on its own/);
assert.match(html, /Both answers must permit one legal Battle Plan/);
assert.match(html, /an extra Cunning answer may need to be answered again for the same field after a leader dies/);
assert.match(html, /If the original answer becomes impossible, answer its original field again first/);
assert.match(html, /This spends no additional Nexus card and does not redraw the victim/);
}
void test('Residual Poison persistently explains provisional native-first Cunning reconciliation without another choice', () => {
const game = projected();
for (let render = 0; render < 3; render++) {
const html = markup(game); priority(html);
assert.match(html, /Residual Poison: before choosing leaders/);
assert.match(html, /leader is randomly sent to the Tanks, with no spice bounty/);
assert.match(html, /You do not choose the leader/);
assert.doesNotMatch(html, /Confirm reconciliation|Acknowledge|Choose a victim|Redraw victim|countdown/);
}
});
void test('a true projected Residual availability remains enabled alongside the Cunning reminder', () => {
const game = projected(), html = markup(game); priority(html);
assert.doesNotMatch(playButton(html), /\sdisabled=/);
assert.doesNotMatch(html, /Cunning.*(?:prevents|forbids) Residual Poison/);
});
void test('busy rendering keeps the inspection priority visible while disabling the original action', () => {
const html = markup(projected(), true); priority(html);
assert.match(playButton(html), /disabled=""/);
});
void test('native unavailable reasons remain authoritative and never imply that committed leaders can be poisoned', () => {
for (const reason of [
'A battle leader has already been committed.',
'Finish the current inspection answer before using Residual Poison.',
'No eligible opposing leader is available.',
]) {
const game = projected(); game.residualPoison!.blocked = reason;
for (let render = 0; render < 2; render++) {
const html = markup(game); priority(html);
assert.ok(html.includes(reason)); assert.match(playButton(html), /disabled=""/);
assert.match(html, /Residual Poison: before choosing leaders/);
}
}
});
void test('missing original event or target still uses the pre-leader fallback rather than enabling the card', () => {
const event = projected(); event.residualPoison!.event = null;
const eventHtml = markup(event); priority(eventHtml);
assert.match(eventHtml, /Use this card against your opponent before battle leaders are chosen/);
assert.match(playButton(eventHtml), /disabled=""/);
const target = projected(); target.residualPoison!.target = null;
const targetHtml = markup(target); priority(targetHtml);
assert.match(targetHtml, /Play Residual Poison against your battle opponent/);
assert.match(targetHtml, /disabled=""[^>]*>Play Residual Poison against your battle opponent/);
const absent = projected(); absent.residualPoison = null;
assert.equal(markup(absent), '');
});
void test('the general priority reminder reads no hidden opposing hand or private inspection answer', () => {
const game = projected();
for (const player of game.players) for (const field of ['hand', 'spice', 'traitors', 'leaders'])
Object.defineProperty(player, field, { get() { throw Error(`Residual guide read private ${field}`); } });
Object.defineProperty(game, 'nexusAtreides', { get() { throw Error('Residual guide inspected private Cunning knowledge'); } });
const html = markup(game); priority(html); assert.doesNotMatch(playButton(html), /\sdisabled=/);
});