import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names'; import { knowledgeFactAnswer, truthKnowledgeOf } from '../game/truthtrance-knowledge'; import { freshPrivateLotGame, passPrivateLotWindow, privateLotPhysicalIds, privateLotPlayer, recyclePrivatePurchaseIntoOriginalNormalLot, sellPrivateBlackMarketLot } from './fixture-homeworld-occupied-private-lots'; /** Stage the questioner's card by exchanging existing physical cards. The * inspected/seller card remains untouched and no hand size is changed. */ function stageTruth(game: Game, protectedId: string) { const original = privateLotPhysicalIds(game), asker = privateLotPlayer(game, 'ally'); const held = asker.hand.find(card => card.effect === 'truthtrance'); if (held) return held.id; const stocks = [game.deck, game.discard, ...game.players.filter(p => p.id !== asker.id).map(p => p.hand)]; const source = stocks.find(cards => cards.some(card => card.effect === 'truthtrance' && card.id !== protectedId)); assert.ok(source, 'A distinct original Truthtrance must be available'); const index = source.findIndex(card => card.effect === 'truthtrance' && card.id !== protectedId); const truth = source[index]; if (asker.hand.length) [asker.hand[0], source[index]] = [source[index], asker.hand[0]]; else asker.hand.push(...source.splice(index, 1)); assert.deepEqual(privateLotPhysicalIds(game), original); return truth.id; } function ask(game: Game, card: string, name: string) { let next = applyAction(game, 'ally', {type:'card',card}); while (next.truthtrance?.stage === 'priority') { const p = next.players.find(player => !next.truthtrance!.passed.includes(player.id)); assert.ok(p); next = applyAction(next, p.id, {type:'truthPass'}); } return applyAction(next, 'ally', {type:'truthAsk', question:{kind:'fact',target:'occupier',fact:{kind:'auctionPeek',name}}}); } function verify(state: Game, truth: string, name: string, expected: 'yes' | 'no', lot: 'ordinary' | 'blackMarket') { const before = privateLotPhysicalIds(state), game = ask(structuredClone(state), truth, name); assert.equal(viewGame(game, 'occupier').truthAnswer, expected); assert.equal(viewGame(game, 'ally').truthAnswer, null); assert.equal(viewGame(game, 'rival').truthAnswer, null); if (lot === 'ordinary') { assert.ok(viewGame(game, 'occupier').auction?.card); assert.equal(viewGame(game, 'ally').auction?.card, null); } else { assert.ok(viewGame(game, 'occupier').richeseAuction?.card); assert.equal(viewGame(game, 'ally').richeseAuction?.card, null); } for (const difficulty of DIFFICULTIES) { const view = viewGame(game, 'occupier'); view.players.find(player => player.id === 'occupier')!.bot = difficulty; assert.deepEqual(botActions(view), [{type:'truthAnswer',answer:expected}]); const done = applyAction(game, 'occupier', {type:'truthAnswer',answer:expected}); assert.equal(done.truthHistory?.at(-1)?.answer, expected); assert.deepEqual(privateLotPhysicalIds(done), before); } } for (const denied of [false, true]) { void test(`Caladan occupier answers actual Advanced Black Market inspection with native Atreides ${denied ? 'canceled' : 'allowed'}`, () => { const fixture = freshPrivateLotGame(); let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket'); game = denied ? applyAction(game, 'rival', {type:'card',mode:'cancel',card:fixture.karama.id}) : passPrivateLotWindow(game, 'atreidesAuction'); assert.equal(game.homeworldAuctionInspection?.occupiedKnown, true); assert.equal(game.richesePeekKnown, !denied); const truth = stageTruth(game, fixture.card.id); const wrong = TRUTH_CARD_NAMES.find(name => name !== fixture.card.name); assert.ok(wrong); verify(game, truth, fixture.card.name, 'yes', 'blackMarket'); verify(game, truth, wrong, 'no', 'blackMarket'); }); } void test('Caladan occupier answers after the original ordinary lot inspection', () => { const fixture = freshPrivateLotGame(); let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket'); game = passPrivateLotWindow(game, 'atreidesAuction'); game = sellPrivateBlackMarketLot(game); game = recyclePrivatePurchaseIntoOriginalNormalLot(game, fixture.card.id); game = passPrivateLotWindow(game, 'atreidesAuction'); const truth = stageTruth(game, fixture.card.id); const wrong = TRUTH_CARD_NAMES.find(name => name !== fixture.card.name); assert.ok(wrong); verify(game, truth, fixture.card.name, 'yes', 'ordinary'); verify(game, truth, wrong, 'no', 'ordinary'); }); void test('absent or stale occupied receipt does not expose an auction observation', () => { const fixture = freshPrivateLotGame(); let game = passPrivateLotWindow(fixture.game, 'richeseBlackMarket'); game = passPrivateLotWindow(game, 'atreidesAuction'); const receipt = structuredClone(game.homeworldAuctionInspection!); game = sellPrivateBlackMarketLot(game); game = recyclePrivatePurchaseIntoOriginalNormalLot(game, fixture.card.id); const fact = {kind:'auctionPeek',name:fixture.card.name} as const; const stale = structuredClone(game); stale.homeworldAuctionInspection = receipt; assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(stale, privateLotPlayer(stale, 'occupier'))), 'unknown'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, privateLotPlayer(game, 'occupier'))), 'unknown'); });