Files
Dune/tests/truthtrance-freeform-v4-runtime.test.ts
T
Quintine 4f990db52e
Verify and publish checkpoint container / container (push) Canceled after 0s
Expand Basic Semuta and Truthtrance rules
2026-10-29 05:57:46 +11:00

77 lines
3.8 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';
/** Use the real card, priority queue and answer actions with conserved hand staging. */
function pending(present: boolean, category: boolean, 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');
if (present) {
const card = [...g.deck, ...g.discard, ...g.players.flatMap(p => p.hand)]
.find(c => category ? c.kind === 'projectile' : c.name === 'Shield');
assert.ok(card); 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'});
}
const text = category ? 'Do you have no weapons?' : 'Do you not have a Shield?';
return applyAction(g, 'a', {type:'truthAsk', question:{kind:'freeform', target:'b', scope, text}});
}
function freeform(g: Game) {
const q = g.truthtrance?.question;
assert.equal(q?.kind, 'freeform');
if (q?.kind !== 'freeform') throw Error('Expected freeform question');
return q;
}
for (const present of [true, false]) for (const category of [true, false]) {
void test(`v4 ${category ? 'category' : 'named card'} negation enforces the truthful ${present ? 'no' : 'yes'} answer`, () => {
const g = pending(present, category);
const answer = present ? 'no' : 'yes';
const fact = category
? {kind:'handInventory', category:'weapon', compare:'eq', value:0}
: {kind:'handCount', name:'Shield', compare:'eq', value:0};
assert.deepEqual(freeform(g).interpreted, {version:4, fact});
assert.equal(viewGame(g, 'b').truthAnswer, answer);
for (const observer of ['a', 'f', 'o']) assert.equal(viewGame(g, observer).truthAnswer, null);
rejectKnowledge(g, 'b', {type:'truthAnswer', answer:present ? 'yes' : 'no'});
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}]);
}
const physical = knowledgePhysical(g);
const restored = JSON.parse(JSON.stringify(g)) as Game;
const done = applyAction(restored, 'b', {type:'truthAnswer', answer});
assert.equal(done.truthHistory!.at(-1)!.answer, answer);
assert.deepEqual(knowledgePhysical(done), physical);
});
}
void test('unstamped negative questions and current-turn text retain manual semantics', () => {
const current = pending(true, false, 'currentTurn');
assert.equal(freeform(current).interpreted, undefined);
const old = pending(true, false);
delete freeform(old).interpreted;
assert.equal(viewGame(old, 'b').truthAnswer, null);
assert.equal(applyAction(old, 'b', {type:'truthAnswer', answer:'yes'}).truthHistory!.at(-1)!.answer, 'yes');
});
void test('a tampered version-4 receipt cannot change the recognized negative fact', () => {
const g = pending(true, false);
const q = freeform(g);
q.interpreted = {version:4, fact:{kind:'handCount', name:'Snooper', compare:'eq', value:0}};
assert.throws(() => viewGame(g, 'b'));
assert.throws(() => applyAction(g, 'b', {type:'truthAnswer', answer:'yes'}));
});