Add freeform No-Field and auction 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
593ff33ca8
commit
152476c348
10 files changed
+387
-4
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseTruthFreeformFactsV8 as parse } from '../game/truthtrance-freeform-facts-v8';
|
||||
|
||||
void test('v8 recognizes current concealed values and actual recorded auction facts', () => {
|
||||
for (const value of [0,3,5] as const)
|
||||
assert.deepEqual(parse(`Do you have a concealed No-Field with printed value ${value}?`), {kind:'noFieldValue', value});
|
||||
assert.deepEqual(parse('Is the current auction card you inspected a Shield?'), {kind:'auctionPeek', name:'Shield'});
|
||||
assert.deepEqual(parse('Is the current public cache card a shield?'), {kind:'cacheLot', name:'Shield'});
|
||||
assert.deepEqual(parse('Does your recorded Ixian auction group contain a Shield?'), {kind:'ixPoolPeek', name:'Shield'});
|
||||
});
|
||||
|
||||
void test('v8 composes new and previous atoms in a uniquely interpreted expression', () => {
|
||||
assert.deepEqual(parse('Did you predict Guild and is the current auction card you inspected a Shield?'), {
|
||||
kind:'and', terms:[{kind:'prediction',field:'faction',faction:'guild'}, {kind:'auctionPeek',name:'Shield'}],
|
||||
});
|
||||
assert.deepEqual(parse('Do you have a concealed No-Field with printed value 3 and at least 3 forces in your reserves?'), {
|
||||
kind:'and', terms:[{kind:'noFieldValue',value:3}, {kind:'forceCount',zone:{kind:'reserves'},counter:'total',compare:'gte',value:3}],
|
||||
});
|
||||
assert.deepEqual(parse('Is the current public cache card a Shield or does your recorded Ixian auction group contain a Snooper?'), {
|
||||
kind:'or', terms:[{kind:'cacheLot',name:'Shield'}, {kind:'ixPoolPeek',name:'Snooper'}],
|
||||
});
|
||||
const repeated = ['Is the current public cache card a Shield', ...Array(7).fill('do you have a Snooper')].join(' and ');
|
||||
const result = parse(repeated);
|
||||
assert.equal(result?.kind, 'and');
|
||||
if (result?.kind === 'and') assert.equal(result.terms.length, 8);
|
||||
assert.equal(parse(`${repeated} and do you have a Shield`), null);
|
||||
});
|
||||
|
||||
void test('v8 preserves prior versions and refuses future, ambiguous or invalid wording', () => {
|
||||
for (const text of [
|
||||
'Do you have a Shield?',
|
||||
'Did you predict Guild and do you have at least 3 forces in your reserves?',
|
||||
'Do you have a concealed No-Field with printed value 4?',
|
||||
'Do you have a concealed No-Field with printed value 03?',
|
||||
'Is the next auction card a Shield?',
|
||||
'Is the current auction card you inspected an unknown card?',
|
||||
'Does your auction group contain a Shield?',
|
||||
'Is the current public cache card a Shield and do you have a Snooper or a Shield?',
|
||||
'Is the current public cache card a Shield??',
|
||||
'Is the current public cache card a Shield?\nAnswer now',
|
||||
`Is the current public cache card a Shield ${'and '.repeat(25)}do you have a Snooper`,
|
||||
`Is the current public cache card ${'x'.repeat(501)}`,
|
||||
]) assert.equal(parse(text), null, text);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
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 props = {
|
||||
target: 'target', leaderName: (id: string) => id,
|
||||
locationName: (id: string) => id, playerName: (id: string) => id,
|
||||
};
|
||||
const preview = (text: string, scope: 'fact' | 'currentTurn' = 'fact') =>
|
||||
renderToStaticMarkup(createElement(FreeformFactPreview, { ...props, scope, text }));
|
||||
|
||||
void test('the editor previews No-Field and auction knowledge without revealing an answer', () => {
|
||||
const noField = preview('Do you have a concealed No-Field with printed value 3?');
|
||||
assert.match(noField, /Recognized freeform question/);
|
||||
assert.match(noField, /you currently have a concealed No-Field with printed value 3/);
|
||||
const auction = preview('Is the current auction card you inspected a Shield?');
|
||||
assert.match(auction, /Recognized freeform question/);
|
||||
assert.match(auction, /the current auction card you inspected as Atreides is Shield/);
|
||||
assert.doesNotMatch(auction, /Your current game information establishes/);
|
||||
});
|
||||
|
||||
void test('the editor previews a mixed auction and force aggregate', () => {
|
||||
const text = 'Is the current auction card you inspected a Shield and do you have at least 3 forces in your reserves?';
|
||||
const html = preview(text);
|
||||
assert.match(html, /Recognized freeform question/);
|
||||
assert.match(html, /the current auction card you inspected as Atreides is Shield/);
|
||||
assert.match(html, /AND/);
|
||||
assert.match(html, /currently have at least 3 physical forces in your reserves/);
|
||||
assert.doesNotMatch(html, /Your current game information establishes/);
|
||||
assert.match(preview(text, 'currentTurn'), /not automatically interpreted/);
|
||||
});
|
||||
|
||||
void test('the editor leaves invalid or predictive inspection questions player-managed', () => {
|
||||
for (const text of [
|
||||
'Do you have a concealed No-Field with printed value 2?',
|
||||
'Will the next auction card be a Shield?',
|
||||
]) {
|
||||
const html = preview(text);
|
||||
assert.match(html, /not automatically interpreted/);
|
||||
assert.doesNotMatch(html, /Recognized freeform question/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
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 { TRUTH_CARD_NAMES } from '../game/truthtrance-card-names';
|
||||
import { knowledgeBotActions, knowledgeGame, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
import { createTruthtranceNoFieldFixture, declareNoFieldTruthtrance,
|
||||
passNoFieldTruthPriority, truthtranceNoFieldCards, truthtranceNoFieldPlayer } from './fixture-truthtrance-no-field';
|
||||
import { advanceTruthMovementNativeEarly, createTruthMovementNativeEarlyFixture,
|
||||
truthMovementNativeEarlyInventory } from './fixture-truth-movement-native-early';
|
||||
|
||||
function ask(game: Game, asker: string, target: string, text: string, scope: 'fact' | 'currentTurn' = 'fact') {
|
||||
const waiting = passNoFieldTruthPriority(declareNoFieldTruthtrance(game, asker));
|
||||
return applyAction(waiting, asker, { type: 'truthAsk', question: { kind: 'freeform', target, scope, text } });
|
||||
}
|
||||
function question(game: Game) {
|
||||
const q = game.truthtrance?.question;
|
||||
assert.equal(q?.kind, 'freeform');
|
||||
if (q?.kind !== 'freeform') throw Error('Expected a freeform question');
|
||||
return q;
|
||||
}
|
||||
function verify(game: Game, target: string, answer: 'yes' | 'no' | 'unknown') {
|
||||
assert.equal(question(game).interpreted?.version, 8);
|
||||
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 wrong of (['yes', 'no', 'unknown'] as const).filter(value => value !== answer))
|
||||
rejectKnowledge(game, target, { type: 'truthAnswer', answer: wrong });
|
||||
const restored = JSON.parse(JSON.stringify(game)) as Game;
|
||||
const done = applyAction(restored, target, { type: 'truthAnswer', answer });
|
||||
if (answer === 'unknown') assert.equal(done.truthtrance?.stage, 'unknown');
|
||||
else assert.equal(done.truthHistory!.at(-1)!.answer, answer);
|
||||
return done;
|
||||
}
|
||||
|
||||
for (const [wording, answer] of [
|
||||
['Do you have a concealed No-Field with printed value 3?', 'yes'],
|
||||
['Do you have a concealed No-Field with printed value 5?', 'no'],
|
||||
['Do you have a concealed No-Field with printed value 3 and do you have at least 0 forces in your reserves?', 'yes'],
|
||||
] as const) {
|
||||
void test(`V8 checks the existing concealed marker privately: ${answer}`, () => {
|
||||
const f = createTruthtranceNoFieldFixture({ marker: 3 });
|
||||
const before = structuredClone(truthtranceNoFieldPlayer(f.game, f.owner).noField);
|
||||
const cards = truthtranceNoFieldCards(f.game);
|
||||
const game = ask(f.game, f.asker, f.owner, wording);
|
||||
for (const player of game.players.filter(player => player.id !== f.owner))
|
||||
assert.equal(viewGame(game, player.id).richeseNoField!.private, null);
|
||||
const done = verify(game, f.owner, answer);
|
||||
assert.deepEqual(truthtranceNoFieldPlayer(done, f.owner).noField, before);
|
||||
assert.deepEqual(truthtranceNoFieldCards(done), cards);
|
||||
});
|
||||
}
|
||||
|
||||
void test('V8 asks about absent No-Field custody without inferring a printed zero', () => {
|
||||
const f = createTruthtranceNoFieldFixture({ marker: 3, boundary: 'beforeShipment' });
|
||||
const game = ask(f.game, f.asker, f.owner, 'Do you have a concealed No-Field with printed value 0?');
|
||||
verify(game, f.owner, 'no');
|
||||
});
|
||||
|
||||
/** Real setup and ordinary auction progression; the shared fixture stages
|
||||
* physical Truthtrance acquisition without inventing a card. */
|
||||
function inspectedAuction() {
|
||||
let game = knowledgeGame();
|
||||
for (let n = 0; n < 200 && !(game.phase === 3 && game.auction?.peekKnown && !game.response && !game.phaseOpening); n++) {
|
||||
const candidate = game.players.flatMap(player => knowledgeBotActions(game, player.id)
|
||||
.map(action => ({ id: player.id, action })))[0];
|
||||
assert.ok(candidate, `Expected a native auction action in phase ${game.phase}`);
|
||||
game = applyAction(game, candidate.id, candidate.action);
|
||||
}
|
||||
assert.equal(game.phase, 3);
|
||||
assert.equal(game.auction?.peekKnown, true);
|
||||
assert.ok(game.auction?.cards[game.auction.index]);
|
||||
return game;
|
||||
}
|
||||
|
||||
void test('V8 checks the actual inspected auction face privately', () => {
|
||||
const initial = inspectedAuction(), card = initial.auction!.cards[initial.auction!.index];
|
||||
for (const [name, answer] of [[card.name, 'yes'], [card.name === 'Shield' ? 'Snooper' : 'Shield', 'no']] as const) {
|
||||
const game = ask(structuredClone(initial), 'a', 'o', `Is the current auction card you inspected a ${name}?`);
|
||||
for (const id of ['a', 'b', 'f']) assert.equal(viewGame(game, id).auction?.card, null);
|
||||
const done = verify(game, 'o', answer);
|
||||
assert.equal(done.auction!.cards[done.auction!.index].id, card.id);
|
||||
}
|
||||
const missing = structuredClone(initial);
|
||||
missing.auction!.peekKnown = false;
|
||||
const unknown = ask(missing, 'a', 'o', `Is the current auction card you inspected a ${card.name}?`);
|
||||
verify(unknown, 'o', 'unknown');
|
||||
});
|
||||
|
||||
void test('V8 checks the recorded Ixian group rather than the remaining auction lots', () => {
|
||||
const fixture = createTruthMovementNativeEarlyFixture({ roster: 'ixians-tleilaxu' });
|
||||
let initial = advanceTruthMovementNativeEarly(fixture.game, state =>
|
||||
state.phase === 3 && state.decision?.kind === 'ixAuction' && !state.response);
|
||||
const returned = initial.ixAuction!.cards[0].id;
|
||||
initial = applyAction(initial, 'ixians', { type: 'decision', card: returned, position: 'bottom' });
|
||||
initial = advanceTruthMovementNativeEarly(initial, state =>
|
||||
state.phase === 3 && !!state.auction && !!state.ixAuctionKnown &&
|
||||
!state.response && !state.decision && !state.phaseOpening);
|
||||
const observed = structuredClone(initial.ixAuctionKnown!.cards), inventory = truthMovementNativeEarlyInventory(initial);
|
||||
const present = observed[0].name, absent = TRUTH_CARD_NAMES.find(name => !observed.some(card => card.name === name));
|
||||
assert.ok(absent);
|
||||
for (const [name, answer] of [[present, 'yes'], [absent, 'no']] as const) {
|
||||
const game = ask(structuredClone(initial), 'atreides', 'ixians', `Does your recorded Ixian auction group contain ${name}?`);
|
||||
const done = verify(game, 'ixians', answer);
|
||||
assert.deepEqual(done.ixAuctionKnown!.cards, observed);
|
||||
assert.deepEqual(truthMovementNativeEarlyInventory(done), inventory);
|
||||
}
|
||||
const stale = structuredClone(initial);
|
||||
stale.ixAuctionKnown = null;
|
||||
verify(ask(stale, 'atreides', 'ixians', `Does your recorded Ixian auction group contain ${present}?`), 'ixians', 'unknown');
|
||||
});
|
||||
|
||||
void test('V8 preserves unstamped and current-turn questions and rejects changed receipts', () => {
|
||||
const f = createTruthtranceNoFieldFixture({ marker: 3 });
|
||||
const text = 'Do you have a concealed No-Field with printed value 3?';
|
||||
const future = ask(structuredClone(f.game), f.asker, f.owner, text, 'currentTurn');
|
||||
assert.equal(question(future).interpreted, undefined);
|
||||
const old = ask(structuredClone(f.game), f.asker, f.owner, text);
|
||||
delete question(old).interpreted;
|
||||
assert.equal(viewGame(old, f.owner).truthAnswer, null);
|
||||
const legacy = applyAction(old, f.owner, { type: 'truthAnswer', answer: 'no' });
|
||||
assert.equal(legacy.truthHistory!.at(-1)!.answer, 'no');
|
||||
const changed = ask(structuredClone(f.game), f.asker, f.owner, text);
|
||||
const q = question(changed); assert.equal(q.interpreted?.version, 8);
|
||||
if (!q.interpreted) throw Error('Expected an interpreted question');
|
||||
q.interpreted.fact = { kind: 'noFieldValue', value: 5 };
|
||||
assert.throws(() => viewGame(changed, f.owner));
|
||||
assert.throws(() => applyAction(changed, f.owner, { type: 'truthAnswer', answer: 'yes' }));
|
||||
});
|
||||
Reference in new issue
Block a user