import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { knowledgeFactAnswer, truthKnowledgeOf } from '../game/truthtrance-knowledge'; import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names'; import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions'; import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share'; import { truthMovementInventory } from './fixture-truth-movement'; import { askKnowledge, rejectKnowledge } from './truthtrance-knowledge-fixture'; function advance(state: Game, done: (game: Game) => boolean): Game { let game = state; for (let i = 0; i < 500 && !done(game); i++) { const next = nextStrongholdFactionsNativeStep(game); assert.ok(next, 'Native setup or Bidding must have a legal next action'); game = applyAction(game, next.actor, next.action); } assert.ok(done(game), 'Native Bidding must reach the requested boundary'); return game; } /** Start the actual public Advanced expansion preview and retain genuine Ixian * auction inspection. Only the question card and first Spice Blow are staged, * by moving original physical cards; no auction or knowledge record is forged. */ function inspected(exchange: boolean) { let game = createGame('ADVIXTRUTH', newPlayer('atreides', 'Atreides', 'atreides'), true, ['ix']); for (const faction of ['ixians', 'emperor', 'fremen'] as const) joinGame(game, newPlayer(faction, faction, faction)); for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' }); game = applyAction(game, 'atreides', { type: 'start', advancedPreview: true }); assert.equal(game.status, 'setup'); game = advance(game, state => state.status === 'playing'); const cards = truthMovementInventory(game); const truth = [...game.deck, ...game.discard, ...game.players.filter(player => player.id !== 'ixians').flatMap(player => player.hand)] .find(card => card.effect === 'truthtrance'); assert.ok(truth, 'An original Truthtrance remains outside the Ixian starting hand'); moveSemutaEmperorCard(game, truth.id, 'atreides'); assert.deepEqual(truthMovementInventory(game), cards); const index = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(index >= 0); const [blow] = game.spiceDeck.splice(index, 1); game.spiceDeck.unshift(blow); game = advance(game, state => state.decision?.kind === 'ixAuction' && !state.response); const pool = game.ixAuction!.cards; assert.ok(pool.length > 1); const returned = pool[0].id; const retained = pool.slice(1).map(card => card.id).sort(); game = applyAction(game, 'ixians', { type: 'decision', card: returned, position: 'bottom' }); assert.equal(game.decision?.kind, 'ixTechnology'); assert.deepEqual(game.ixAuctionKnown!.cards.map(card => card.id).sort(), retained); const before = [...game.ixAuctionKnown!.cards]; const incoming = game.auction!.cards[game.auction!.index]; const offered = game.players.find(player => player.id === 'ixians')!.hand[0]; assert.ok(incoming && offered); game = applyAction(game, 'ixians', exchange ? { type: 'decision', card: offered.id } : { type: 'decision', decline: true }); if (exchange) { // A response with no eligible counter resolves automatically; otherwise // the native pass queue completes the same original exchange. if (game.response) assert.equal(game.response.kind, 'ixTechnology'); game = advance(game, state => !state.response && !state.decision && !state.phaseOpening && !!state.auction); assert.equal(game.auction!.cards[game.auction!.index].id, offered.id); assert.ok(game.players.find(player => player.id === 'ixians')!.hand.some(card => card.id === incoming.id)); assert.deepEqual(game.ixAuctionKnown!.cards.map(card => card.id).sort(), [...before.filter(card => card.id !== incoming.id && card.id !== offered.id), offered].map(card => card.id).sort()); } else game = advance(game, state => !state.response && !state.decision && !state.phaseOpening && !!state.auction); assert.deepEqual(truthMovementInventory(game), cards); return { game, cards, returned, offered }; } for (const exchange of [false, true]) void test(`Advanced native Ixian group knowledge after Technology ${exchange ? 'exchange' : 'decline'}`, () => { const { game, cards, offered } = inspected(exchange); const group = structuredClone(game.ixAuctionKnown!.cards); const name = exchange ? offered.name : group[0].name; const questioned = askKnowledge(game, 'ixians', { kind: 'ixPoolPeek', name }); assert.equal(viewGame(questioned, 'ixians').truthAnswer, 'yes'); for (const id of ['atreides', 'emperor', 'fremen']) assert.equal(viewGame(questioned, id).truthAnswer, null); rejectKnowledge(questioned, 'ixians', { type: 'truthAnswer', answer: 'no' }); for (const difficulty of DIFFICULTIES) { const view = viewGame(questioned, 'ixians'); view.players.find(player => player.id === 'ixians')!.bot = difficulty; const actions = botActions(view); assert.deepEqual(actions, [{ type: 'truthAnswer', answer: 'yes' }]); const done = applyAction(questioned, 'ixians', actions[0]); assert.equal(done.truthHistory!.at(-1)!.answer, 'yes'); assert.deepEqual(done.ixAuctionKnown!.cards, group); assert.deepEqual(truthMovementInventory(done), cards); } }); void test('Advanced auction-group answers require current native Ixian knowledge', () => { const { game } = inspected(false); const ix = game.players.find(player => player.id === 'ixians')!; const other = game.players.find(player => player.id === 'emperor')!; const name = TRUTH_CARD_NAMES.find(candidate => !game.ixAuctionKnown!.cards.some(card => card.name === candidate)); assert.ok(name); const fact = { kind: 'ixPoolPeek', name } as const; assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, ix)), 'no'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, other)), 'unknown'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, phase: 4 }, ix)), 'unknown'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, ixAuctionKnown: null }, ix)), 'unknown'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, ixAuctionKnown: { ...game.ixAuctionKnown!, turn: game.turn - 1 } }, ix)), 'unknown'); assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, ixAuctionKnown: { ...game.ixAuctionKnown!, controller: other.id } }, ix)), 'unknown'); });