Files
Dune/tests/semuta-market-ghola-controls.test.tsx
T
Quintine 79fc12c1c1
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Semuta after market Ghola
2026-10-29 08:34:18 +11:00

111 lines
6.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement, type ReactElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { SemutaReaction } from '../components/semuta-reaction';
import { viewGame, type Action, type Game } from '../game/engine';
import {
createSemutaMarketGholaFixture, actSemutaMarketGhola as act,
allowSemutaMarketGholaIncome, passSemutaMarketGhola, semutaMarketGholaPlayer as player,
} from './fixture-semuta-market-ghola';
type Props = Parameters<typeof SemutaReaction>[0];
type Control = { children?: unknown; disabled?: boolean; onClick: () => void };
function controls(node: unknown): Control[] {
if (Array.isArray(node)) return node.flatMap(controls);
if (!node || typeof node !== 'object' || !('props' in node)) return [];
const props = (node as ReactElement<{children?: unknown; disabled?: boolean; onClick?: () => void}>).props;
return [...(props.onClick ? [{...props, onClick: props.onClick}] : []), ...controls(props.children)];
}
const markup = (props: Props) => renderToStaticMarkup(createElement(SemutaReaction, props));
function control(game: Game, actor: string, label: 'Continue' | 'Commit Semuta Drug', busy = false) {
const sent: Action[] = [], props = {game:viewGame(game, actor), busy, act(action: Action) {sent.push(action);}};
const button = controls(SemutaReaction(props)).find(candidate => candidate.children === label);
assert.ok(button, `The actual fresh discard offers ${label}`);
assert.equal(button.disabled, busy);
button.onClick();
if (busy) { assert.deepEqual(sent, []); return null; }
assert.equal(sent.length, 1);
assert.deepEqual(sent[0], {type:label === 'Continue' ? 'semutaPass' : 'semutaCommit',
event:props.game.semutaReaction!.event});
return sent[0];
}
function parked(game: Game) {
const pending = game.pendingTreacheryDiscard, market = game.pendingChoamMarketGhola;
assert.ok(pending?.continuation.kind === 'ordinaryCardDiscard');
assert.equal(pending.continuation.effect, 'ghola');
assert.ok(market);
assert.equal(market.response.kind, 'choamSale');
assert.deepEqual(game.choamMarket, market.market);
const income = pending.continuation.resume.response;
assert.ok(income?.kind === 'revivalIncome');
assert.equal(game.response, null);
return {event:pending.batch.event, response:structuredClone(market.response),
market:structuredClone(market.market), income:structuredClone(income)};
}
void test('every seat sees a neutral persistent market-Ghola offer; a pass leaves the parent and income parked', () => {
const f = createSemutaMarketGholaFixture(), game = f.game;
const original = parked(game), before = structuredClone(game);
for (const seat of game.players) {
const view = viewGame(game, seat.id), reaction = view.semutaReaction;
assert.ok(reaction); assert.equal(reaction.stage, 'offer');
assert.deepEqual(reaction.candidates, []);
const html = markup({game:view, busy:false, act() {}});
assert.match(html, /Fresh Treachery discard/);
assert.match(html, /Continue when you are ready/);
assert.doesNotMatch(html, /Ghola/);
}
assert.deepEqual(game, before, 'Rendering does not acknowledge or resume the parent.');
assert.equal(control(game, f.owner, 'Continue', true), null);
const pass = control(game, f.owner, 'Continue'); assert.ok(pass);
assert.throws(() => act(game, f.owner, {...pass,event:'stale-market-ghola'}));
assert.deepEqual(game, before, 'A stale event cannot change the pending response.');
const next = act(game, f.owner, pass);
assert.deepEqual(parked(next), original);
assert.match(markup({game:viewGame(next, f.owner), busy:false, act() {}}), /You continued. Waiting for other seats/);
const continued = passSemutaMarketGhola(next);
assert.equal(continued.pendingTreacheryDiscard, null);
assert.deepEqual(continued.response, original.income);
assert.deepEqual(continued.pendingChoamMarketGhola?.response, original.response);
const restored = allowSemutaMarketGholaIncome(continued);
assert.equal(restored.pendingChoamMarketGhola, null);
assert.deepEqual(restored.response, original.response);
assert.deepEqual(restored.choamMarket, original.market);
});
for (const fullHand of [false, true]) void test(
`the market-Ghola claim resumes the same income and sale${fullHand ? ' with a full hand' : ''}`, () => {
const f = createSemutaMarketGholaFixture({fullHand}), game = f.game;
const original = parked(game), before = structuredClone(game);
const view = viewGame(game, f.claimant);
assert.equal(view.semutaReaction?.canCommit, true);
const html = markup({game:view,busy:false,act() {}});
assert.match(html, /Commit Semuta Drug/);
assert.doesNotMatch(html, /Ghola/);
assert.equal(control(game, f.claimant, 'Commit Semuta Drug', true), null);
const commit = control(game, f.claimant, 'Commit Semuta Drug'); assert.ok(commit);
assert.throws(() => act(game, f.claimant, {...commit,event:'stale-market-ghola'}));
assert.deepEqual(game, before);
let next = act(game, f.claimant, commit);
if (next.pendingTreacheryDiscard?.reaction?.stage === 'select') {
const selection = viewGame(next, f.claimant).semutaReaction;
assert.ok(selection && selection.stage === 'select');
assert.deepEqual(selection.candidates.map(card => card.id), [f.ghola]);
assert.deepEqual(viewGame(next, f.owner).semutaReaction?.candidates, []);
next = act(next, f.claimant, {type:'semutaSelect',event:original.event,card:f.ghola});
}
assert.equal(next.pendingTreacheryDiscard, null);
assert.deepEqual(next.response, original.income);
assert.deepEqual(next.choamMarket, original.market);
assert.deepEqual(next.pendingChoamMarketGhola?.response, original.response);
assert.ok(player(next, f.claimant).hand.some(card => card.id === f.ghola));
assert.ok(!player(next, f.claimant).hand.some(card => card.id === f.semuta));
next = allowSemutaMarketGholaIncome(next);
assert.equal(next.pendingChoamMarketGhola, null);
assert.deepEqual(next.response, original.response);
assert.deepEqual(next.choamMarket, original.market);
},
);