Support Truthtrance on Basic Ixian auction knowledge
This commit is contained in:
1 parent
910e1f9b40
commit
c3f37a574d
11 files changed
+248
-13
No files matched your search
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { createGame, newPlayer } from '../game/engine';
|
||||
import { truthFactAnswer } from '../game/truthtrance';
|
||||
import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names';
|
||||
import { knowledgeFactAnswer, knowledgeFactText, parseKnowledgeFact, truthKnowledgeOf } from '../game/truthtrance-knowledge';
|
||||
|
||||
void test('Ixian pool question accepts only canonical card names and describes the recorded group', () => {
|
||||
for (const name of TRUTH_CARD_NAMES)
|
||||
assert.deepEqual(parseKnowledgeFact({ kind: 'ixPoolPeek', name, extra: true }), { kind: 'ixPoolPeek', name });
|
||||
for (const name of ['', 'a hidden card', null, 7, {}, ['Shield']])
|
||||
assert.throws(() => parseKnowledgeFact({ kind: 'ixPoolPeek', name }));
|
||||
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', () => {
|
||||
const game = createGame('IXKNOW', newPlayer('ix', 'Ixians', 'ixians'), false, ['ix']);
|
||||
const ix = game.players[0];
|
||||
const other = newPlayer('other', 'Other', 'atreides');
|
||||
const shield = baseDeck().find(card => card.name === 'Shield')!;
|
||||
const absent = TRUTH_CARD_NAMES.find(name => name !== shield.name)!;
|
||||
const yes = { kind: 'ixPoolPeek', name: shield.name } as const;
|
||||
const no = { kind: 'ixPoolPeek', name: absent } as const;
|
||||
game.phase = 3;
|
||||
game.ixAuctionKnown = { turn: game.turn, cards: [shield] };
|
||||
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf(game, ix)), 'yes');
|
||||
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, 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');
|
||||
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, ixAuctionKnown: { turn: game.turn, cards: [shield], controller: ix.id } }, ix)), 'yes');
|
||||
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, homeworldOccupationPreview: true }, ix)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, homeworldOccupationPreview: true, ixAuctionKnown: { turn: game.turn, cards: [shield], controller: ix.id } }, ix)), 'yes');
|
||||
assert.equal(knowledgeFactAnswer(yes, truthKnowledgeOf({ ...game, homeworldOccupationPreview: true, ixAuctionKnown: { turn: game.turn, cards: [shield], controller: other.id } }, other)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(yes), 'unknown');
|
||||
});
|
||||
|
||||
void test('non-entitled targets do not inspect the private recorded group and compounds return only the answer', () => {
|
||||
const game = createGame('IXKNOW', newPlayer('ix', 'Ixians', 'ixians'), false, ['ix']);
|
||||
const ix = game.players[0];
|
||||
const other = newPlayer('other', 'Other', 'atreides');
|
||||
const shield = baseDeck().find(card => card.name === 'Shield')!;
|
||||
const hidden = { ...game, phase: 3, get ixAuctionKnown(): never { throw new Error('Unentitled pool access'); } };
|
||||
assert.equal(truthKnowledgeOf(hidden, other).ixPoolPeek, null);
|
||||
hidden.phase = 4;
|
||||
assert.equal(truthKnowledgeOf(hidden, ix).ixPoolPeek, null);
|
||||
game.phase = 3;
|
||||
game.ixAuctionKnown = { turn: game.turn, cards: [shield] };
|
||||
const fact = { kind: 'ixPoolPeek', name: shield.name } as const;
|
||||
const knowledge = truthKnowledgeOf(game, ix);
|
||||
assert.equal(truthFactAnswer(ix, { kind: 'and', terms: [fact, { kind: 'spice', compare: 'gte', value: 0 }] }, knowledge), 'yes');
|
||||
assert.equal(truthFactAnswer(ix, { kind: 'or', terms: [fact, { kind: 'spice', compare: 'gte', value: 999 }] }, knowledge), 'yes');
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names';
|
||||
import { askKnowledge, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
import {
|
||||
advanceTruthMovementNativeEarly, createTruthMovementNativeEarlyFixture,
|
||||
truthMovementNativeEarlyInventory,
|
||||
} from './fixture-truth-movement-native-early';
|
||||
|
||||
/** Original Basic start and native Ixian inspection. The shared fixture selects
|
||||
* unopened starting positions so Atreides receives a real Truthtrance card. */
|
||||
function inspectedPool(): { game: Game; returned: string } {
|
||||
const fixture = createTruthMovementNativeEarlyFixture({ roster: 'ixians-tleilaxu' });
|
||||
let game = advanceTruthMovementNativeEarly(fixture.game, state =>
|
||||
state.phase === 3 && state.decision?.kind === 'ixAuction' && !state.response);
|
||||
const initial = game.ixAuction!.cards;
|
||||
assert.ok(initial.length > 1);
|
||||
const returned = initial[0].id;
|
||||
const retained = initial.slice(1).map(card => card.id).sort();
|
||||
game = applyAction(game, 'ixians', { type: 'decision', card: returned, position: 'bottom' });
|
||||
game = advanceTruthMovementNativeEarly(game, state =>
|
||||
state.phase === 3 && !!state.auction && !!state.ixAuctionKnown &&
|
||||
!state.response && !state.decision && !state.phaseOpening);
|
||||
assert.deepEqual(game.ixAuctionKnown!.cards.map(card => card.id).sort(), retained);
|
||||
assert.ok(!game.ixAuctionKnown!.cards.some(card => card.id === returned));
|
||||
assert.deepEqual(truthMovementNativeEarlyInventory(game), fixture.originalCards);
|
||||
return { game, returned };
|
||||
}
|
||||
|
||||
void test('native Basic Ixian pool knowledge answers privately and preserves the original cards', () => {
|
||||
const { game: initial, returned } = inspectedPool();
|
||||
const pool = structuredClone(initial.ixAuctionKnown!.cards);
|
||||
const lot = structuredClone(initial.auction);
|
||||
const owned = viewGame(initial, 'ixians').ixTechnology;
|
||||
assert.deepEqual(owned?.known.map(card => card.id).sort(), pool.map(card => card.id).sort());
|
||||
assert.equal(viewGame(initial, 'emperor').ixTechnology, null);
|
||||
const name = pool[0].name;
|
||||
const questioned = askKnowledge(initial, 'ixians', { kind: 'ixPoolPeek', name });
|
||||
assert.equal(viewGame(questioned, 'ixians').truthAnswer, 'yes');
|
||||
for (const id of ['atreides', 'emperor', 'tleilaxu'])
|
||||
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.truthtrance, null);
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, 'yes');
|
||||
assert.deepEqual(done.ixAuctionKnown!.cards, pool);
|
||||
assert.deepEqual(done.auction, lot);
|
||||
assert.ok(done.deck.some(card => card.id === returned));
|
||||
assert.deepEqual(truthMovementNativeEarlyInventory(done), truthMovementNativeEarlyInventory(initial));
|
||||
}
|
||||
});
|
||||
|
||||
void test('the recorded group answers no for an absent face and unknown when no matching knowledge exists', () => {
|
||||
const { game: initial } = inspectedPool();
|
||||
const absent = TRUTH_CARD_NAMES.find(name => !initial.ixAuctionKnown!.cards.some(card => card.name === name));
|
||||
assert.ok(absent);
|
||||
const notInPool = askKnowledge(initial, 'ixians', { kind: 'ixPoolPeek', name: absent });
|
||||
assert.equal(viewGame(notInPool, 'ixians').truthAnswer, 'no');
|
||||
const done = applyAction(notInPool, 'ixians', { type: 'truthAnswer', answer: 'no' });
|
||||
assert.deepEqual(truthMovementNativeEarlyInventory(done), truthMovementNativeEarlyInventory(initial));
|
||||
|
||||
// These explicit saved-state fixtures remove or age the actual learned
|
||||
// observation; no simulated card is inserted into the physical auction.
|
||||
for (const missing of [null, { ...initial.ixAuctionKnown!, turn: initial.turn - 1 }]) {
|
||||
const staged = structuredClone(initial);
|
||||
staged.ixAuctionKnown = missing;
|
||||
const unknown = askKnowledge(staged, 'ixians', { kind: 'ixPoolPeek', name: absent });
|
||||
assert.equal(viewGame(unknown, 'ixians').truthAnswer, 'unknown');
|
||||
rejectKnowledge(unknown, 'ixians', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.equal(applyAction(unknown, 'ixians', { type: 'truthAnswer', answer: 'unknown' }).truthtrance?.stage, 'unknown');
|
||||
}
|
||||
const wrong = askKnowledge(initial, 'emperor', { kind: 'ixPoolPeek', name: initial.ixAuctionKnown!.cards[0].name });
|
||||
assert.equal(viewGame(wrong, 'emperor').truthAnswer, 'unknown');
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { knowledgeFactInputError } from '../game/truthtrance-knowledge';
|
||||
import { createGame, joinGame, newPlayer, viewGame } from '../game/engine';
|
||||
import type { TruthFact } from '../game/truthtrance';
|
||||
|
||||
function markup(value: Extract<TruthFact, { kind: 'prediction' | 'stormDial' | 'stormForecast' | 'spicePeek' | 'auctionPeek' }>) {
|
||||
function markup(value: Extract<TruthFact, { kind: 'prediction' | 'stormDial' | 'stormForecast' | 'spicePeek' | 'auctionPeek' | 'ixPoolPeek' }>) {
|
||||
return renderToStaticMarkup(createElement(KnowledgeFactFields, { value, onChange() {} }));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,24 @@ void test('knowledge controls expose full public faction registry and bounded st
|
||||
assert.match(forecast, /If no such value is available/);
|
||||
});
|
||||
|
||||
void test('recorded Ixian pool selector appears in standalone and compound facts', () => {
|
||||
const game = createGame('IXPOOLCONTROLS', newPlayer('a', 'Asker', 'emperor'), false, ['ix']);
|
||||
joinGame(game, newPlayer('b', 'Target', 'ixians'));
|
||||
const view = viewGame(game, 'a');
|
||||
const fact: TruthFact = { kind: 'ixPoolPeek', name: 'Shield' };
|
||||
const single = renderToStaticMarkup(createElement(TruthFactFields, { value: fact, game: view, target: 'b', onChange() {} }));
|
||||
assert.match(single, /value="ixPoolPeek" selected/);
|
||||
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, /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() {} }));
|
||||
assert.match(grouped, /Recorded Ixian inspected auction pool includes/);
|
||||
assert.match(grouped, /value="Shield" selected/);
|
||||
});
|
||||
|
||||
void test('auction peek selector is available in single and compound current-fact questions', () => {
|
||||
const game = createGame('AUCTIONCONTROLS', newPlayer('a', 'Asker', 'emperor'));
|
||||
joinGame(game, newPlayer('b', 'Target', 'atreides'));
|
||||
|
||||
Reference in new issue
Block a user