Support Truthtrance on Atreides auction inspection
This commit is contained in:
1 parent
235c2eb03f
commit
aa6f77372f
11 files changed
+228
-11
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { createGame, newPlayer, type Auction } 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('auction knowledge accepts canonical Treachery Card names and normalizes inputs', () => {
|
||||
for (const name of TRUTH_CARD_NAMES)
|
||||
assert.deepEqual(parseKnowledgeFact({ kind: 'auctionPeek', name, extra: true }), { kind: 'auctionPeek', name });
|
||||
for (const name of ['', 'a hidden card', null, 7, {}, ['Shield']])
|
||||
assert.throws(() => parseKnowledgeFact({ kind: 'auctionPeek', name }));
|
||||
assert.match(knowledgeFactText({ kind: 'auctionPeek', name: 'Shield' }), /auction card.*Shield/);
|
||||
});
|
||||
|
||||
void test('only Atreides with an active successful ordinary-lot inspection can answer', () => {
|
||||
const game = createGame('LOTTEST', newPlayer('a', 'Atreides', 'atreides'));
|
||||
const owner = game.players[0];
|
||||
const other = newPlayer('b', 'Other', 'guild');
|
||||
const cards = baseDeck();
|
||||
const shield = cards.find(card => card.name === 'Shield')!;
|
||||
const otherCard = cards.find(card => card.name !== 'Shield')!;
|
||||
const auction: Auction = { cards: [shield, otherCard], index: 0, bid: 0, bidder: null,
|
||||
active: 'a', passed: [], opener: 0, peekKnown: true };
|
||||
game.phase = 3;
|
||||
game.auction = auction;
|
||||
const fact = { kind: 'auctionPeek', name: 'Shield' } as const;
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, owner)), 'yes');
|
||||
assert.equal(knowledgeFactAnswer({ kind: 'auctionPeek', name: otherCard.name }, truthKnowledgeOf(game, owner)), 'no');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(game, other)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, phase: 4 }, owner)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, auction: null }, owner)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, auction: { ...auction, peekKnown: false } }, owner)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...game, auction: { ...auction, index: 8 } }, owner)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact), 'unknown');
|
||||
const next = { ...game, auction: { ...auction, index: 1 } };
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(next, owner)), 'no');
|
||||
assert.equal(knowledgeFactAnswer({ kind: 'auctionPeek', name: otherCard.name }, truthKnowledgeOf(next, owner)), 'yes');
|
||||
});
|
||||
|
||||
void test('non-entitled callers cannot read private lot cards; compound questions expose only the aggregate', () => {
|
||||
const game = createGame('LOTTEST', newPlayer('a', 'Atreides', 'atreides'));
|
||||
const owner = game.players[0];
|
||||
const other = newPlayer('b', 'Other', 'guild');
|
||||
game.phase = 3;
|
||||
const shield = baseDeck().find(card => card.name === 'Shield')!;
|
||||
const lot: Auction = { cards: [shield], index: 0, bid: 0, bidder: null,
|
||||
active: 'a', passed: [], opener: 0, peekKnown: true };
|
||||
game.auction = lot;
|
||||
const hidden = { ...game, get auction(): never { throw new Error('Unentitled auction access'); } };
|
||||
assert.equal(truthKnowledgeOf(hidden, other).auctionPeek, null);
|
||||
hidden.phase = 4;
|
||||
assert.equal(truthKnowledgeOf(hidden, owner).auctionPeek, null);
|
||||
const uninspected = { ...lot, peekKnown: false, get cards(): never { throw new Error('Uninspected card access'); } };
|
||||
assert.equal(truthKnowledgeOf({ ...game, auction: uninspected }, owner).auctionPeek, null);
|
||||
const knowledge = truthKnowledgeOf(game, owner);
|
||||
const fact = { kind: 'auctionPeek', name: 'Shield' } as const;
|
||||
assert.equal(truthFactAnswer(owner, { kind: 'and', terms: [fact, { kind: 'spice', compare: 'gte', value: 0 }] }, knowledge), 'yes');
|
||||
assert.equal(truthFactAnswer(owner, { kind: 'or', terms: [fact, { kind: 'spice', compare: 'gte', value: 999 }] }, knowledge), 'yes');
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { knowledgeGame, knowledgeBotActions, askKnowledge, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
|
||||
/** Real setup and ordinary auction progression. The fixture stages only the
|
||||
* holder's acquisition of the two physical Truthtrance cards. */
|
||||
function withInspection(advanced = false) {
|
||||
let g = knowledgeGame(advanced);
|
||||
for (let n = 0; n < 200 && !(g.phase === 3 && g.auction?.peekKnown && !g.response && !g.phaseOpening); n++) {
|
||||
const candidate = g.players.flatMap(player => knowledgeBotActions(g, player.id)
|
||||
.map(action => ({ id: player.id, action })))[0];
|
||||
assert.ok(candidate, `Native auction progression needs a legal decision in phase ${g.phase}`);
|
||||
g = applyAction(g, candidate.id, candidate.action);
|
||||
}
|
||||
assert.equal(g.phase, 3);
|
||||
assert.equal(g.auction?.peekKnown, true);
|
||||
assert.ok(g.auction?.cards[g.auction.index]);
|
||||
return g;
|
||||
}
|
||||
|
||||
void test('actual ordinary auction inspection supplies a private Truthtrance answer for all AI profiles', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const initial = withInspection(advanced);
|
||||
const card = initial.auction!.cards[initial.auction!.index];
|
||||
const g = askKnowledge(initial, 'o', { kind: 'auctionPeek', name: card.name });
|
||||
assert.equal(viewGame(g, 'o').truthAnswer, 'yes');
|
||||
assert.equal(viewGame(g, 'o').auction?.card?.id, card.id);
|
||||
for (const id of ['a', 'b', 'f']) {
|
||||
assert.equal(viewGame(g, id).truthAnswer, null);
|
||||
assert.equal(viewGame(g, id).auction?.card, null);
|
||||
}
|
||||
rejectKnowledge(g, 'o', { type: 'truthAnswer', answer: 'no' });
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(g, 'o');
|
||||
view.players.find(player => player.id === 'o')!.bot = difficulty;
|
||||
const actions = botActions(view);
|
||||
assert.deepEqual(actions, [{ type: 'truthAnswer', answer: 'yes' }]);
|
||||
const done = applyAction(g, 'o', actions[0]);
|
||||
assert.equal(done.truthtrance, null);
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, 'yes');
|
||||
assert.equal(done.auction!.cards[done.auction!.index].id, card.id);
|
||||
assert.deepEqual(knowledgePhysical(done), knowledgePhysical(initial));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('an unavailable auction inspection yields unknown and retains the physical card', () => {
|
||||
const initial = withInspection();
|
||||
initial.auction!.peekKnown = false;
|
||||
const name = initial.auction!.cards[initial.auction!.index].name;
|
||||
const g = askKnowledge(initial, 'o', { kind: 'auctionPeek', name });
|
||||
assert.equal(viewGame(g, 'o').truthAnswer, 'unknown');
|
||||
rejectKnowledge(g, 'o', { type: 'truthAnswer', answer: 'yes' });
|
||||
const done = applyAction(g, 'o', { type: 'truthAnswer', answer: 'unknown' });
|
||||
assert.equal(done.truthtrance?.stage, 'unknown');
|
||||
assert.deepEqual(knowledgePhysical(done), knowledgePhysical(initial));
|
||||
});
|
||||
@@ -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' }>) {
|
||||
function markup(value: Extract<TruthFact, { kind: 'prediction' | 'stormDial' | 'stormForecast' | 'spicePeek' | 'auctionPeek' }>) {
|
||||
return renderToStaticMarkup(createElement(KnowledgeFactFields, { value, onChange() {} }));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,22 @@ void test('knowledge controls expose full public faction registry and bounded st
|
||||
assert.match(forecast, /If no such value is available/);
|
||||
});
|
||||
|
||||
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'));
|
||||
const view = viewGame(game, 'a');
|
||||
const fact: TruthFact = { kind: 'auctionPeek', name: 'Shield' };
|
||||
const single = renderToStaticMarkup(createElement(TruthFactFields, { value: fact, game: view, target: 'b', onChange() {} }));
|
||||
assert.match(single, /value="auctionPeek" selected/);
|
||||
assert.match(single, /value="Shield" selected/);
|
||||
assert.match(single, /current ordinary auction card/);
|
||||
assert.match(single, /If no current card is known/);
|
||||
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, /Inspected ordinary auction card/);
|
||||
assert.match(grouped, /value="Shield" selected/);
|
||||
});
|
||||
|
||||
void test('spice peek controls offer public card identities with private-knowledge guidance', () => {
|
||||
const markupText = markup({ kind: 'spicePeek', value: 'worm' });
|
||||
assert.match(markupText, /value="worm"/);
|
||||
|
||||
Reference in new issue
Block a user