Interpret compound Truthtrance hand facts
Verify and publish checkpoint container / container (push) Successful in 10m54s
Verify and publish checkpoint container / container (push) Successful in 10m54s
This commit is contained in:
1 parent
4224bb3e2a
commit
15a4f3038a
9 files changed
+268
-17
No files matched your search
+20
-11
@@ -69,6 +69,7 @@ import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import { parseFreeformFactV2 } from '@/game/truthtrance-freeform-facts-v2';
|
||||
import { parseFreeformFactV3 } from '@/game/truthtrance-freeform-facts-v3';
|
||||
import { parseFreeformFactV4 } from '@/game/truthtrance-freeform-facts-v4';
|
||||
import { parseTruthFreeformFactsV5 } from '@/game/truthtrance-freeform-facts-v5';
|
||||
import {
|
||||
truthQuestionText,
|
||||
type TruthFact,
|
||||
@@ -102,6 +103,23 @@ export function EarlyRicheseBidFields({ minimum, onMinimum, disabled }: {
|
||||
</fieldset>;
|
||||
}
|
||||
|
||||
/** Show only a recognized current-fact interpretation; unsupported text and
|
||||
* current-turn promises keep their existing manual-answer guidance. */
|
||||
export function FreeformFactPreview({ scope, text, target, leaderName, locationName, playerName }: {
|
||||
scope: 'fact' | 'currentTurn'; text: string; target: string;
|
||||
leaderName: (id: string) => string; locationName: (id: string) => string; playerName: (id: string) => string;
|
||||
}) {
|
||||
const fact = scope === 'fact' ? parseFreeformFactV2(text) || parseFreeformFactV3(text) ||
|
||||
parseFreeformFactV4(text) || parseTruthFreeformFactsV5(text) : null;
|
||||
return fact ? <p className="notice" aria-label="Recognized freeform question">
|
||||
This will be checked as: {truthQuestionText({ kind: 'fact', target, fact }, leaderName, locationName, playerName)}
|
||||
</p> : <p className="notice">
|
||||
This wording is not automatically interpreted. Players must answer
|
||||
facts truthfully; AI will answer I don’t know. Automatic checks
|
||||
for arbitrary promises remain unfinished.
|
||||
</p>;
|
||||
}
|
||||
|
||||
export function Truthtrance({
|
||||
game: g,
|
||||
act,
|
||||
@@ -979,17 +997,8 @@ export function Truthtrance({
|
||||
placeholder="Will you ship at least six forces to Carthag this turn?"
|
||||
/>
|
||||
</label>
|
||||
{scope === 'fact' && (parseFreeformFactV2(text) || parseFreeformFactV3(text) || parseFreeformFactV4(text)) ? (
|
||||
<p className="notice" aria-label="Recognized freeform question">
|
||||
This will be checked as: {truthQuestionText({ kind: 'fact', target, fact: (parseFreeformFactV2(text) || parseFreeformFactV3(text) || parseFreeformFactV4(text))! }, leaderName, locationName, name)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="notice">
|
||||
This wording is not automatically interpreted. Players must answer
|
||||
facts truthfully; AI will answer I don’t know. Automatic checks
|
||||
for arbitrary promises remain unfinished.
|
||||
</p>
|
||||
)}
|
||||
<FreeformFactPreview scope={scope} text={text} target={target}
|
||||
leaderName={leaderName} locationName={locationName} playerName={name} />
|
||||
</>
|
||||
)}
|
||||
{kind === 'pledgeAid' && <>
|
||||
|
||||
@@ -17,6 +17,16 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Compound freeform Truthtrance facts — 29 October. Prototyped / Partial.**
|
||||
New fact-scoped questions recognize an unambiguous AND/OR of two supported
|
||||
current hand or spice facts, including category and negative conditions. The
|
||||
editor previews the interpretation; the target and bots use the same checked
|
||||
answer. Earlier versioned and unstamped questions retain their semantics. The
|
||||
runtime fixture stages conserved card custody; eleven related files pass 54
|
||||
cases. Unsupported language remains manual, and full rules and deployed
|
||||
verification remain open.
|
||||
[Contract](TRUTHTRANCE_KNOWLEDGE.md#freeform-compound-hand-category-facts--29-october-2026).
|
||||
|
||||
**Ghola promise during Box selection — 29 October. Prototyped / Partial.**
|
||||
A paid Nullentropy Box selection must preserve an outstanding named Ghola Yes;
|
||||
an incompatible card is rejected before shuffle while the paid search remains
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Truthtrance: recorded predictions and storm knowledge
|
||||
|
||||
## Freeform compound hand-category facts — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** New fact-scoped freeform questions can combine two
|
||||
supported current-hand facts with one unambiguous AND or OR, including printed
|
||||
weapon/defense categories and supported negative card or category counts. For
|
||||
example, a player may ask whether the respondent holds both a poison weapon and
|
||||
a defense, or holds a Shield and no poison weapons. The public editor previews
|
||||
the interpreted aggregate; the target alone sees the verified answer before
|
||||
publishing it, and minimal bots follow the same authoritative fact. Category
|
||||
meanings use the existing printed primary-role classification.
|
||||
|
||||
Newly recognized compositions carry version 5. Existing versions 1–4 and
|
||||
unstamped saved questions keep their original behavior. Ambiguous forms, mixed
|
||||
operators, unsupported wording and current-turn promises remain player-managed;
|
||||
this is a bounded interpretation of the FAQ's general game-related yes/no
|
||||
permission, not general language understanding. The focused runtime fixture
|
||||
stages conserved physical card custody, rather than claiming the exact starting
|
||||
hand was earned. See [parser](../game/truthtrance-freeform-facts-v5.ts),
|
||||
[pure cases](../tests/truth-freeform-facts-v5.test.ts) and
|
||||
[runtime cases](../tests/truthtrance-freeform-v5-runtime.test.ts). Full rules,
|
||||
deployed and broader combinations remain open.
|
||||
|
||||
## Freeform negative hand facts — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** New current-fact questions recognize bounded forms
|
||||
@@ -8,8 +30,9 @@ The editor shows the checked zero-card interpretation, the respondent sees
|
||||
the private answer, and bots use the same truthful answer. Named faces and
|
||||
the existing primary-role categories use their existing count predicates.
|
||||
Newly recognized questions carry version 4; older versions and unstamped
|
||||
questions retain their prior semantics. Ambiguous, compound, future and other
|
||||
unrecognized wording remains player-managed. The parser is a provisional
|
||||
questions retain their prior semantics. Ambiguous, unsupported compound, future and other
|
||||
unrecognized wording remains player-managed; the later V5 section adds bounded
|
||||
compound forms. The parser is a provisional
|
||||
online interpretation of the FAQ's game-related yes/no permission, not
|
||||
general language understanding. [Parser](../game/truthtrance-freeform-facts-v4.ts),
|
||||
[runtime cases](../tests/truthtrance-freeform-v4-runtime.test.ts).
|
||||
|
||||
+1
-1
@@ -4031,7 +4031,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
steps: [
|
||||
'Declare the card from your hand at any time after cards are dealt. Other players may declare a competing Truthtrance or pass; questions are then asked in storm order. The interrupted phase, response, or battle decision resumes afterward.',
|
||||
'Ask a game-related yes/no question. Named-card, selected-traitor, currently unrevealed Face Dancer and current personal-spice questions are checked by the server. Face Dancer and ordinary Traitor holdings stay distinct. AND and OR combine facts; only the combined answer is published, with no extra inventory details or separate answer for each clause.',
|
||||
'In a new freeform game-fact question, exact personal-spice, named-card, total-hand-size and selected weapon, defense or Worthless category forms are checked as current facts. Supported negative hand questions check that the named face or category count is zero. Category questions use the displayed printed primary role. Two spice or named-card facts may be joined by one AND or OR; only their aggregate answer is published. The recognized interpretation appears before asking and with the question. Other wording and current-turn promises remain player-managed; bots answer unknown. Already-pending older questions keep their original behavior.',
|
||||
'In a new freeform game-fact question, exact personal-spice, named-card, total-hand-size and selected weapon, defense or Worthless category forms are checked as current facts. Supported negative hand questions check that the named face or category count is zero. Category questions use the displayed printed primary role. Two supported current facts may be joined by one unambiguous AND or OR, including selected printed hand categories and negative card or category counts; only their aggregate answer is published. The recognized interpretation appears before asking and with the question. Other wording and current-turn promises remain player-managed; bots answer unknown. Already-pending older questions keep their original behavior.',
|
||||
'In supported quiet Basic Bidding or Shipment and Movement, ask whether a player will update funding for their current mutual ally to at least a chosen amount this phase. Yes requires a future accepted update while possible; No forbids reaching the amount. An existing pledge alone does not count, a repeated same total does, and the ally chooses whether to spend it. Only one definite funding question is supported per phase; other future-action questions in that phase and optional modules remain separate.',
|
||||
'For current spice, choose Exactly, At least or At most and a nonnegative whole amount. This counts spice personally held when answering, excluding separately pledged aid and incoming payments. It does not ask whether a particular action is affordable or promise that the balance will stay unchanged. A true current-spice answer does not prevent later legal spending.',
|
||||
'For a concealed No-Field, choose its current printed value0,3 or5, not the prospective force pool. A missing or already revealed marker answers No, including for0; a concealed5 with empty reserves still answers Yes to5. Only the respondent sees the expected aggregate before public reply. Neither question nor answer turns up the physical token or commits later use; current-force facts continue to exclude markers.',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { parseFreeformFactV3 } from './truthtrance-freeform-facts-v3';
|
||||
import { parseFreeformFactV4 } from './truthtrance-freeform-facts-v4';
|
||||
import type { TruthFact } from './truthtrance';
|
||||
|
||||
type Compound = Extract<TruthFact, { kind: 'and' | 'or' }>;
|
||||
|
||||
/**
|
||||
* A new, deliberately narrow grammar: two existing current-fact atoms joined
|
||||
* by one unambiguous AND/OR. Older parser versions and their saved receipts
|
||||
* remain unchanged. Call only for fact-scoped text and show the interpretation.
|
||||
*/
|
||||
export function parseTruthFreeformFactsV5(text: string): Compound | null {
|
||||
if (typeof text !== 'string' || text.length > 500) return null;
|
||||
const input = text.trim();
|
||||
// A complete old interpretation, including a card name containing "and",
|
||||
// retains its original meaning rather than being reinterpreted by V5.
|
||||
if (parseFreeformFactV3(input) || parseFreeformFactV4(input)) return null;
|
||||
const question = /^do[ \t]+you[ \t]+have[ \t]+(.+?)[ \t]*\??$/i.exec(input);
|
||||
if (!question) return null;
|
||||
const body = question[1];
|
||||
const atom = (part: string): TruthFact | null => {
|
||||
const value = part.trim();
|
||||
if (!value || /[?\r\n]/.test(value)) return null;
|
||||
const full = /^do[ \t]+you[ \t]+(?:have|not[ \t]+have)[ \t]+/i.test(value)
|
||||
? value : /^not[ \t]+have[ \t]+/i.test(value) ? `Do you ${value}`
|
||||
: /^have[ \t]+/i.test(value) ? `Do you ${value}` : `Do you have ${value}`;
|
||||
const fact = parseFreeformFactV3(full) ?? parseFreeformFactV4(full);
|
||||
return fact && fact.kind !== 'and' && fact.kind !== 'or' ? fact : null;
|
||||
};
|
||||
const matches: Compound[] = [];
|
||||
for (const connector of body.matchAll(/[ \t]+(and|or)[ \t]+/gi)) {
|
||||
const index = connector.index ?? 0;
|
||||
const left = atom(body.slice(0, index));
|
||||
const right = atom(body.slice(index + connector[0].length));
|
||||
if (left && right) matches.push({ kind: connector[1].toLowerCase() as 'and' | 'or', terms: [left, right] });
|
||||
}
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
+11
-2
@@ -71,6 +71,7 @@ import { parseFreeformFactV1 } from './truthtrance-freeform-facts';
|
||||
import { parseFreeformFactV2 } from './truthtrance-freeform-facts-v2';
|
||||
import { parseFreeformFactV3 } from './truthtrance-freeform-facts-v3';
|
||||
import { parseFreeformFactV4 } from './truthtrance-freeform-facts-v4';
|
||||
import { parseTruthFreeformFactsV5 } from './truthtrance-freeform-facts-v5';
|
||||
import { CHEAP_HERO_TRAITOR } from './traitors';
|
||||
import {
|
||||
TRUTH_FACT_MAX_NODES, TRUTH_FACT_MAX_DEPTH, TRUTH_FACT_MAX_TERMS,
|
||||
@@ -136,7 +137,7 @@ export type TruthQuestion =
|
||||
text: string;
|
||||
scope: 'fact' | 'currentTurn';
|
||||
/** Only newly asked, explicitly recognized facts carry this stable interpretation. */
|
||||
interpreted?: { version: 1; fact: Extract<TruthFact, { kind: 'hand' | 'spice' }> } | { version: 2 | 3 | 4; fact: TruthFact };
|
||||
interpreted?: { version: 1; fact: Extract<TruthFact, { kind: 'hand' | 'spice' }> } | { version: 2 | 3 | 4 | 5; fact: TruthFact };
|
||||
};
|
||||
export type TruthAnswer = 'yes' | 'no' | 'unknown';
|
||||
export type TruthQueueEntry = {
|
||||
@@ -698,10 +699,14 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
? parseFreeformFactV3(question.text) : null;
|
||||
const negativeFact = question.scope === 'fact' && marker.version === 4 && !parseFreeformFactV3(question.text)
|
||||
? parseFreeformFactV4(question.text) : null;
|
||||
const compoundFact = question.scope === 'fact' && marker.version === 5 &&
|
||||
!parseFreeformFactV3(question.text) && !parseFreeformFactV4(question.text)
|
||||
? parseTruthFreeformFactsV5(question.text) : null;
|
||||
const expected = oldFact ? { version: 1 as const, fact: oldFact }
|
||||
: newFact ? { version: 2 as const, fact: newFact }
|
||||
: categoryFact ? { version: 3 as const, fact: categoryFact }
|
||||
: negativeFact ? { version: 4 as const, fact: negativeFact } : null;
|
||||
: negativeFact ? { version: 4 as const, fact: negativeFact }
|
||||
: compoundFact ? { version: 5 as const, fact: compoundFact } : null;
|
||||
check(expected !== null && JSON.stringify(v.interpreted) === JSON.stringify(expected),
|
||||
'The saved freeform interpretation does not match its original question.');
|
||||
question.interpreted = expected;
|
||||
@@ -1175,6 +1180,10 @@ export function resolveTruthAction(
|
||||
else {
|
||||
const negativeFact = parseFreeformFactV4(w.question.text);
|
||||
if (negativeFact) w.question.interpreted = { version: 4, fact: negativeFact };
|
||||
else {
|
||||
const compoundFact = parseTruthFreeformFactsV5(w.question.text);
|
||||
if (compoundFact) w.question.interpreted = { version: 5, fact: compoundFact };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseTruthFreeformFactsV5 as parse } from '../game/truthtrance-freeform-facts-v5';
|
||||
|
||||
void test('v5 composes current category, card, spice and negative facts', () => {
|
||||
assert.deepEqual(parse('Do you have a poison weapon and a defense?'), {
|
||||
kind: 'and', terms: [
|
||||
{ kind: 'handInventory', category: 'poisonWeapon', compare: 'gte', value: 1 },
|
||||
{ kind: 'handInventory', category: 'defense', compare: 'gte', value: 1 },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(parse('Do you have a Shield and no poison weapons?'), {
|
||||
kind: 'and', terms: [
|
||||
{ kind: 'hand', name: 'Shield' },
|
||||
{ kind: 'handInventory', category: 'poisonWeapon', compare: 'eq', value: 0 },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(parse('Do you have at least 5 spice OR do you not have any defenses?'), {
|
||||
kind: 'or', terms: [
|
||||
{ kind: 'spice', compare: 'gte', value: 5 },
|
||||
{ kind: 'handInventory', category: 'defense', compare: 'eq', value: 0 },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(parse('Do you have a weapon and have exactly 2 cards in your hand?'), {
|
||||
kind: 'and', terms: [
|
||||
{ kind: 'handInventory', category: 'weapon', compare: 'gte', value: 1 },
|
||||
{ kind: 'handInventory', category: 'all', compare: 'eq', value: 2 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
void test('v5 leaves old, ambiguous, mixed, future and unsupported wording untouched', () => {
|
||||
for (const question of [
|
||||
'Do you have a Shield?', 'Do you have a Shield and a Snooper?',
|
||||
'Do you have a weapon and a defense or a Shield?',
|
||||
'Do you have a weapon and a defense and a Shield?',
|
||||
'Do you have a weapon or a special card?',
|
||||
'Will you have a weapon and a defense?',
|
||||
'Do you have a weapon and a defense this turn?',
|
||||
'Do you have a weapon and a defense??',
|
||||
'Do you have a weapon and a defense?\nAnswer now',
|
||||
`Do you have ${'x'.repeat(501)} and a weapon?`,
|
||||
]) assert.equal(parse(question), null, question);
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { Truthtrance } from '../components/truthtrance';
|
||||
import { FreeformFactPreview, Truthtrance } from '../components/truthtrance';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { knowledgeGame } from './truthtrance-knowledge-fixture';
|
||||
|
||||
@@ -79,3 +79,43 @@ void test('a negative freeform displays the checked zero-card meaning without ex
|
||||
assert.match(target, /Your current game information establishes:/);
|
||||
assert.doesNotMatch(observer, /Your current game information establishes:/);
|
||||
});
|
||||
|
||||
void test('a category and negative-card compound displays its checked aggregate without revealing the answer', () => {
|
||||
const game = pending(5, 'Do you have a weapon and no Shield?');
|
||||
const target = markup(game, 'b'), observer = markup(game, 'o');
|
||||
for (const html of [target, observer]) {
|
||||
assert.match(html, /Checked as:/);
|
||||
assert.match(html, /you currently hold at least 1 card whose primary role is a weapon/);
|
||||
assert.match(html, /AND/);
|
||||
assert.match(html, /you currently hold exactly 0 cards named Shield/);
|
||||
}
|
||||
assert.match(target, /Your current game information establishes:/);
|
||||
assert.doesNotMatch(observer, /Your current game information establishes:/);
|
||||
});
|
||||
|
||||
void test('unsupported mixed conjunctions keep their manual answer control', () => {
|
||||
const game = pending(5, 'Do you have a weapon and a defense or a Shield?');
|
||||
const target = markup(game, 'b');
|
||||
assert.doesNotMatch(target, /Checked as:/);
|
||||
assert.doesNotMatch(target, /Your current game information establishes:/);
|
||||
assert.match(target, /Answer Yes/);
|
||||
assert.match(target, /Answer No/);
|
||||
});
|
||||
|
||||
void test('the question editor previews the v5 interpretation only for current facts', () => {
|
||||
const props = { scope: 'fact' as const, text: 'Do you have a weapon and no Shield?', target: 'b',
|
||||
leaderName: (id: string) => id, locationName: (id: string) => id, playerName: (id: string) => id };
|
||||
const recognized = renderToStaticMarkup(createElement(FreeformFactPreview, props));
|
||||
assert.match(recognized, /Recognized freeform question/);
|
||||
assert.match(recognized, /primary role is a weapon/);
|
||||
assert.match(recognized, /AND/);
|
||||
assert.match(recognized, /exactly 0 cards named Shield/);
|
||||
for (const other of [
|
||||
{ ...props, scope: 'currentTurn' as const },
|
||||
{ ...props, text: 'Do you have a weapon and a defense or a Shield?' },
|
||||
]) {
|
||||
const manual = renderToStaticMarkup(createElement(FreeformFactPreview, other));
|
||||
assert.match(manual, /not automatically interpreted/);
|
||||
assert.doesNotMatch(manual, /Recognized freeform question/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
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'}));
|
||||
});
|
||||
Reference in new issue
Block a user