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 { SpiceBankerControl } from '../components/spice-banker'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { applyAction, viewGame, type GameView, type Plan, } from '../game/engine'; import { spiceBankerGame } from './spice-banker-fixture'; const aliases = registerHooks({ resolve(specifier, context, next) { return next( specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context, ); }, }); const { PrivateBattlePlan } = await import('../components/private-battle-plan'); const { RevealedBattle } = await import('../components/revealed-battle'); aliases.deregister(); function renderControl( game: GameView, selectedLeader: string, ownSupport: number, value: number, ) { return renderToStaticMarkup( createElement(SpiceBankerControl, { game, selectedLeader, ownSupport, value, onChange() {}, }), ); } void test('Spice Banker renders a bounded separate own-spice commitment only for the controlled living skilled disc', () => { const native = spiceBankerGame(); native.players[0].spice = 4; const view = viewGame(native, 'a'); const html = renderControl(view, 'emperor-0', 2, 2); assert.match(html, /aria-label="Spice Banker battle commitment"/); assert.match(html, /id="battle-banker-spice"[^>]*max="2"[^>]*value="2"/); const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' '); assert.match(text, /Plan funding: 2 own spice for force support \+ 2 committed to Spice Banker = 4 own spice total/); assert.match(text, /Each committed spice adds 1 strength if this skilled leader survives/); assert.match(text, /commitment goes to the Bank after a win or loss/); assert.match(text, /sole winner through a traitor call spends no Battle Plan spice/); assert.equal(renderControl(view, 'emperor-1', 0, 3), ''); const faceUp = structuredClone(view); faceUp.leaderSkills!.assignments.find( (assignment) => assignment.skill === 'spice-banker', )!.faceUp = true; assert.equal(renderControl(faceUp, 'emperor-0', 0, 3), ''); const dead = structuredClone(view); dead.players .find((player) => player.id === 'a')! .leaders.find((leader) => leader.id === 'emperor-0')!.dead = true; assert.equal(renderControl(dead, 'emperor-0', 0, 3), ''); const foreign = structuredClone(view); foreign.leaderSkills!.assignments.find( (assignment) => assignment.skill === 'spice-banker', )!.controller = 'd'; assert.equal(renderControl(foreign, 'emperor-0', 0, 3), ''); const combined = structuredClone(native); combined.expansions = ['ix']; assert.match(renderControl(viewGame(combined, 'a'), 'emperor-0', 2, 2), /id="battle-banker-spice"[^>]*max="2"[^>]*value="2"/); const capturedGame = spiceBankerGame(false, true); capturedGame.players[0].spice = 3; const captive = viewGame(capturedGame, 'a'); assert.match( renderControl(captive, 'guild-0', 0, 3), /Spice Banker commitment/, ); }); void test('full-plan inspection and public reveal show the exact commitment without leaking it to another private viewer', () => { const plan: Plan = { dial: 1, support: 0, bankerSpice: 3, leader: 'emperor-0', weapon: null, defense: null, }; const inspected = { me: 'atreides', players: [ { id: 'atreides', name: 'Atreides' }, { id: 'emperor', name: 'Emperor' }, { id: 'observer', name: 'Observer' }, ], allLeaders: [], battle: { revealed: false, noFieldPlayers: [], fullPlan: { owner: 'atreides', target: 'emperor' }, fullPlanInsight: { target: 'emperor', plan, cards: [] }, }, } as unknown as GameView; const privateHtml = renderToStaticMarkup( createElement(PrivateBattlePlan, { game: inspected }), ); assert.match(privateHtml, /Spice Banker commitment<\/dt>