109 lines
6.5 KiB
TypeScript
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');
|
|
});
|