Enable Advanced Ixian auction knowledge questions
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 04:26:29 +11:00
1 parent 3a27c29a03
commit 88c0ab1547
10 files changed
+161 -13

No files matched your search

+11
View File
@@ -47,6 +47,17 @@ void test('canonical Richese inspectors describe available previews and retain c
assert.doesNotMatch(cardPresentation(definition.card).availability ?? '', /expansion starts remain disabled/i);
});
void test('Semuta inspector explains the supported Basic and Advanced previews', () => {
const semuta = RICHESE_CARD_DEFINITIONS.find(row => row.card.effect === 'semutaDrug');
assert.ok(semuta);
const guide = richeseCardActionBlock(semuta.card) ?? '';
assert.match(guide, /fresh Basic Richese games/);
assert.match(guide, /Advanced games with Richese, the CHOAM deck and classic or CHOAM opponents/);
assert.match(guide, /some discard reactions/);
assert.match(guide, /Older games and other module previews keep their existing scope/);
assert.match(guide, /Commit before inspecting private discarded faces/);
});
void test('missing, altered and merely similar Richese identities receive no canonical full guide', () => {
for (const card of richeseCards()) {
const impostors: Card[] = [
+108
View File
@@ -0,0 +1,108 @@
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');
});
+2 -2
View File
@@ -14,7 +14,7 @@ void test('Ixian pool question accepts only canonical card names and describes t
assert.match(knowledgeFactText({ kind: 'ixPoolPeek', name: 'Shield' }), /recorded Ixian auction group includes Shield/);
});
void test('only native Basic Ixians with the current recorded inspection can answer', () => {
void test('only native Ixians with the current recorded inspection can answer', () => {
const game = createGame('IXKNOW', newPlayer('ix', 'Ixians', 'ixians'), false, ['ix']);
const ix = game.players[0];
const other = newPlayer('other', 'Other', 'atreides');
@@ -28,7 +28,7 @@ void test('only native Basic Ixians with the current recorded inspection can ans
assert.equal(knowledgeFactAnswer(no, truthKnowledgeOf(game, ix)), 'no');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf(game, other)), 'unknown');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, phase: 4 }, ix)), 'unknown');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, advanced: true }, ix)), 'unknown');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, advanced: true }, ix)), 'yes');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, ixAuctionKnown: null }, ix)), 'unknown');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, ixAuctionKnown: { turn: game.turn - 1, cards: [shield] } }, ix)), 'unknown');
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, ixAuctionKnown: { turn: game.turn, cards: [shield], controller: 'other' } }, ix)), 'unknown');
@@ -46,7 +46,7 @@ void test('recorded Ixian pool selector appears in standalone and compound facts
assert.match(single, /Recorded Ixian inspected auction pool includes/);
assert.match(single, /value="Shield" selected/);
assert.match(single, /pool itself remains private/);
assert.match(single, /In Basic Bidding/);
assert.match(single, /During Bidding/);
assert.match(single, /If that knowledge is unavailable/);
const compound: TruthFact = { kind: 'or', terms: [fact, { kind: 'spice', compare: 'gte', value: 3 }] };
const grouped = renderToStaticMarkup(createElement(TruthFactExpressionFields, { value: compound, game: view, target: 'b', disabled: false, onChange() {} }));