111 lines
6.1 KiB
TypeScript
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);
|
|
},
|
|
);
|