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 { applyAction, viewGame, type Game, type GameView } from '../game/engine'; import { EmperorAuctionGift } from '../components/emperor-auction-gift'; import { advanceTruthAlliance, createTruthAllianceFixture, truthAllianceClean } from './fixture-truth-alliance'; 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'); aliases.deregister(); /** Original public Advanced setup, natural worm and actual alliance, followed * by ordinary bidding actions. Wallet amounts are deliberately staged. */ function fixture(emperorSpice: number): Game { const source = createTruthAllianceFixture({advanced:true, consent:'none'}); let game = source.game; game = applyAction(game, 'harkonnen', {type:'alliance', target:'emperor'}); game = applyAction(game, 'emperor', {type:'alliance', target:'harkonnen'}); game = advanceTruthAlliance(game, state => state.phase === 3 && !!state.auction && truthAllianceClean(state)); for (let count = 0; game.auction && game.auction.active !== 'harkonnen' && count < game.players.length; count++) game = applyAction(game, game.auction.active, {type:'passBid'}); assert.equal(game.auction?.active, 'harkonnen'); game.players.find(player => player.id === 'harkonnen')!.spice = 6; game.players.find(player => player.id === 'emperor')!.spice = emperorSpice; game = applyAction(game, 'harkonnen', {type:'bid', amount:4, allyPayment:0}); for (let count = 0; game.auction && !game.response && !game.decision && count < 12; count++) game = applyAction(game, game.auction.active, {type:'passBid'}); assert.equal(game.decision?.kind, 'auctionPayment'); assert.deepEqual(viewGame(game, 'emperor').emperorAuctionGiftPending, {owner:'emperor'}); return game; } const component = (game: GameView, busy = false) => renderToStaticMarkup(createElement(EmperorAuctionGift, {game, busy, act() {}})); const table = (game: GameView, busy = false) => renderToStaticMarkup(createElement(GameTable, {game, busy, send:async () => {}, onExit() {}})); const enabled = (label: string) => new RegExp(`]*disabled)[^>]*>${label}<\\/button>`); const disabled = (label: string) => new RegExp(`]*disabled=""[^>]*>${label}<\\/button>`); void test('the owner sees the quoted recipient, amount and continuation on the actual table', () => { const game = fixture(10), own = viewGame(game, 'emperor'); assert.equal(own.emperorAuctionGift?.recipient, 'harkonnen'); assert.equal(own.emperorAuctionGift?.maximum, 10); const html = component(own); assert.match(html, /Share spice with Harkonnen/); assert.match(html, /committed ally payment split remain unchanged/); assert.match(html, /type="number"[^>]*min="1"[^>]*max="10"/); assert.match(html, enabled('Share spice')); assert.match(html, enabled('Continue to payment')); assert.match(table(own), /Share spice with Harkonnen/); assert.match(table(own), enabled('Continue to payment')); }); void test('zero available spice still permits the owner to continue, but not to give', () => { const own = viewGame(fixture(0), 'emperor'); assert.equal(own.emperorAuctionGift?.maximum, 0); const html = component(own); assert.match(html, disabled('Share spice')); assert.match(html, enabled('Continue to payment')); assert.match(table(own), enabled('Continue to payment')); }); void test('nonowners see the public wait without an owner quote or private amount', () => { const game = fixture(10); for (const id of ['harkonnen', 'atreides']) { const view = viewGame(game, id); assert.equal(view.emperorAuctionGift, null); assert.deepEqual(view.emperorAuctionGiftPending, {owner:'emperor'}); assert.equal(component(view), ''); const html = table(view); assert.match(html, /Waiting for the Emperor to finish the allied gift opportunity/); assert.doesNotMatch(html, /Emperor auction gift/); assert.doesNotMatch(html, /Continue to payment/); assert.doesNotMatch(html, /Spice to share/); } }); void test('busy controls disable both owner actions while preserving the waiting state', () => { const own = viewGame(fixture(10), 'emperor'); const html = component(own, true); assert.match(html, disabled('Share spice')); assert.match(html, disabled('Continue to payment')); assert.match(table(own, true), disabled('Continue to payment')); });