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'})); });