Expand recognized Truthtrance fact questions
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
8ae275f598
commit
9dd386ff7a
11 files changed
+279
-16
No files matched your search
@@ -8,7 +8,7 @@ import { knowledgeGame } from './truthtrance-knowledge-fixture';
|
||||
|
||||
/** Actual native game and Truthtrance actions, with the target's scalar
|
||||
* spice staged to show both personalized result branches. */
|
||||
function pending(spice: number): Game {
|
||||
function pending(spice: number, text = 'Do you have at least 5 spice?'): Game {
|
||||
let game = knowledgeGame();
|
||||
game.players.find(player => player.id === 'b')!.spice = spice;
|
||||
const card = game.players[0].hand.find(item => item.effect === 'truthtrance');
|
||||
@@ -19,7 +19,7 @@ function pending(spice: number): Game {
|
||||
assert.ok(voter); game = applyAction(game,voter.id,{type:'truthPass'});
|
||||
}
|
||||
return applyAction(game,'a',{type:'truthAsk',question:{kind:'freeform',target:'b',
|
||||
scope:'fact',text:'Do you have at least 5 spice?'}});
|
||||
scope:'fact',text}});
|
||||
}
|
||||
function markup(game: Game, seat: string) {
|
||||
return renderToStaticMarkup(createElement(Truthtrance, {game:viewGame(game,seat), busy:false, act() {}}));
|
||||
@@ -44,3 +44,16 @@ for (const [spice, answer] of [[5,'Yes'],[4,'No']] as const) {
|
||||
assert.match(observer, /Waiting for .* to answer/);
|
||||
});
|
||||
}
|
||||
|
||||
void test('a compound freeform shows its aggregate interpretation to the table', () => {
|
||||
const game = pending(5, 'Do you have at least 5 spice or a 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 5 spice/);
|
||||
assert.match(html, /OR/);
|
||||
assert.match(html, /Shield/);
|
||||
}
|
||||
assert.match(target, /<strong>Yes<\/strong>/);
|
||||
assert.doesNotMatch(observer, /Your current game information establishes:/);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseFreeformFactV1 } from '../game/truthtrance-freeform-facts';
|
||||
import { parseFreeformFactV2 } from '../game/truthtrance-freeform-facts-v2';
|
||||
import { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names';
|
||||
|
||||
void test('freeform v2 preserves each supported v1 form and every canonical named card', () => {
|
||||
for (const text of ['Do you have at least 5 spice?', 'Do you have exactly 0 spice?',
|
||||
'Do you have at most 23 spice', ...TRUTH_CARD_NAMES.map(name => `Do you have a ${name}?`)]) {
|
||||
assert.ok(parseFreeformFactV1(text), text);
|
||||
assert.deepEqual(parseFreeformFactV2(text), parseFreeformFactV1(text), text);
|
||||
}
|
||||
});
|
||||
|
||||
void test('freeform v2 recognizes explicit total physical hand comparisons', () => {
|
||||
for (const [text, compare, value] of [
|
||||
['Do you have at least 3 cards in your hand?', 'gte', 3],
|
||||
['Do you have exactly 1 card in your hand', 'eq', 1],
|
||||
['DO YOU HAVE AT MOST 0 CARDS IN YOUR HAND?', 'lte', 0],
|
||||
] as const) assert.deepEqual(parseFreeformFactV2(text), { kind: 'handInventory', category: 'all', compare, value });
|
||||
assert.equal(parseFreeformFactV2('Do you have exactly 9007199254740992 cards in your hand?'), null);
|
||||
assert.equal(parseFreeformFactV2('Do you have at least 3 cards in your hand for battle?'), null);
|
||||
});
|
||||
|
||||
void test('freeform v2 recognizes one explicit AND or OR of v1 current facts', () => {
|
||||
const shield = { kind: 'hand', name: 'Shield' };
|
||||
const snooper = { kind: 'hand', name: 'Snooper' };
|
||||
const spice = { kind: 'spice', compare: 'gte', value: 5 };
|
||||
assert.deepEqual(parseFreeformFactV2('Do you have at least 5 spice and a Shield?'),
|
||||
{ kind: 'and', terms: [spice, shield] });
|
||||
assert.deepEqual(parseFreeformFactV2('Do you have a Shield or a Snooper?'),
|
||||
{ kind: 'or', terms: [shield, snooper] });
|
||||
assert.deepEqual(parseFreeformFactV2('Do you have a Shield AND do you have at least 5 spice?'),
|
||||
{ kind: 'and', terms: [shield, spice] });
|
||||
assert.deepEqual(parseFreeformFactV2(' do\tyou have an ARTILLERY STRIKE or a Shield '),
|
||||
{ kind: 'or', terms: [{ kind: 'hand', name: 'Artillery Strike' }, shield] });
|
||||
});
|
||||
|
||||
void test('freeform v2 never guesses at ambiguous, mixed, future or unsupported language', () => {
|
||||
for (const text of [
|
||||
'', 'Do you have 5 spice?', 'Do you have more than 5 spice?',
|
||||
'Do you not have a Shield?', 'Do you have a Shield this turn?',
|
||||
'Will you have at least 5 spice?', 'Do you have a Shield??',
|
||||
'Do you have a Shield and a Snooper or a Lasgun?',
|
||||
'Do you have a Shield and (a Snooper or a Lasgun)?',
|
||||
'Do you have at least 5 spice and will you win?',
|
||||
'Do you have a Shield or a secret weapon?',
|
||||
'Do you have at least 3 cards in your hand and a Shield?',
|
||||
'Do you have at least 5 spice?\nOr a Shield?',
|
||||
'Do you have a Shield?'.padEnd(501, ' '),
|
||||
]) assert.equal(parseFreeformFactV2(text), null, text);
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share';
|
||||
import { knowledgeGame, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
|
||||
type Stamp = { version: 1; fact: unknown };
|
||||
type Stamp = { version: 1 | 2; fact: unknown };
|
||||
type Freeform = {kind: 'freeform'; target: string; text: string; scope: 'fact' | 'currentTurn'; interpreted?: Stamp};
|
||||
|
||||
/** Actual Truthtrance declaration, priority, question and response actions.
|
||||
@@ -51,7 +51,7 @@ for (const expected of ['yes','no'] as const) for (const kind of ['spice','hand'
|
||||
const physical = knowledgePhysical(initial);
|
||||
const card = initial.players[0].hand.find(item => item.effect === 'truthtrance')!;
|
||||
const pending = ask(initial, {kind:'freeform', target:target.id, scope:'fact', text});
|
||||
assert.deepEqual(freeform(pending).interpreted, {version:1, fact});
|
||||
assert.deepEqual(freeform(pending).interpreted, {version:2, fact});
|
||||
assert.equal(viewGame(pending, target.id).truthAnswer, expected);
|
||||
for (const id of ['a','f','o']) assert.equal(viewGame(pending,id).truthAnswer, null);
|
||||
rejectKnowledge(pending, target.id, {type:'truthAnswer', answer:expected === 'yes' ? 'no' : 'yes'});
|
||||
@@ -74,7 +74,7 @@ void test('only server interpretation is used when a client forges a stamp', ()
|
||||
initial.players.find(player => player.id === 'b')!.spice = 4;
|
||||
const asked = ask(initial, {kind:'freeform', target:'b', scope:'fact',
|
||||
text:'Do you have at least 5 spice?', interpreted:{version:1, fact:{kind:'spice', compare:'gte', value:0}}});
|
||||
assert.deepEqual(freeform(asked).interpreted, {version:1, fact:{kind:'spice',compare:'gte',value:5}});
|
||||
assert.deepEqual(freeform(asked).interpreted, {version:2, fact:{kind:'spice',compare:'gte',value:5}});
|
||||
assert.equal(viewGame(asked,'b').truthAnswer,'no');
|
||||
rejectKnowledge(asked,'b',{type:'truthAnswer',answer:'yes'});
|
||||
const unsupported = ask(initial, {kind:'freeform', target:'b', scope:'fact',
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, 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';
|
||||
|
||||
type Stamp = { version: 1 | 2; fact: unknown };
|
||||
type Freeform = { kind: 'freeform'; target: string; text: string; scope: 'fact' | 'currentTurn'; interpreted?: Stamp };
|
||||
|
||||
/** Native declaration, priority and question; the shared fixture stages only
|
||||
* original physical cards following setup. */
|
||||
function ask(initial: Game, question: Freeform): Game {
|
||||
const owner = initial.players[0];
|
||||
const card = owner.hand.find(row => row.effect === 'truthtrance'); assert.ok(card);
|
||||
let game = applyAction(initial, owner.id, {type: 'card', card: card.id});
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const player = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(player);
|
||||
game = applyAction(game, player.id, {type: 'truthPass'});
|
||||
}
|
||||
return applyAction(game, owner.id, {type: 'truthAsk', question} as Action);
|
||||
}
|
||||
function question(game: Game): Freeform {
|
||||
const current = game.truthtrance?.question;
|
||||
assert.equal(current?.kind, 'freeform');
|
||||
return current as Freeform;
|
||||
}
|
||||
function held(game: Game, name: string, present: boolean) {
|
||||
const target = game.players.find(row => row.id === 'b')!;
|
||||
for (const card of [...target.hand].filter(row => row.name === name))
|
||||
moveSemutaEmperorCard(game, card.id, 'f');
|
||||
if (present) {
|
||||
const card = [...game.deck, ...game.discard, ...game.players.flatMap(row => row.hand)].find(row => row.name === name);
|
||||
assert.ok(card); moveSemutaEmperorCard(game, card.id, target.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const expected of ['yes', 'no'] as const) for (const mode of ['count', 'and', 'or'] as const) {
|
||||
void test(`v2 ${mode} freeform yields only the truthful aggregate ${expected}`, () => {
|
||||
const initial = knowledgeGame();
|
||||
const target = initial.players.find(row => row.id === 'b')!;
|
||||
let text: string, fact: unknown;
|
||||
if (mode === 'count') {
|
||||
const value = target.hand.length + (expected === 'yes' ? 0 : 1);
|
||||
text = `Do you have at least ${value} cards in your hand?`;
|
||||
fact = {kind: 'handInventory', category: 'all', compare: 'gte', value};
|
||||
} else if (mode === 'and') {
|
||||
target.spice = expected === 'yes' ? 5 : 4;
|
||||
held(initial, 'Shield', true);
|
||||
text = 'Do you have at least 5 spice and a Shield?';
|
||||
fact = {kind: 'and', terms: [
|
||||
{kind: 'spice', compare: 'gte', value: 5}, {kind: 'hand', name: 'Shield'},
|
||||
]};
|
||||
} else {
|
||||
held(initial, 'Shield', expected === 'yes');
|
||||
held(initial, 'Snooper', false);
|
||||
text = 'Do you have a Shield or a Snooper?';
|
||||
fact = {kind: 'or', terms: [{kind: 'hand', name: 'Shield'}, {kind: 'hand', name: 'Snooper'}]};
|
||||
}
|
||||
const physical = knowledgePhysical(initial);
|
||||
const source = initial.players[0].hand.find(row => row.effect === 'truthtrance')!;
|
||||
const pending = ask(initial, {kind: 'freeform', target: 'b', scope: 'fact', text});
|
||||
assert.deepEqual(question(pending).interpreted, {version: 2, fact});
|
||||
assert.equal(viewGame(pending, 'b').truthAnswer, expected);
|
||||
for (const observer of ['a', 'f', 'o']) assert.equal(viewGame(pending, observer).truthAnswer, null);
|
||||
rejectKnowledge(pending, 'b', {type: 'truthAnswer', answer: expected === 'yes' ? 'no' : 'yes'});
|
||||
rejectKnowledge(pending, 'b', {type: 'truthAnswer', answer: 'unknown'});
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending, 'b');
|
||||
view.players.find(row => row.id === 'b')!.bot = difficulty;
|
||||
assert.deepEqual(botActions(view), [{type: 'truthAnswer', answer: expected}]);
|
||||
const done = applyAction(structuredClone(pending), 'b', {type: 'truthAnswer', answer: expected});
|
||||
assert.equal(done.truthtrance, null);
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, expected);
|
||||
assert.equal(done.discard.filter(row => row.id === source.id).length, 1);
|
||||
assert.deepEqual(knowledgePhysical(done), physical);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('v2 ignores a forged client interpretation and stamps supported v1 syntax anew', () => {
|
||||
const game = knowledgeGame();
|
||||
game.players.find(row => row.id === 'b')!.spice = 4;
|
||||
const pending = ask(game, {kind:'freeform', target:'b', scope:'fact', text:'Do you have at least 5 spice?',
|
||||
interpreted: {version:2, fact:{kind:'spice', compare:'gte', value:0}}});
|
||||
assert.deepEqual(question(pending).interpreted, {version:2, fact:{kind:'spice', compare:'gte', value:5}});
|
||||
assert.equal(viewGame(pending, 'b').truthAnswer, 'no');
|
||||
rejectKnowledge(pending, 'b', {type:'truthAnswer', answer:'yes'});
|
||||
});
|
||||
|
||||
void test('unsupported combinations and current-turn questions keep the manual semantics', () => {
|
||||
for (const q of [
|
||||
{kind:'freeform', target:'b', scope:'fact', text:'Do you have a Shield or a Snooper and a Lasgun?'},
|
||||
{kind:'freeform', target:'b', scope:'currentTurn', text:'Do you have at least 3 cards in your hand?'},
|
||||
] as Freeform[]) {
|
||||
const pending = ask(knowledgeGame(), q);
|
||||
assert.equal(question(pending).interpreted, undefined);
|
||||
assert.equal(viewGame(pending, 'b').truthAnswer, null);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending, 'b'); view.players.find(row => row.id === 'b')!.bot = difficulty;
|
||||
assert.deepEqual(botActions(view), [{type:'truthAnswer', answer:'unknown'}]);
|
||||
}
|
||||
const done = applyAction(pending, 'b', {type:'truthAnswer', answer:'yes'});
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, 'yes');
|
||||
}
|
||||
});
|
||||
|
||||
void test('a saved v1-stamped question is validated and answered with v1 semantics', () => {
|
||||
const initial = knowledgeGame();
|
||||
initial.players.find(row => row.id === 'b')!.spice = 5;
|
||||
const pending = ask(initial, {kind:'freeform', target:'b', scope:'fact', text:'Do you have at least 5 spice?'});
|
||||
question(pending).interpreted = {version:1, fact:{kind:'spice', compare:'gte', value:5}};
|
||||
const restored = JSON.parse(JSON.stringify(pending)) as Game;
|
||||
assert.equal(viewGame(restored, 'b').truthAnswer, 'yes');
|
||||
rejectKnowledge(restored, 'b', {type:'truthAnswer', answer:'no'});
|
||||
const done = applyAction(restored, 'b', {type:'truthAnswer', answer:'yes'});
|
||||
assert.deepEqual((done.truthHistory!.at(-1)!.question as Freeform).interpreted,
|
||||
{version:1, fact:{kind:'spice', compare:'gte', value:5}});
|
||||
});
|
||||
@@ -475,7 +475,7 @@ void test('AI handles unparseable freeform questions honestly and can complete a
|
||||
text: 'Will you win two battles?',
|
||||
};
|
||||
const pending = ask(priority(declare(initial)), q);
|
||||
assert.throws(() => answer(pending, 'yes'), /cannot yet interpret/);
|
||||
assert.throws(() => answer(pending, 'yes'), /cannot interpret this freeform question/);
|
||||
const unknown = runBots(pending);
|
||||
assert.equal(unknown.truthtrance!.stage, 'unknown');
|
||||
assert.equal(unknown.discard.length, 0);
|
||||
|
||||
Reference in new issue
Block a user