import assert from 'node:assert/strict'; import test from 'node:test'; import { registerHooks } from 'node:module'; import { readFileSync } from 'node:fs'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { createGame, joinGame, newPlayer, viewGame, type Action, type Game, type GameView } from '../game/engine'; import { act, advance, clean, inventory, player } from './fixture-kull-richese-semuta'; const aliases = registerHooks({ resolve(specifier, context, next) { return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context); }, load(url, context, next) { if (!url.endsWith('.module.css')) return next(url, context); const css = readFileSync(new URL(url), 'utf8'); const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]])); return {format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true}; }, }); const { GameTable } = await import('../components/game-table'); const { Truthtrance } = await import('../components/truthtrance'); const { BribePromiseGuideButton } = await import('../components/bribe-promises'); aliases.deregister(); const table = (game:GameView) => renderToStaticMarkup(createElement(GameTable, {game,busy:false,send:async () => {},onExit() {}})); const truth = (game:GameView) => renderToStaticMarkup(createElement(Truthtrance, {game,busy:false,act() {}})); /** Native lobby/start and actions; only original unopened deal and Spice * positions are selected. The question is at a real settled Charity opening. */ function fixture() { let game=createGame('KULLRICHTABLE',newPlayer('choam','CHOAM','choam'),false,['choam','ix']); for (const id of ['richese','emperor','harkonnen','guild'] as const) joinGame(game,newPlayer(id,id,id)); game=act(game,'choam',{type:'moduleOption',module:'kullPreview',enabled:true}); for (const seat of game.players) game=act(game,seat.id,{type:'ready'}); game=act(game,'choam',{type:'start'}); assert.equal(game.status,'setup'); assert.ok(game.kullPreview && game.semutaPreview); const originalCards=inventory(game), selected:Game['deck']=[]; for (const kind of ['kull','filler','karama','karama','filler','truth'] as const) { const index=game.deck.findIndex(card=>kind==='kull' ? card.id==='ix-kull-wahad' : kind==='truth' ? card.effect==='truthtrance' : kind==='karama' ? card.effect==='karama' : card.kind==='worthless' && card.id!=='ix-kull-wahad'); assert.ok(index>=0); selected.push(game.deck.splice(index,1)[0]); } game.deck.unshift(...selected); game=advance(game,state=>state.status==='playing'); const truthCard=selected[5].id; assert.ok(player(game,'guild').hand.some(card=>card.id===truthCard)); const spice=game.spiceDeck.findIndex(card=>'territory' in card && !card.discovery); assert.ok(spice>=0); game.spiceDeck.unshift(game.spiceDeck.splice(spice,1)[0]); game=advance(game,state=>state.phase===2 && clean(state) && state.choamCharity?.turn===state.turn); game=act(game,'guild',{type:'card',card:truthCard}); while (game.truthtrance?.stage==='priority') { const next=game.players.find(row=>!game.truthtrance!.passed.includes(row.id)); assert.ok(next); game=act(game,next.id,{type:'truthPass'}); } return {game,originalCards}; } void test('Basic Kull and Richese render the real bribe question, answer and subsequent payment guide',()=>{ const f=fixture(); let game=f.game; const asker=viewGame(game,'guild'); assert.ok(asker.truthBribeTargets.includes('emperor')); assert.match(truth(asker), /