Files
Dune/tests/truthtrance-freeform-v5-runtime.test.ts
T
Quintine 15a4f3038a
Verify and publish checkpoint container / container (push) Successful in 10m54s
Interpret compound Truthtrance hand facts
2026-10-29 17:56:30 +11:00

79 lines
4.1 KiB
TypeScript

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 { moveSemutaEmperorCard } from './fixture-semuta-emperor-share';
import { knowledgeGame, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
/** Preserve every physical card while staging only the target's hand. */
function pending(names: readonly string[], text: string, scope: 'fact' | 'currentTurn' = 'fact') {
let g = knowledgeGame();
const target = g.players.find(p => p.id === 'b')!;
for (const card of target.hand.slice()) moveSemutaEmperorCard(g, card.id, 'f');
for (const name of names) {
const card = [...g.deck, ...g.discard, ...g.players.flatMap(p => p.hand)].find(c => c.name === name);
assert.ok(card, `Find physical ${name}`);
moveSemutaEmperorCard(g, card.id, target.id);
}
const truth = g.players[0].hand.find(card => card.effect === 'truthtrance');
assert.ok(truth);
g = applyAction(g, 'a', {type: 'card', card: truth.id});
while (g.truthtrance?.stage === 'priority') {
const p = g.players.find(row => !g.truthtrance!.passed.includes(row.id));
assert.ok(p); g = applyAction(g, p.id, {type:'truthPass'});
}
return applyAction(g, 'a', {type:'truthAsk', question: {kind:'freeform', target:'b', scope, text}});
}
function question(g: Game) {
const q = g.truthtrance?.question;
assert.equal(q?.kind, 'freeform');
if (q?.kind !== 'freeform') throw Error('Expected a freeform question');
return q;
}
for (const row of [
{name:'both category terms hold', cards:['Chaumas', 'Shield'], text:'Do you have a poison weapon and a defense?', answer:'yes'},
{name:'one category is absent', cards:['Shield'], text:'Do you have a poison weapon and a defense?', answer:'no'},
{name:'named card and negative category', cards:['Shield'], text:'Do you have a Shield and no poison weapons?', answer:'yes'},
{name:'OR accepts the negative alternative', cards:['Shield'], text:'Do you have a poison weapon or no poison weapons?', answer:'yes'},
] as const) {
void test(`v5 ${row.name} enforces the target's actual answer`, () => {
const g = pending(row.cards, row.text);
assert.equal(question(g).interpreted?.version, 5);
assert.equal(viewGame(g, 'b').truthAnswer, row.answer);
for (const observer of ['a', 'f', 'o']) assert.equal(viewGame(g, observer).truthAnswer, null);
rejectKnowledge(g, 'b', {type:'truthAnswer', answer:row.answer === 'yes' ? 'no' : 'yes'});
rejectKnowledge(g, 'b', {type:'truthAnswer', answer:'unknown'});
for (const difficulty of DIFFICULTIES) {
const view = viewGame(g, 'b');
view.players.find(p => p.id === 'b')!.bot = difficulty;
assert.deepEqual(botActions(view), [{type:'truthAnswer', answer:row.answer}]);
}
const physical = knowledgePhysical(g), restored = JSON.parse(JSON.stringify(g)) as Game;
const done = applyAction(restored, 'b', {type:'truthAnswer', answer:row.answer});
assert.equal(done.truthHistory!.at(-1)!.answer, row.answer);
assert.deepEqual(knowledgePhysical(done), physical);
});
}
void test('uninterpreted old receipts and current-turn wording keep their existing manual semantics', () => {
const text = 'Do you have a Shield and no poison weapons?';
const future = pending(['Shield'], text, 'currentTurn');
assert.equal(question(future).interpreted, undefined);
const old = pending(['Shield'], text);
delete question(old).interpreted;
assert.equal(viewGame(old, 'b').truthAnswer, null);
assert.equal(applyAction(old, 'b', {type:'truthAnswer', answer:'no'}).truthHistory!.at(-1)!.answer, 'no');
});
void test('a modified version-5 interpreted receipt is rejected', () => {
const g = pending(['Shield'], 'Do you have a Shield and no poison weapons?');
const q = question(g);
assert.equal(q.interpreted?.version, 5);
if (!q.interpreted) throw Error('Expected the recognized receipt');
q.interpreted.fact = {kind:'hand', name:'Shield'};
assert.throws(() => viewGame(g, 'b'));
assert.throws(() => applyAction(g, 'b', {type:'truthAnswer', answer:'yes'}));
});