import assert from 'node:assert/strict'; import test from 'node:test'; import { Children, createElement, isValidElement, type ReactNode } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { NullentropyBox, NullentropySearch } from '../components/nullentropy-box'; import { createGame, joinGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine'; import { baseDeck } from '../game/cards'; import { richeseCards } from '../game/richese-cards'; // Controlled owner projections supplement the native Box/price source suite. // They prove presentation and the actual hook-free selection callback only; // no paid Game state, original cost identity or refund receipt is installed. function projected(paid = false) { const game = createGame('BOXSHIPMENTPRICECONTROLS', newPlayer('a', 'Atreides', 'atreides'), false, ['choam']); joinGame(game, newPlayer('r', 'Richese', 'richese')); joinGame(game, newPlayer('g', 'Guild', 'guild')); const view = viewGame(game, 'a'); const box = richeseCards().find(card => card.effect === 'nullentropyBox'); const cost = baseDeck().find(card => card.effect === 'karama'), other = baseDeck().find(card => card.kind === 'shield'); assert.ok(box && cost && other); view.status = 'playing'; view.phase = 5; view.active = 'a'; view.karamaShipping = { owner: 'r', player: 'g' }; view.nullentropy = { card: box, blocked: null, search: paid ? { event: 'controlled-paid-box-event', cards: [cost, other] } : null, }; if (paid) view.decision = { kind: 'nullentropy', player: 'a' }; return { game: view, box, cost, other }; } function boxMarkup(game: GameView, busy = false) { // NullentropyBox owns useId: always enter it through the React renderer. return renderToStaticMarkup(createElement(NullentropyBox, { game, busy, act() {} })); } function searchMarkup(game: GameView, busy = false) { return renderToStaticMarkup(createElement(NullentropySearch, { game, busy, act() {} })); } function labelText(node: ReactNode): string { if (typeof node === 'string' || typeof node === 'number') return String(node); return Children.toArray(node).map(child => isValidElement<{ children?: ReactNode }>(child) ? labelText(child.props.children) : typeof child === 'string' || typeof child === 'number' ? String(child) : '').join(''); } function click(node: ReactNode, label: string): boolean { for (const child of Children.toArray(node)) { if (!isValidElement<{ children?: ReactNode; onClick?: () => void }>(child)) continue; if (labelText(child.props.children) === label && child.props.onClick) { child.props.onClick(); return true; } if (click(child.props.children, label)) return true; } return false; } function select(game: GameView, name: string, busy = false): Action[] { const actions: Action[] = []; // This leaf is hook-free; only its real selection handler is invoked. click(NullentropySearch({ game, busy, act(action) { actions.push(action); } }), `Take ${name} and finish search`); return actions; } function guidance(html: string) { assert.match(html, /Provisional refund rule/); assert.match(html, /purchased shipping price, the player who bought it and its shipment beneficiary stay unchanged/); assert.match(html, /Recovering its original card cost prevents a refund if the Guild later stops that shipment with special Karama/); assert.match(html, /even after that card is discarded again/); assert.match(html, /Taking another card leaves any still-unclaimed cost under its usual refund rule/); } void test('active purchased shipping price guidance is persistent before Box payment without showing discard candidates', () => { const f = projected(); for (let render = 0; render < 3; render++) { const html = boxMarkup(f.game); guidance(html); assert.match(html, /Pay 2 spice to the bank to privately search/); assert.match(html, /Pay 2 spice and search privately/); assert.match(html, /There is no free cancellation after seeing the cards/); assert.doesNotMatch(html, new RegExp(`Take ${f.cost.name}|Take ${f.other.name}|controlled-paid-box-event`)); } }); void test('paid Box reminder and entitled selection panel retain the same provisional price and refund explanation', () => { const f = projected(true); for (let render = 0; render < 3; render++) { guidance(boxMarkup(f.game)); guidance(searchMarkup(f.game)); assert.match(boxMarkup(f.game), /Your Nullentropy Box search is paid/); assert.match(searchMarkup(f.game), /You have paid 2 spice/); assert.match(searchMarkup(f.game), /These discard cards are visible only to you during this paid search/); assert.match(searchMarkup(f.game), /Inspection does not select a card/); } }); void test('the actual paid search callbacks select either entitled card with only the unchanged event and card', () => { const f = projected(true), event = f.game.nullentropy!.search!.event; assert.deepEqual(select(f.game, f.cost.name), [{ type: 'decision', event, card: f.cost.id }]); assert.deepEqual(select(f.game, f.other.name), [{ type: 'decision', event, card: f.other.id }]); assert.deepEqual(select(f.game, 'Unshown card'), []); for (const card of [f.cost, f.other]) assert.equal(Object.keys(select(f.game, card.name)[0]).sort().join(','), 'card,event,type'); }); void test('busy controls preserve the two-spice fee and guide while suppressing actual paid selections', () => { const before = projected(), paid = projected(true); const html = boxMarkup(before.game, true); guidance(html); assert.match(html, /disabled=""[^>]*>Pay 2 spice and search privately/); assert.deepEqual(select(paid.game, paid.cost.name, true), []); assert.deepEqual(select(paid.game, paid.other.name, true), []); guidance(searchMarkup(paid.game, true)); }); void test('existing prepay blocks and full-hand guidance coexist with the purchased-price notice', () => { const f = projected(); f.game.nullentropy!.blocked = 'Finish the current owned preparation before paying.'; f.game.nullentropy!.fullHandExchange = true; const html = boxMarkup(f.game); guidance(html); assert.match(html, /Finish the current owned preparation before paying/); assert.match(html, /disabled=""[^>]*>Pay 2 spice and search privately/); assert.match(html, /Provisional full-hand exchange/); const paid = projected(true); paid.game.nullentropy!.fullHandExchange = true; assert.match(searchMarkup(paid.game), /Provisional full-hand exchange/); guidance(searchMarkup(paid.game)); }); void test('the price notice is conditional on an active public grant and a paid search remains owner-only', () => { const before = projected(); before.game.karamaShipping = null; assert.doesNotMatch(boxMarkup(before.game), /Provisional refund rule|Recovering its original card cost/); const paid = projected(true); paid.game.karamaShipping = null; assert.doesNotMatch(boxMarkup(paid.game), /Provisional refund rule/); assert.doesNotMatch(searchMarkup(paid.game), /Provisional refund rule/); paid.game.me = 'g'; assert.equal(searchMarkup(paid.game), ''); assert.deepEqual(select(paid.game, paid.cost.name), []); paid.game.me = 'a'; paid.game.decision = null; assert.equal(searchMarkup(paid.game), ''); assert.deepEqual(select(paid.game, paid.cost.name), []); }); void test('prepay guidance never inspects discard faces or an unprojected physical shipping cost', () => { const f = projected(); Object.defineProperty(f.game, 'discard', { get() { throw Error('The prepay panel inspected discard faces'); } }); Object.defineProperty(f.game.karamaShipping!, 'card', { get() { throw Error('The price cost is not public projection'); } }); guidance(boxMarkup(f.game)); });