import assert from 'node:assert/strict'; import test from 'node:test'; import { type Action, type Game, viewGame } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { knowledgeFactAnswer, truthKnowledgeOf } from '../game/truthtrance-knowledge'; import { createTruthtranceRichesePromisesFixture, advanceTruthtranceRichesePromises as advance, actTruthtranceRichesePromises as act, truthtranceRichesePromisesInventory as inventory } from './fixture-truthtrance-richese-promises'; const player = (g: Game, id: string) => { const seat = g.players.find(candidate => candidate.id === id); assert.ok(seat); return seat; }; /** Genuine Advanced Richese/Atreides/Emperor setup, Black Market offer and * successful inspection. Only the existing Truthtrance is moved between hands * so a different faction can ask the native Atreides seat. */ function inspectedLot(method: 'silent' | 'onceAround' = 'silent') { const fixture = createTruthtranceRichesePromisesFixture({ advanced: true }); let game = structuredClone(fixture.game); const atreides = player(game, 'atreides'), emperor = player(game, 'emperor'); const truth = atreides.hand.find(card => card.effect === 'truthtrance'); assert.ok(truth); atreides.hand = atreides.hand.filter(card => card.id !== truth.id); emperor.hand.push(truth); game = advance(game, state => state.decision?.kind === 'richeseBlackMarket'); const card = player(game, 'richese').hand.find(candidate => candidate.name === 'Shield'); assert.ok(card); game = act(game, 'richese', { type:'decision', event:game.richeseBidding!.event, card:card.id, method, ...(method === 'onceAround' ? { direction:'clockwise' } : {}) } as Action); for (let count = 0; game.response && count < 20; count++) { const respondent = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(respondent); game = act(game, respondent.id, {type:'passResponse'}); } assert.equal(game.response, null); assert.equal(game.richeseAuction?.source, 'blackMarket'); assert.equal(game.richeseAuction?.cardId, card.id); assert.equal(game.richesePeekKnown, true); assert.deepEqual(inventory(game), fixture.originalCards); return {game, card, truth}; } function ask(game: Game, truth: string, name: string): Game { game = act(game, 'emperor', {type:'card', card:truth}); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const respondent = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(respondent); game = act(game, respondent.id, {type:'truthPass'}); } return act(game, 'emperor', {type:'truthAsk', question:{kind:'fact', target:'atreides', fact:{kind:'auctionPeek',name}}}); } for (const method of ['silent', 'onceAround'] as const) void test(`native Advanced ${method} Black Market inspection gives Atreides a private Yes or No and all bots answer legally`, () => { const {game, card, truth} = inspectedLot(method); assert.equal(game.richeseAuction?.method, method); for (const [name,answer] of [[card.name,'yes'],['Snooper','no']] as const) { const question = ask(structuredClone(game), truth.id, name); assert.equal(viewGame(question, 'atreides').truthAnswer, answer); assert.equal(viewGame(question, 'emperor').truthAnswer, null); assert.equal(viewGame(question, 'emperor').richeseAuction?.card, null); assert.equal(viewGame(question, 'atreides').richeseAuction?.card?.id, card.id); for (const difficulty of DIFFICULTIES) { const view = viewGame(question, 'atreides'); const seat = view.players.find(candidate => candidate.id === 'atreides'); assert.ok(seat); seat.bot = difficulty; const actions = botActions(view); assert.deepEqual(actions, [{type:'truthAnswer',answer}]); const done = act(question, 'atreides', actions[0]); assert.equal(done.truthHistory?.at(-1)?.answer, answer); assert.equal(done.richeseAuction?.cardId, card.id); assert.deepEqual(inventory(done), inventory(game)); } } }); void test('an uninspected, expired, missing or unauthorized Black Market observation is unknown', () => { const {game, card} = inspectedLot(); const fact = {kind:'auctionPeek', name:card.name} as const; const answer = (state: Game, id = 'atreides') => knowledgeFactAnswer(fact, truthKnowledgeOf(state, player(state,id))); assert.equal(answer(game), 'yes'); assert.equal(answer(game, 'emperor'), 'unknown'); const uninspected = structuredClone(game); uninspected.richesePeekKnown = false; assert.equal(answer(uninspected), 'unknown'); const ended = structuredClone(game); ended.richeseAuction!.outcome = {kind:'unbid'}; assert.equal(answer(ended), 'unknown'); const absent = structuredClone(game); absent.richeseAuction = null; assert.equal(answer(absent), 'unknown'); const missing = structuredClone(game); missing.richeseOfferedCard = null; player(missing, 'richese').hand = player(missing, 'richese').hand.filter(item => item.id !== card.id); assert.equal(answer(missing), 'unknown'); const saved = structuredClone(game); saved.richeseOfferedCard = null; assert.equal(answer(saved), 'yes', 'Older lot frames can still use the reserved physical seller card.'); const later = structuredClone(game); later.phase = 4; assert.equal(answer(later), 'unknown'); });