Expand freeform Truthtrance current facts
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
a10e019b01
commit
cc9cc0945d
10 files changed
+290
-4
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseFreeformFactV6 } from '../game/truthtrance-freeform-facts-v6';
|
||||
import type { KnowledgeFact } from '../game/truthtrance-knowledge';
|
||||
import type { ForceCountFact } from '../game/truthtrance-force-count';
|
||||
|
||||
void test('v6 recognizes named stored predictions and current forecast questions', () => {
|
||||
const cases: Array<[string, KnowledgeFact]> = [
|
||||
['Did you predict Harkonnen?', {kind:'prediction', field:'faction', faction:'harkonnen'}],
|
||||
['Is your predicted faction Spacing Guild?', {kind:'prediction', field:'faction', faction:'guild'}],
|
||||
['Did you predict Bene Gesserit to win?', {kind:'prediction', field:'faction', faction:'beneGesserit'}],
|
||||
['Is your predicted turn at least 5?', {kind:'prediction', field:'turn', compare:'gte', value:5}],
|
||||
['Is your storm forecast exactly 3?', {kind:'stormForecast', compare:'eq', value:3}],
|
||||
['Is your submitted storm dial at most 4?', {kind:'stormDial', compare:'lte', value:4}],
|
||||
['Is your storm dial exactly 0?', {kind:'stormDial', compare:'eq', value:0}],
|
||||
['Is the next Spice Blow a sandworm?', {kind:'spicePeek', value:'worm'}],
|
||||
['Is the next Spice Blow Sandtrout?', {kind:'spicePeek', value:'sandtrout'}],
|
||||
['Is the next Spice Blow in The Great Flat?', {kind:'spicePeek', value:'the_great_flat'}],
|
||||
['Is the next Spice Blow at Great Flat?', {kind:'spicePeek', value:'the_great_flat'}],
|
||||
];
|
||||
for (const [text, expected] of cases) assert.deepEqual(parseFreeformFactV6(text), expected, text);
|
||||
});
|
||||
|
||||
void test('v6 maps explicit current force pools to total physical counter facts', () => {
|
||||
const cases: Array<[string, ForceCountFact]> = [
|
||||
['Do you have at least 3 forces in your reserves?', {kind:'forceCount', zone:{kind:'reserves'}, counter:'total', compare:'gte', value:3}],
|
||||
['Do you have exactly 2 forces in the Tanks?', {kind:'forceCount', zone:{kind:'tanks'}, counter:'total', compare:'eq', value:2}],
|
||||
['Do you have at most 5 physical forces on the board?', {kind:'forceCount', zone:{kind:'board'}, counter:'total', compare:'lte', value:5}],
|
||||
['Do you have exactly 1 physical force in reserves?', {kind:'forceCount', zone:{kind:'reserves'}, counter:'total', compare:'eq', value:1}],
|
||||
];
|
||||
for (const [text, expected] of cases) assert.deepEqual(parseFreeformFactV6(text), expected, text);
|
||||
});
|
||||
|
||||
void test('v6 leaves ambiguous, invalid, future and unsupported sentences manual', () => {
|
||||
for (const text of [
|
||||
'Do you have a Shield?', 'Will you predict Harkonnen?', 'Did you predict Arrakeen?',
|
||||
'Is your predicted turn exactly 0?', 'Is your predicted turn exactly 11?',
|
||||
'Is your storm forecast at least 0?', 'Is your storm forecast exactly 7?',
|
||||
'Is your storm dial exactly 21?', 'Is your storm dial exactly -1?',
|
||||
'Is your storm dial exactly 1.5?', 'Is your storm dial exactly 9007199254740992?',
|
||||
'Is the next Spice Blow in Arrakeen?', 'Is the next Spice Blow a sandworm or Sandtrout?',
|
||||
'Do you have exactly -1 forces in your reserves?', 'Do you have exactly 1.5 forces in your reserves?',
|
||||
'Do you have at most 9007199254740992 forces on the board?',
|
||||
'Will you have 3 forces in reserves?', 'Do you have exactly 2 forces?',
|
||||
'Do you have exactly 2 advisors on the board?', 'Do you have exactly 2 forces on the board this turn?',
|
||||
'Is your predicted faction Harkonnen??', 'Is your storm forecast exactly 3?\nExplain',
|
||||
'Is your storm forecast exactly 3?'.padEnd(501, ' '),
|
||||
]) assert.equal(parseFreeformFactV6(text), null, text);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { FreeformFactPreview } from '../components/truthtrance';
|
||||
|
||||
const base = { scope: 'fact' as const, target: 'target',
|
||||
leaderName: (id: string) => id, locationName: (id: string) => id, playerName: (id: string) => id };
|
||||
const preview = (scope: 'fact' | 'currentTurn', text: string) =>
|
||||
renderToStaticMarkup(createElement(FreeformFactPreview, { ...base, scope, text }));
|
||||
|
||||
void test('the editor previews v6 knowledge and physical force facts only for current facts', () => {
|
||||
const prediction = preview('fact', 'Did you predict Harkonnen?');
|
||||
assert.match(prediction, /Recognized freeform question/);
|
||||
assert.match(prediction, /your recorded Bene Gesserit prediction names Harkonnen/);
|
||||
const forces = preview('fact', 'Do you have at least 3 forces in your reserves?');
|
||||
assert.match(forces, /Recognized freeform question/);
|
||||
assert.match(forces, /currently have at least 3 physical forces in your reserves/);
|
||||
const forecast = preview('fact', 'Is your storm forecast exactly 3?');
|
||||
assert.match(forecast, /Recognized freeform question/);
|
||||
assert.match(forecast, /storm card currently known to you as Fremen shows exactly 3 sectors/);
|
||||
const spice = preview('fact', 'Is the next Spice Blow a sandworm?');
|
||||
assert.match(spice, /Recognized freeform question/);
|
||||
assert.match(spice, /next Spice Card currently known to you as Atreides is a sandworm/);
|
||||
for (const text of ['Did you predict Harkonnen?', 'Do you have at least 3 forces in your reserves?']) {
|
||||
const manual = preview('currentTurn', text);
|
||||
assert.match(manual, /not automatically interpreted/);
|
||||
assert.doesNotMatch(manual, /Recognized freeform question/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
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 { knowledgeGame, knownForecastGame, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
|
||||
/** Native setup and prediction; the fixture stages Truthtrance acquisition. */
|
||||
function ask(initial: Game, target: string, text: string, scope: 'fact' | 'currentTurn' = 'fact'): Game {
|
||||
const asker = initial.players[0];
|
||||
const card = asker.hand.find(candidate => candidate.effect === 'truthtrance'); assert.ok(card);
|
||||
let game = applyAction(initial, asker.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, asker.id, { type: 'truthAsk', question: { kind: 'freeform', target, scope, text } });
|
||||
}
|
||||
function question(game: Game) {
|
||||
const value = game.truthtrance?.question;
|
||||
assert.equal(value?.kind, 'freeform');
|
||||
if (value?.kind !== 'freeform') throw Error('Expected a freeform question');
|
||||
return value;
|
||||
}
|
||||
function assertPrivateAndBots(game: Game, target: string, answer: 'yes' | 'no' | 'unknown') {
|
||||
assert.equal(question(game).interpreted?.version, 6);
|
||||
assert.equal(viewGame(game, target).truthAnswer, answer);
|
||||
for (const observer of game.players.filter(player => player.id !== target))
|
||||
assert.equal(viewGame(game, observer.id).truthAnswer, null);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, target); view.players.find(player => player.id === target)!.bot = difficulty;
|
||||
assert.deepEqual(botActions(view), [{ type: 'truthAnswer', answer }]);
|
||||
}
|
||||
for (const incorrect of (['yes', 'no', 'unknown'] as const).filter(value => value !== answer))
|
||||
rejectKnowledge(game, target, { type: 'truthAnswer', answer: incorrect });
|
||||
}
|
||||
|
||||
void test('V6 prediction questions check private current knowledge and all bot answers', () => {
|
||||
for (const [text, answer] of [['Did you predict Guild?', 'yes'], ['Did you predict Harkonnen?', 'no']] as const) {
|
||||
const initial = knowledgeGame(), physical = knowledgePhysical(initial);
|
||||
const game = ask(initial, 'b', text); assertPrivateAndBots(game, 'b', answer);
|
||||
assert.equal('prediction' in viewGame(game, 'a').players.find(player => player.id === 'b')!, false);
|
||||
const restored = JSON.parse(JSON.stringify(game)) as Game;
|
||||
const done = applyAction(restored, 'b', { type: 'truthAnswer', answer });
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, answer);
|
||||
assert.deepEqual(knowledgePhysical(done), physical);
|
||||
}
|
||||
});
|
||||
|
||||
void test('V6 unknown knowledge stays private and retains Truthtrance for another question', () => {
|
||||
const initial = knowledgeGame(), before = knowledgePhysical(initial);
|
||||
const game = ask(initial, 'o', 'Did you predict Harkonnen?');
|
||||
assertPrivateAndBots(game, 'o', 'unknown');
|
||||
const done = applyAction(game, 'o', { type: 'truthAnswer', answer: 'unknown' });
|
||||
assert.equal(done.truthtrance?.stage, 'unknown');
|
||||
assert.deepEqual(knowledgePhysical(done), before);
|
||||
});
|
||||
|
||||
void test('V6 uses the native Fremen storm forecast without revealing it to other seats', () => {
|
||||
const initial = knownForecastGame(); assert.ok(initial.stormCard);
|
||||
const game = ask(initial, 'f', `Is your storm forecast exactly ${initial.stormCard}?`);
|
||||
assertPrivateAndBots(game, 'f', 'yes');
|
||||
for (const id of ['a', 'b', 'o']) assert.equal(viewGame(game, id).stormForecast, null);
|
||||
const done = applyAction(game, 'f', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.equal(done.stormCard, initial.stormCard);
|
||||
});
|
||||
|
||||
void test('V6 reads the current Atreides Spice forecast only for its entitled owner', () => {
|
||||
const initial = knowledgeGame();
|
||||
// Stage the Movement forecast and choose an existing undealt worm without
|
||||
// changing the physical Spice deck; acquisition itself is not claimed.
|
||||
initial.phase = 5; initial.active = 'o'; initial.spicePeekKnown = true;
|
||||
const worm = initial.spiceDeck.findIndex(card => 'worm' in card && card.worm);
|
||||
assert.ok(worm >= 0);
|
||||
[initial.spiceDeck[0], initial.spiceDeck[worm]] = [initial.spiceDeck[worm], initial.spiceDeck[0]];
|
||||
const game = ask(initial, 'o', 'Is the next Spice Blow a sandworm?');
|
||||
assertPrivateAndBots(game, 'o', 'yes');
|
||||
const done = applyAction(game, 'o', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.deepEqual(done.spiceDeck, initial.spiceDeck);
|
||||
});
|
||||
|
||||
void test('V6 checks physical reserve forces using the existing force-count fact', () => {
|
||||
const initial = knowledgeGame(), target = initial.players.find(player => player.id === 'b')!;
|
||||
assert.ok(target.reserves >= 3);
|
||||
const game = ask(initial, 'b', 'Do you have at least 3 forces in your reserves?');
|
||||
assertPrivateAndBots(game, 'b', 'yes');
|
||||
const done = applyAction(game, 'b', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.equal(done.players.find(player => player.id === 'b')!.reserves, target.reserves);
|
||||
});
|
||||
|
||||
void test('older unstamped and current-turn questions keep their existing manual semantics', () => {
|
||||
const text = 'Did you predict Harkonnen?';
|
||||
const future = ask(knowledgeGame(), 'b', text, 'currentTurn');
|
||||
assert.equal(question(future).interpreted, undefined);
|
||||
const old = ask(knowledgeGame(), 'b', text);
|
||||
delete question(old).interpreted;
|
||||
assert.equal(viewGame(old, 'b').truthAnswer, null);
|
||||
const done = applyAction(old, 'b', { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, 'yes');
|
||||
});
|
||||
|
||||
void test('a modified V6 receipt cannot change the interpreted fact', () => {
|
||||
const game = ask(knowledgeGame(), 'b', 'Did you predict Harkonnen?');
|
||||
const q = question(game); assert.equal(q.interpreted?.version, 6);
|
||||
if (!q.interpreted) throw Error('Expected a recognized receipt');
|
||||
q.interpreted.fact = { kind: 'prediction', field: 'faction', faction: 'guild' };
|
||||
assert.throws(() => viewGame(game, 'b'));
|
||||
assert.throws(() => applyAction(game, 'b', { type: 'truthAnswer', answer: 'yes' }));
|
||||
});
|
||||
Reference in new issue
Block a user