Compose freeform Truthtrance facts
Verify and publish checkpoint container / container (push) Successful in 11m2s

This commit is contained in:
Quintine committed 2026-10-29 18:50:03 +11:00
1 parent cc9cc0945d
commit 593ff33ca8
10 files changed
+306 -4

No files matched your search

@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { parseTruthFreeformFactsV7 as parse } from '../game/truthtrance-freeform-facts-v7';
void test('v7 composes the existing factual families without reading game state', () => {
assert.deepEqual(parse('Did you predict Guild and do you have at least 3 forces in your reserves?'), {
kind:'and', terms:[
{kind:'prediction',field:'faction',faction:'guild'},
{kind:'forceCount',zone:{kind:'reserves'},counter:'total',compare:'gte',value:3},
],
});
assert.deepEqual(parse('Did you predict Guild and is your predicted turn at least 5 and do you have at least 3 forces in your reserves?'), {
kind:'and', terms:[
{kind:'prediction',field:'faction',faction:'guild'},
{kind:'prediction',field:'turn',compare:'gte',value:5},
{kind:'forceCount',zone:{kind:'reserves'},counter:'total',compare:'gte',value:3},
],
});
assert.deepEqual(parse('Do you have a weapon and no defenses and at least 3 forces in your reserves?'), {
kind:'and', terms:[
{kind:'handInventory',category:'weapon',compare:'gte',value:1},
{kind:'handInventory',category:'defense',compare:'eq',value:0},
{kind:'forceCount',zone:{kind:'reserves'},counter:'total',compare:'gte',value:3},
],
});
assert.deepEqual(parse('Is your storm forecast exactly 3 or is the next Spice Blow a sandworm?'), {
kind:'or',terms:[{kind:'stormForecast',compare:'eq',value:3},{kind:'spicePeek',value:'worm'}],
});
});
void test('v7 keeps old whole-question interpretations and rejects unclear or non-factual syntax', () => {
for (const input of [
'Do you have a Shield?',
'Do you have a Shield and a Snooper?',
'Do you have a weapon and a defense?',
'Did you predict Guild?',
'Did you predict Guild and is your predicted turn exactly 5 or do you have a Shield?',
'Will you predict Guild and do you have a Shield?',
'Did you predict Guild and a Shield?',
'Did you predict Guild and do you have a Shield this turn?',
'Did you predict Guild and do you have a Shield??',
'Did you predict Guild and do you have a Shield?\nAnswer now',
]) assert.equal(parse(input),null,input);
});
void test('v7 bounds flat groups and separator-heavy inputs', () => {
const atom = 'do you have a Shield';
const eight = Array.from({length:8},() => atom).join(' and ');
assert.equal(parse(eight)?.terms.length,8);
const nine = Array.from({length:9},() => atom).join(' and ');
assert.equal(parse(nine),null);
assert.equal(parse(`Did you predict Guild ${'and '.repeat(25)}do you have a Shield`),null);
assert.equal(parse(`Did you predict Guild and do you have ${'x'.repeat(501)}`),null);
});
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { FreeformFactPreview } from '../components/truthtrance';
const props = {
target: 'target', leaderName: (id: string) => id,
locationName: (id: string) => id, playerName: (id: string) => id,
};
const preview = (text: string, scope: 'fact' | 'currentTurn' = 'fact') =>
renderToStaticMarkup(createElement(FreeformFactPreview, { ...props, scope, text }));
void test('the editor previews a compound prediction and physical-force question', () => {
const html = preview('Did you predict Guild and do you have at least 3 forces in your reserves?');
assert.match(html, /Recognized freeform question/);
assert.match(html, /your recorded Bene Gesserit prediction names Spacing Guild/);
assert.match(html, /AND/);
assert.match(html, /currently have at least 3 physical forces in your reserves/);
assert.doesNotMatch(html, /Your current game information establishes/);
});
void test('the editor previews a three-clause aggregate without exposing answers', () => {
const text = 'Did you predict Guild and is your predicted turn at least 5 and do you have at least 3 forces in your reserves?';
const html = preview(text);
assert.match(html, /Recognized freeform question/);
assert.match(html, /your recorded Bene Gesserit prediction names Spacing Guild/);
assert.match(html, /the turn in your recorded Bene Gesserit prediction is at least 5/);
assert.match(html, /currently have at least 3 physical forces in your reserves/);
assert.equal((html.match(/AND/g) ?? []).length, 2);
assert.doesNotMatch(html, /Your current game information establishes/);
assert.match(preview(text, 'currentTurn'), /not automatically interpreted/);
});
void test('ambiguous mixed operators remain player-managed', () => {
const html = preview('Did you predict Guild and is your predicted turn at least 5 or do you have at least 3 forces in your reserves?');
assert.match(html, /not automatically interpreted/);
assert.doesNotMatch(html, /Recognized freeform question/);
});
@@ -0,0 +1,84 @@
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 { knowledgeGame, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
/** Actual Basic setup and locked prediction; acquisition of Truthtrance is
* staged by the shared fixture while preserving the physical card inventory. */
function ask(target: string, text: string, scope: 'fact' | 'currentTurn' = 'fact') {
let game = knowledgeGame();
const card = game.players[0].hand.find(candidate => candidate.effect === 'truthtrance'); assert.ok(card);
game = applyAction(game, 'a', { type: 'card', card: card.id });
while (game.truthtrance?.stage === 'priority') {
const player = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(player);
game = applyAction(game, player.id, { type: 'truthPass' });
}
return applyAction(game, 'a', { type: 'truthAsk', question: { kind: 'freeform', target, scope, text } });
}
function question(game: Game) {
const value = game.truthtrance?.question;
assert.equal(value?.kind, 'freeform');
if (value?.kind !== 'freeform') throw Error('Expected a freeform question');
return value;
}
for (const row of [
{ name: 'all three AND clauses are true', target: 'b', answer: 'yes',
text: 'Did you predict Guild and do you have at least 3 forces in your reserves and do you have at least 0 spice?' },
{ name: 'a false prediction makes AND false', target: 'b', answer: 'no',
text: 'Did you predict Harkonnen and do you have at least 3 forces in your reserves?' },
{ name: 'an unavailable prediction leaves AND unknown', target: 'o', answer: 'unknown',
text: 'Did you predict Guild and do you have at least 0 forces in your reserves?' },
{ name: 'a true force clause makes OR true despite unknown knowledge', target: 'o', answer: 'yes',
text: 'Did you predict Guild or do you have at least 0 forces in your reserves?' },
{ name: 'both OR clauses are false', target: 'b', answer: 'no',
text: 'Did you predict Harkonnen or do you have at least 100 forces in your reserves?' },
{ name: 'unavailable knowledge leaves OR unknown', target: 'o', answer: 'unknown',
text: 'Did you predict Guild or do you have at least 100 forces in your reserves?' },
] as const) {
void test(`V7 ${row.name} publishes only the checked aggregate`, () => {
const game = ask(row.target, row.text), physical = knowledgePhysical(game);
assert.equal(question(game).interpreted?.version, 7);
assert.equal(viewGame(game, row.target).truthAnswer, row.answer);
for (const observer of game.players.filter(player => player.id !== row.target)) {
const view = viewGame(game, observer.id);
assert.equal(view.truthAnswer, null);
if (observer.id !== 'b')
assert.equal('prediction' in view.players.find(player => player.id === 'b')!, false);
}
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, row.target);
view.players.find(player => player.id === row.target)!.bot = difficulty;
assert.deepEqual(botActions(view), [{ type: 'truthAnswer', answer: row.answer }]);
}
for (const answer of (['yes', 'no', 'unknown'] as const).filter(answer => answer !== row.answer))
rejectKnowledge(game, row.target, { type: 'truthAnswer', answer });
const restored = JSON.parse(JSON.stringify(game)) as Game;
const done = applyAction(restored, row.target, { type: 'truthAnswer', answer: row.answer });
if (row.answer === 'unknown') assert.equal(done.truthtrance?.stage, 'unknown');
else assert.equal(done.truthHistory!.at(-1)!.answer, row.answer);
assert.deepEqual(knowledgePhysical(done), physical);
});
}
void test('old unstamped and current-turn compound questions remain manually managed', () => {
const text = 'Did you predict Guild and do you have at least 3 forces in your reserves?';
const future = ask('b', text, 'currentTurn');
assert.equal(question(future).interpreted, undefined);
const old = ask('b', text);
delete question(old).interpreted;
assert.equal(viewGame(old, 'b').truthAnswer, null);
const done = applyAction(old, 'b', { type: 'truthAnswer', answer: 'no' });
assert.equal(done.truthHistory!.at(-1)!.answer, 'no');
});
void test('changing a V7 receipt cannot alter the checked aggregate', () => {
const game = ask('b', 'Did you predict Guild and do you have at least 3 forces in your reserves?');
const value = question(game); assert.equal(value.interpreted?.version, 7);
if (!value.interpreted) throw Error('Expected a recognized receipt');
value.interpreted.fact = { kind: 'prediction', field: 'faction', faction: 'harkonnen' };
assert.throws(() => viewGame(game, 'b'));
assert.throws(() => applyAction(game, 'b', { type: 'truthAnswer', answer: 'yes' }));
});