116 lines
6.2 KiB
TypeScript
116 lines
6.2 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};
|
|
|
|
/** Actual Truthtrance declaration, priority, question and response actions.
|
|
* The shared fixture starts the native game and stages original card custody. */
|
|
function ask(state: Game, question: Freeform): Game {
|
|
const owner = state.players[0];
|
|
const card = owner.hand.find(item => item.effect === 'truthtrance'); assert.ok(card);
|
|
let game = applyAction(state, owner.id, {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, owner.id, {type:'truthAsk', question} as Action);
|
|
}
|
|
function freeform(game: Game): Freeform {
|
|
const question = game.truthtrance?.question;
|
|
assert.equal(question?.kind, 'freeform');
|
|
return question as Freeform;
|
|
}
|
|
|
|
for (const expected of ['yes','no'] as const) for (const kind of ['spice','hand'] as const) {
|
|
void test(`recognized ${kind} freeform yields verified ${expected} for humans and all bots`, () => {
|
|
const initial = knowledgeGame();
|
|
const target = initial.players.find(player => player.id === 'b')!;
|
|
let text: string, fact: Stamp['fact'];
|
|
if (kind === 'spice') {
|
|
target.spice = expected === 'yes' ? 5 : 4;
|
|
text = 'Do you have at least 5 spice?';
|
|
fact = {kind:'spice', compare:'gte', value:5};
|
|
} else {
|
|
const shield = [...initial.deck, ...initial.players.flatMap(player => player.hand)]
|
|
.find(item => item.name === 'Shield'); assert.ok(shield);
|
|
moveSemutaEmperorCard(initial, shield.id, expected === 'yes' ? target.id : 'f');
|
|
// No duplicated physical cards and no other Shield face in this target's hand.
|
|
if (expected === 'no') {
|
|
for (const card of [...target.hand].filter(item => item.name === 'Shield'))
|
|
moveSemutaEmperorCard(initial, card.id, 'f');
|
|
}
|
|
text = 'Do you have a Shield?';
|
|
fact = {kind:'hand', name:'Shield'};
|
|
}
|
|
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: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'});
|
|
rejectKnowledge(pending, target.id, {type:'truthAnswer', answer:'unknown'});
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(pending,target.id);
|
|
view.players.find(player => player.id === target.id)!.bot = difficulty;
|
|
assert.deepEqual(botActions(view), [{type:'truthAnswer', answer:expected}]);
|
|
const done = applyAction(structuredClone(pending), target.id, {type:'truthAnswer', answer:expected});
|
|
assert.equal(done.truthtrance, null);
|
|
assert.equal(done.truthHistory!.at(-1)!.answer, expected);
|
|
assert.equal(done.discard.filter(item => item.id === card.id).length,1);
|
|
assert.deepEqual(knowledgePhysical(done), physical);
|
|
}
|
|
});
|
|
}
|
|
|
|
void test('only server interpretation is used when a client forges a stamp', () => {
|
|
const initial = knowledgeGame();
|
|
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: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',
|
|
text:'Will you win two battles?', interpreted:{version:1, fact:{kind:'spice', compare:'gte', value:0}}});
|
|
assert.equal(freeform(unsupported).interpreted, undefined);
|
|
});
|
|
|
|
void test('unsupported and current-turn prose retain previous freeform behavior', () => {
|
|
for (const question of [
|
|
{kind:'freeform', target:'b', scope:'fact', text:'Will you win two battles?'},
|
|
{kind:'freeform', target:'b', scope:'currentTurn', text:'Do you have at least 5 spice?'},
|
|
] as Freeform[]) {
|
|
const initial = knowledgeGame();
|
|
const pending = ask(initial, question);
|
|
assert.equal(freeform(pending).interpreted, undefined);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(pending,'b');
|
|
view.players.find(player => player.id === 'b')!.bot = difficulty;
|
|
assert.deepEqual(botActions(view), [{type:'truthAnswer',answer:'unknown'}]);
|
|
}
|
|
const human = applyAction(pending,'b',{type:'truthAnswer',answer:'yes'});
|
|
assert.equal(human.truthHistory!.at(-1)!.answer,'yes');
|
|
assert.equal(human.truthtrance,null);
|
|
}
|
|
});
|
|
|
|
void test('already-pending unstamped legacy freeform is not reinterpreted on answer', () => {
|
|
const initial = knowledgeGame();
|
|
initial.players.find(player => player.id === 'b')!.spice = 4;
|
|
const legacy = ask(initial, {kind:'freeform', target:'b', scope:'fact', text:'Do you have at least 5 spice?'});
|
|
const pendingQuestion = freeform(legacy);
|
|
delete pendingQuestion.interpreted;
|
|
assert.equal(viewGame(legacy,'b').truthAnswer, null);
|
|
const answered = applyAction(legacy,'b',{type:'truthAnswer',answer:'yes'});
|
|
const recorded = answered.truthHistory!.at(-1)!.question as Freeform;
|
|
assert.equal(recorded.interpreted,undefined);
|
|
assert.equal(answered.truthHistory!.at(-1)!.answer,'yes');
|
|
});
|