import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Game } from '../game/engine'; import { actChoamCharityFunding, advanceChoamCharityFunding, createChoamCharityFundingFixture, nextChoamCharityFundingStep } from './fixture-choam-charity-funding'; let fixture: ReturnType | undefined; const source = () => fixture ??= createChoamCharityFundingFixture(); const owner = 'choam', observer = 'atreides'; const seat = (g: Game, id: string) => { const player = g.players.find(p => p.id === id); assert.ok(player); return player; }; const step = (g: Game) => { const next = nextChoamCharityFundingStep(g); assert.ok(next); return actChoamCharityFunding(g, next.actor, next.action); }; const rejected = (g: Game, id: string, action: {type:string; [key:string]:unknown}) => { const before = structuredClone(g); assert.throws(() => applyAction(g, id, action)); assert.deepEqual(g, before); }; /** Public Basic CHOAM/Atreides/Guild game, with the fixture's original deal * and genuine setup and Storm actions; stop at the actual first closing market. */ function firstMarket() { const g = advanceChoamCharityFunding(structuredClone(source().afterSetup), state => state.turn === 1 && state.phase === 0 && state.decision?.kind === 'choamMarket'); assert.deepEqual(g.expansions, ['choam']); assert.equal(g.advanced, false); assert.equal(g.choamMarket?.owner, owner); assert.equal(g.choamMarket.resume, 'storm'); return g; } function optIn(g: Game) { return actChoamCharityFunding(g, owner, {type:'decision', done:true, skipMarketsThisTurn:true}); } void test('a human CHOAM can waive remaining markets this turn and the native Storm continuation proceeds', () => { const before = firstMarket(); assert.equal(viewGame(before, owner).choamMarketSkipAvailable, true); assert.equal(viewGame(before, owner).choamMarketSkipActive, false); assert.equal(viewGame(before, observer).choamMarketSkipAvailable, false); assert.equal(viewGame(before, observer).choamMarketSkipActive, false); const game = optIn(before); assert.equal(game.turn, 1); assert.equal(game.phase, 1); assert.equal(game.choamMarket, null); assert.equal(viewGame(game, owner).choamMarketSkipActive, true); assert.equal(viewGame(game, observer).choamMarketSkipActive, false); let next = JSON.parse(JSON.stringify(game)) as Game; for (let count = 0; next.turn === 1 && next.phase < 7 && count < 800; count++) { assert.notEqual(next.decision?.kind, 'choamMarket', `Unexpected market in phase ${next.phase}`); next = step(next); } assert.equal(next.turn, 1); assert.equal(next.phase, 7); assert.equal(viewGame(next, owner).choamMarketSkipActive, true); assert.notEqual(next.decision?.kind, 'choamMarket'); }); void test('waiver continuation does not inspect or depend on CHOAM’s hidden card faces', () => { const original = optIn(firstMarket()); const changed = structuredClone(original); const choam = seat(changed, owner), guild = seat(changed, 'guild'); assert.ok(choam.hand.length && guild.hand.length); // Swap two actual held cards, preserving all public counts and custody. [choam.hand[0], guild.hand[0]] = [guild.hand[0], choam.hand[0]]; assert.deepEqual(viewGame(original, observer), viewGame(changed, observer)); for (let game of [original, changed]) { for (let count = 0; game.phase === 1 && count < 100; count++) { assert.notEqual(game.decision?.kind, 'choamMarket'); game = step(game); } assert.equal(game.phase, 2); assert.notEqual(game.decision?.kind, 'choamMarket'); } }); void test('only CHOAM can opt in or revoke, and revocation restores the next real market', () => { const market = firstMarket(); rejected(market, observer, {type:'decision', done:true, skipMarketsThisTurn:true}); rejected(market, owner, {type:'cancelChoamMarketSkip'}); let game = optIn(market); rejected(game, observer, {type:'cancelChoamMarketSkip'}); game = actChoamCharityFunding(game, owner, {type:'cancelChoamMarketSkip'}); assert.equal(viewGame(game, owner).choamMarketSkipActive, false); rejected(game, owner, {type:'cancelChoamMarketSkip'}); game = advanceChoamCharityFunding(game, state => state.phase === 1 && state.decision?.kind === 'choamMarket'); assert.equal(game.choamMarket?.owner, owner); assert.equal(viewGame(game, owner).choamMarketSkipAvailable, true); game = actChoamCharityFunding(game, owner, {type:'decision', done:true}); assert.equal(viewGame(game, owner).choamMarketSkipActive, false); }); for (const mode of ['bot','autopilot'] as const) void test( `the standing waiver does not skip a market when CHOAM is under ${mode} control`, () => { let game = optIn(firstMarket()); // A labelled staged controller handover; all game actions before it were native. seat(game, owner)[mode] = 'Easy'; game = advanceChoamCharityFunding(game, state => state.phase === 1 && state.decision?.kind === 'choamMarket'); assert.equal(game.choamMarket?.owner, owner); }, ); void test('the waiver expires at the next turn and preserves CHOAM’s final Mentat decision', () => { let game = optIn(firstMarket()); game = advanceChoamCharityFunding(game, state => state.turn === 1 && state.decision?.kind === 'choamMentat'); assert.equal(game.phase, 8); assert.equal(game.decision?.player, owner); game = advanceChoamCharityFunding(game, state => state.turn === 2 && state.phase === 0 && state.decision?.kind === 'choamMarket'); assert.equal(viewGame(game, owner).choamMarketSkipActive, false); assert.equal(viewGame(game, owner).choamMarketSkipAvailable, true); });