121 lines
6.5 KiB
TypeScript
121 lines
6.5 KiB
TypeScript
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}});
|
|
});
|