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
@@ -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