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"/); assert.match( html, /Plan funding: 2 own spice for force support \+ 2 committed to Spice Banker = 4 own spice total/, ); assert.match( html, /Each committed spice adds 1 strength if this skilled leader survives/, ); assert.match(html, /commitment goes to the Bank after a win or loss/); assert.match( html, /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 combinedModule = structuredClone(view); combinedModule.expansions = ['ix']; assert.equal(renderControl(combinedModule, 'emperor-0', 0, 3), ''); 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, 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>