Files
Dune/tests/truthtrance-ix-pool-advanced.test.ts
T
Quintine 88c0ab1547
Verify and publish checkpoint container / container (push) Canceled after 0s
Enable Advanced Ixian auction knowledge questions
2026-10-29 04:26:29 +11:00

109 lines
6.5 KiB
TypeScript

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');
});