Support Truthtrance on Atreides spice foresight
This commit is contained in:
1 parent
24255905ed
commit
235c2eb03f
11 files changed
+255
-16
No files matched your search
@@ -3,10 +3,13 @@ import assert from 'node:assert/strict';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { KnowledgeFactFields } from '../components/truthtrance-knowledge';
|
||||
import { TruthFactFields } from '../components/mixed-shipment-fields';
|
||||
import { TruthFactExpressionFields } from '../components/truth-fact-expression-fields';
|
||||
import { knowledgeFactInputError } from '../game/truthtrance-knowledge';
|
||||
import { createGame, joinGame, newPlayer, viewGame } from '../game/engine';
|
||||
import type { TruthFact } from '../game/truthtrance';
|
||||
|
||||
function markup(value: Extract<TruthFact, { kind: 'prediction' | 'stormDial' | 'stormForecast' }>) {
|
||||
function markup(value: Extract<TruthFact, { kind: 'prediction' | 'stormDial' | 'stormForecast' | 'spicePeek' }>) {
|
||||
return renderToStaticMarkup(createElement(KnowledgeFactFields, { value, onChange() {} }));
|
||||
}
|
||||
|
||||
@@ -33,6 +36,30 @@ void test('knowledge controls expose full public faction registry and bounded st
|
||||
assert.match(forecast, /If no such value is available/);
|
||||
});
|
||||
|
||||
void test('spice peek controls offer public card identities with private-knowledge guidance', () => {
|
||||
const markupText = markup({ kind: 'spicePeek', value: 'worm' });
|
||||
assert.match(markupText, /value="worm"/);
|
||||
assert.match(markupText, /Sandworm/);
|
||||
assert.match(markupText, /value="sandtrout"/);
|
||||
assert.match(markupText, /value="the_great_flat"/);
|
||||
assert.match(markupText, /private Spice Blow peek/);
|
||||
assert.match(markupText, /Only the answer is published/);
|
||||
});
|
||||
|
||||
void test('standalone and compound fact editors both expose the spice peek question', () => {
|
||||
const game = createGame('PEEKCONTROLS', newPlayer('a', 'Asker', 'emperor'));
|
||||
joinGame(game, newPlayer('b', 'Target', 'atreides'));
|
||||
const view = viewGame(game, 'a');
|
||||
const fact: TruthFact = { kind: 'spicePeek', value: 'broken_land' };
|
||||
const single = renderToStaticMarkup(createElement(TruthFactFields, { value: fact, game: view, target: 'b', onChange() {} }));
|
||||
assert.match(single, /value="spicePeek" selected/);
|
||||
assert.match(single, /value="broken_land" selected/);
|
||||
const compound: TruthFact = { kind: 'and', terms: [fact, { kind: 'spice', compare: 'gte', value: 3 }] };
|
||||
const grouped = renderToStaticMarkup(createElement(TruthFactExpressionFields, { value: compound, game: view, target: 'b', disabled: false, onChange() {} }));
|
||||
assert.match(grouped, /Known next Spice Blow card/);
|
||||
assert.match(grouped, /value="broken_land" selected/);
|
||||
});
|
||||
|
||||
void test('knowledge helper rejects invalid stored bounds', () => {
|
||||
assert.ok(knowledgeFactInputError({ kind: 'prediction', field: 'turn', compare: 'eq', value: 0 }));
|
||||
assert.ok(knowledgeFactInputError({ kind: 'prediction', field: 'turn', compare: 'eq', value: 11 }));
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, newPlayer } from '../game/engine';
|
||||
import { SPICE_CARDS } from '../game/cards';
|
||||
import { truthFactAnswer } from '../game/truthtrance';
|
||||
import {
|
||||
SPICE_PEEK_VALUES, knowledgeFactAnswer, knowledgeFactText,
|
||||
parseKnowledgeFact, truthKnowledgeOf,
|
||||
} from '../game/truthtrance-knowledge';
|
||||
|
||||
void test('Spice knowledge questions accept only printed territories, worm and Sandtrout', () => {
|
||||
assert.deepEqual(SPICE_PEEK_VALUES, ['worm', 'sandtrout', ...SPICE_CARDS.map(([id]) => id)]);
|
||||
for (const value of SPICE_PEEK_VALUES)
|
||||
assert.deepEqual(parseKnowledgeFact({ kind: 'spicePeek', value, extra: 'discarded' }), { kind: 'spicePeek', value });
|
||||
for (const value of ['', 'arrakeen', 'unknown', 7, null, {}, ['worm']])
|
||||
assert.throws(() => parseKnowledgeFact({ kind: 'spicePeek', value }));
|
||||
assert.match(knowledgeFactText({ kind: 'spicePeek', value: 'worm' }), /sandworm/);
|
||||
assert.match(knowledgeFactText({ kind: 'spicePeek', value: 'hagga_basin' }), /Hagga Basin/);
|
||||
});
|
||||
|
||||
void test('only the Atreides respondent with a current successful peek can answer', () => {
|
||||
const g = createGame('PEEKTEST', newPlayer('a', 'Atreides', 'atreides'));
|
||||
const atreides = g.players[0];
|
||||
const other = newPlayer('other', 'Other', 'guild');
|
||||
g.phase = 5;
|
||||
g.spicePeekKnown = true;
|
||||
const fact = { kind: 'spicePeek', value: 'hagga_basin' } as const;
|
||||
g.spiceDeck = [{ territory: 'hagga_basin', amount: 6, sector: 12 }];
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(g, atreides)), 'yes');
|
||||
assert.equal(knowledgeFactAnswer({ kind: 'spicePeek', value: 'worm' }, truthKnowledgeOf(g, atreides)), 'no');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf(g, other)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...g, spicePeekKnown: false }, atreides)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...g, phase: 6 }, atreides)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact, truthKnowledgeOf({ ...g, spiceDeck: [] }, atreides)), 'unknown');
|
||||
assert.equal(knowledgeFactAnswer(fact), 'unknown');
|
||||
|
||||
// Private deck access itself is fenced when this respondent is not entitled.
|
||||
const hidden = { ...g, get spiceDeck(): never { throw new Error('Private deck read'); } };
|
||||
assert.equal(truthKnowledgeOf(hidden, other).spicePeek, null);
|
||||
hidden.phase = 6;
|
||||
assert.equal(truthKnowledgeOf(hidden, atreides).spicePeek, null);
|
||||
});
|
||||
|
||||
void test('worm, Sandtrout and discovery blows stay distinct and compose with other facts', () => {
|
||||
const g = createGame('PEEKTEST', newPlayer('a', 'Atreides', 'atreides'));
|
||||
const p = g.players[0];
|
||||
Object.assign(g, { phase: 5, spicePeekKnown: true, spiceDeck: [{ worm: true }] });
|
||||
const worm = { kind: 'spicePeek', value: 'worm' } as const;
|
||||
assert.equal(knowledgeFactAnswer(worm, truthKnowledgeOf(g, p)), 'yes');
|
||||
g.spiceDeck = [{ sandtrout: true }];
|
||||
assert.equal(knowledgeFactAnswer(worm, truthKnowledgeOf(g, p)), 'no');
|
||||
assert.equal(knowledgeFactAnswer({ kind: 'spicePeek', value: 'sandtrout' }, truthKnowledgeOf(g, p)), 'yes');
|
||||
g.spiceDeck = [{ territory: 'hagga_basin', amount: 6, sector: 12, discovery: 'discovery-hagga-basin' }];
|
||||
const knowledge = truthKnowledgeOf(g, p);
|
||||
const blow = { kind: 'spicePeek', value: 'hagga_basin' } as const;
|
||||
assert.equal(knowledgeFactAnswer(blow, knowledge), 'yes');
|
||||
assert.equal(truthFactAnswer(p, { kind: 'and', terms: [blow, { kind: 'spice', compare: 'gte', value: 0 }] }, knowledge), 'yes');
|
||||
assert.equal(truthFactAnswer(p, { kind: 'or', terms: [worm, { kind: 'spice', compare: 'gte', value: 0 }] }, knowledge), 'yes');
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { knowledgeGame, knowledgeBotActions, askKnowledge, knowledgePhysical, rejectKnowledge } from './truthtrance-knowledge-fixture';
|
||||
|
||||
/** Native setup and phase progression; only the holder's Truthtrance acquisition
|
||||
* is staged by knowledgeGame, with each physical card retained exactly once. */
|
||||
function withPeek(advanced = false) {
|
||||
let g = knowledgeGame(advanced);
|
||||
for (let n = 0; n < 300 && !(g.phase === 5 && g.spicePeekKnown && !g.response && !g.phaseOpening); n++) {
|
||||
const candidate = g.players.flatMap(player => knowledgeBotActions(g, player.id)
|
||||
.map(action => ({ id: player.id, action })))[0];
|
||||
assert.ok(candidate, `Native progression needs a legal decision in phase ${g.phase}`);
|
||||
g = applyAction(g, candidate.id, candidate.action);
|
||||
}
|
||||
assert.equal(g.phase, 5);
|
||||
assert.equal(g.spicePeekKnown, true);
|
||||
assert.ok(g.spiceDeck[0]);
|
||||
return g;
|
||||
}
|
||||
|
||||
void test('actual Atreides peek supplies a private Truthtrance answer and native AI continuation', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const initial = withPeek(advanced);
|
||||
const card = initial.spiceDeck[0];
|
||||
const value = 'territory' in card ? card.territory : 'worm' in card ? 'worm' : 'sandtrout';
|
||||
const question = { kind: 'spicePeek' as const, value };
|
||||
const g = askKnowledge(initial, 'o', question);
|
||||
assert.equal(viewGame(g, 'o').truthAnswer, 'yes');
|
||||
assert.deepEqual(viewGame(g, 'o').spicePeek, card);
|
||||
for (const id of ['a', 'b', 'f']) {
|
||||
assert.equal(viewGame(g, id).truthAnswer, null);
|
||||
assert.equal(viewGame(g, id).spicePeek, null);
|
||||
}
|
||||
rejectKnowledge(g, 'o', { type: 'truthAnswer', answer: 'no' });
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(g, 'o');
|
||||
view.players.find(player => player.id === 'o')!.bot = difficulty;
|
||||
const actions = botActions(view);
|
||||
assert.deepEqual(actions, [{ type: 'truthAnswer', answer: 'yes' }]);
|
||||
const done = applyAction(g, 'o', actions[0]);
|
||||
assert.equal(done.truthtrance, null);
|
||||
assert.equal(done.truthHistory!.at(-1)!.answer, 'yes');
|
||||
assert.deepEqual(done.spiceDeck[0], card);
|
||||
assert.deepEqual(knowledgePhysical(done), knowledgePhysical(initial));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('a canceled peek yields unknown and retains the physical Truthtrance', () => {
|
||||
const initial = withPeek();
|
||||
initial.spicePeekKnown = false;
|
||||
const g = askKnowledge(initial, 'o', { kind: 'spicePeek', value: 'worm' });
|
||||
assert.equal(viewGame(g, 'o').truthAnswer, 'unknown');
|
||||
rejectKnowledge(g, 'o', { type: 'truthAnswer', answer: 'yes' });
|
||||
const done = applyAction(g, 'o', { type: 'truthAnswer', answer: 'unknown' });
|
||||
assert.equal(done.truthtrance?.stage, 'unknown');
|
||||
assert.deepEqual(knowledgePhysical(done), knowledgePhysical(initial));
|
||||
});
|
||||
Reference in new issue
Block a user