Combine current facts with native Revival
This commit is contained in:
1 parent
6b2dfb84a2
commit
957d5a172f
19 files changed
+1386
-5
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import type { MixedActionExpression } from '../game/mixed-action-question';
|
||||
import type { RevivalExpressionLeaf } from '../game/revival-expression-promises';
|
||||
import type { TruthFact } from '../game/truthtrance';
|
||||
import {
|
||||
actTruthRevivalExpression, advanceTruthRevivalExpression, createTruthRevivalExpressionFixture,
|
||||
declareTruthRevivalExpression, finishTruthRevivalExpression, nextTruthRevivalExpression,
|
||||
settleTruthRevivalExpression, truthRevivalExpressionClean, truthRevivalExpressionInventory,
|
||||
truthRevivalExpressionPlayer, type TruthRevivalExpressionStep,
|
||||
} from './fixture-truth-revival-expression';
|
||||
|
||||
export type TruthMixedRevivalExpression = MixedActionExpression<RevivalExpressionLeaf>;
|
||||
export type TruthMixedRevivalStep = TruthRevivalExpressionStep;
|
||||
export const actTruthMixedRevival = actTruthRevivalExpression;
|
||||
export const advanceTruthMixedRevival = advanceTruthRevivalExpression;
|
||||
export const finishTruthMixedRevival = finishTruthRevivalExpression;
|
||||
export const nextTruthMixedRevival = nextTruthRevivalExpression;
|
||||
export const settleTruthMixedRevival = settleTruthRevivalExpression;
|
||||
export const truthMixedRevivalClean = truthRevivalExpressionClean;
|
||||
export const truthMixedRevivalInventory = truthRevivalExpressionInventory;
|
||||
export const truthMixedRevivalPlayer = truthRevivalExpressionPlayer;
|
||||
|
||||
export const truthMixedRevivalFact = (fact: TruthFact): TruthMixedRevivalExpression => ({ kind: 'fact', fact });
|
||||
export const truthMixedRevivalForce = (minimum = 1, counter: 'total' | 'normal' | 'elite' = 'total'):
|
||||
TruthMixedRevivalExpression => ({ kind: 'action', action: { kind: 'forces', counter, minimum } });
|
||||
export const truthMixedRevivalLeader = (leader: string): TruthMixedRevivalExpression =>
|
||||
({ kind: 'action', action: { kind: 'leader', leader } });
|
||||
export const truthMixedRevivalGroup = (kind: 'and' | 'or', ...terms: TruthMixedRevivalExpression[]):
|
||||
TruthMixedRevivalExpression => ({ kind, terms });
|
||||
|
||||
/** Original public H/A/T Ix47: the inherited paid shipment and actual projectile
|
||||
* kill reach T4, one tank counter and a cost2 personal early Hidar return. This
|
||||
* adapter changes only the raw question. It installs no played state or binding. */
|
||||
export function createTruthMixedRevivalFixture(options: {
|
||||
advanced?: boolean; window?: 'active' | 'waiting'; ghola?: boolean; bothDead?: boolean;
|
||||
mixed?: TruthMixedRevivalExpression;
|
||||
} = {}) {
|
||||
const { mixed: suppliedMixed, ...sourceOptions } = options;
|
||||
const base = createTruthRevivalExpressionFixture(sourceOptions);
|
||||
const spiceFact: TruthFact = { kind: 'spice', compare: 'gte', value: 4 };
|
||||
const falseSpiceFact: TruthFact = { kind: 'spice', compare: 'gte', value: 5 };
|
||||
const forecastFact: TruthFact = { kind: 'stormForecast', compare: 'gte', value: 1 };
|
||||
const mixed = structuredClone(suppliedMixed ?? truthMixedRevivalGroup('and', truthMixedRevivalFact(spiceFact),
|
||||
truthMixedRevivalForce(), truthMixedRevivalLeader(base.leader)));
|
||||
const question = { kind: 'mixedRevival' as const, target: base.target, turn: base.game.turn, mixed };
|
||||
return { ...base, revivalSource: base, mixed, question, spiceFact, falseSpiceFact, forecastFact,
|
||||
questionAction: { type: 'truthAsk', question } satisfies Action,
|
||||
sourceCounts: { inherited: base.sourceActions.length, postStorm: base.postStormSourceActions.length,
|
||||
phase3: base.phase3SourceActions.length },
|
||||
sourceDisclosure: `${base.sourceDisclosure} This thin mixed adapter changes only the original raw question: current facts and future ordinary typed force/named-leader returns share one AND/OR tree. It inherits every original action and freezes no fact or residual itself.` };
|
||||
}
|
||||
export type TruthMixedRevivalFixture = ReturnType<typeof createTruthMixedRevivalFixture>;
|
||||
|
||||
export function declareTruthMixedRevival(f: TruthMixedRevivalFixture, source = f.game,
|
||||
options: { copies?: 1 | 2 } = {}) {
|
||||
return declareTruthRevivalExpression(f.revivalSource, source, options);
|
||||
}
|
||||
export function askTruthMixedRevival(game: Game, asker: string, target: string, mixed: TruthMixedRevivalExpression,
|
||||
actions: TruthMixedRevivalStep[] = []): Game {
|
||||
return actTruthMixedRevival(game, asker, { type: 'truthAsk', question: {
|
||||
kind: 'mixedRevival', target, turn: game.turn, mixed: structuredClone(mixed),
|
||||
} }, actions);
|
||||
}
|
||||
export function answerTruthMixedRevival(game: Game, target: string, answer: 'yes' | 'no',
|
||||
actions: TruthMixedRevivalStep[] = []): Game {
|
||||
return actTruthMixedRevival(game, target, { type: 'truthAnswer', answer }, actions);
|
||||
}
|
||||
export function bindTruthMixedRevival(f: TruthMixedRevivalFixture, answer: 'yes' | 'no' = 'yes',
|
||||
mixed = f.mixed, source = f.game) {
|
||||
const declared = declareTruthMixedRevival(f, source), actions = [...declared.actions];
|
||||
const asked = askTruthMixedRevival(declared.game, f.asker, f.target, mixed, actions);
|
||||
const game = answerTruthMixedRevival(asked, f.target, answer, actions);
|
||||
return { game, declared, asked, actions, actionCount: actions.length };
|
||||
}
|
||||
|
||||
/** Own public raw aggregate only; no private frozen clause or residual access. */
|
||||
export function truthMixedRevivalPromise(game: Game,
|
||||
target = game.truthHistory?.at(-1)?.question.target ?? game.players[0].id) {
|
||||
const row = viewGame(game, target).mixedRevivalPromises.find(row => row.player === target);
|
||||
assert.ok(row, 'The respondent retains the original mixed question and whole-answer status');
|
||||
return row;
|
||||
}
|
||||
export function completeTruthMixedRevival(game: Game, target: string, actions: TruthMixedRevivalStep[] = []): Game {
|
||||
for (let count = 0; count < 4 && !truthMixedRevivalPromise(game, target).fulfilled; count++) {
|
||||
const guide = viewGame(game, target).revivalExpressionPromiseGuide;
|
||||
assert.ok(guide, 'The owned mixed Yes has an exact native ordinary return guide');
|
||||
assert.equal(guide.phase, game.phase);
|
||||
game = settleTruthMixedRevival(actTruthMixedRevival(game, target, guide.action, actions), actions);
|
||||
}
|
||||
assert.equal(truthMixedRevivalPromise(game, target).fulfilled, true);
|
||||
return game;
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { MixedRevivalFields, mixedRevivalInputError } from '../components/mixed-revival-fields';
|
||||
import { MixedRevivalPromises } from '../components/mixed-revival-promises';
|
||||
import { TruthFactFields } from '../components/mixed-shipment-fields';
|
||||
import { RevivalExpressionPromiseGuideButton, RevivalExpressionPromises } from '../components/revival-expression-promises';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine';
|
||||
import type { MixedActionExpression } from '../game/mixed-action-question';
|
||||
import type { RevivalActionClaim } from '../game/mixed-revival-question';
|
||||
import { createRicheseAuction, projectRicheseAuction } from '../game/richese-auction';
|
||||
import { createTechTokens } from '../game/tech-tokens';
|
||||
import type { TruthFact } from '../game/truthtrance';
|
||||
|
||||
type Expression = MixedActionExpression<RevivalActionClaim>;
|
||||
type ElementProps = { children?: ReactNode; [key: string]: unknown };
|
||||
type Element = ReactElement<ElementProps>;
|
||||
const fact = (value: TruthFact = { kind: 'hand', name: 'Shield' }): Expression => ({ kind: 'fact', fact: value });
|
||||
const force = (minimum = 1, counter: 'total' | 'normal' | 'elite' = 'total'): Expression =>
|
||||
({ kind: 'action', action: { kind: 'forces', counter, minimum } });
|
||||
const named = (leader = 'named-leader'): Expression => ({ kind: 'action', action: { kind: 'leader', leader } });
|
||||
const claim = (): Expression => ({ kind: 'or', terms: [fact(), { kind: 'and', terms: [force(3), named()] }] });
|
||||
function elements(node: ReactNode): Element[] {
|
||||
if (Array.isArray(node)) return node.flatMap(elements);
|
||||
if (!isValidElement<ElementProps>(node)) return [];
|
||||
return [node, ...elements(node.props.children)];
|
||||
}
|
||||
function find(tree: ReactNode, key: string, value: string): Element {
|
||||
const found = elements(tree).find(element => element.props[key] === value);
|
||||
assert.ok(found, `${key}=${value}`); return found;
|
||||
}
|
||||
const control = (tree: ReactNode, key: string) => find(tree, 'data-mixed-revival-control', key);
|
||||
const click = (button: Element) => (button.props.onClick as () => void)();
|
||||
function select(tree: ReactNode, path: string, field: string, value: string) {
|
||||
const input = find(tree, 'id', `truth-mixed-revival-${path}-${field}`);
|
||||
(input.props.onChange as (event: { currentTarget: { value: string } }) => void)({ currentTarget: { value } });
|
||||
}
|
||||
function minimum(tree: ReactNode, path: string, valueAsNumber: number) {
|
||||
const input = find(tree, 'id', `truth-mixed-revival-${path}-minimum`);
|
||||
(input.props.onChange as (event: { currentTarget: { valueAsNumber: number } }) => void)({ currentTarget: { valueAsNumber } });
|
||||
}
|
||||
function editFact(tree: ReactNode, path: string, next: TruthFact) {
|
||||
const scope = find(tree, 'data-mixed-revival-fact-path', path);
|
||||
const fields = elements(scope).find(element => element.type === TruthFactFields);
|
||||
assert.ok(fields, `Current fact fields ${path}`);
|
||||
(fields.props.onChange as (fact: TruthFact) => void)(next);
|
||||
}
|
||||
|
||||
/** Controlled projections prove field and exact callback contracts. Earned
|
||||
* cards, actual casualties, quotes and native returns have separate proof. */
|
||||
function projected(phase = 4, type = 'revive', advanced = false): GameView {
|
||||
const source = createGame('TRUTHMIXEDREVIVALCONTROLS', newPlayer('p', 'Respondent', 'emperor'), advanced);
|
||||
joinGame(source, newPlayer('a', 'Asker', 'atreides'));
|
||||
const game = viewGame(source, 'p');
|
||||
game.status = 'playing'; game.turn = 2; game.phase = phase; game.ready = [];
|
||||
game.allLeaders = [
|
||||
{ id: 'named-leader', name: 'Named Commander', faction: 'emperor', strength: 3 },
|
||||
{ id: 'second-leader', name: 'Second Commander', faction: 'emperor', strength: 4 },
|
||||
{ id: 'foreign-leader', name: 'Foreign Commander', faction: 'atreides', strength: 5 },
|
||||
];
|
||||
game.revivalExpressionPromises = [];
|
||||
game.mixedRevivalPromises = [{ event: 'controlled-mixed-revival-event', player: 'p', asker: 'a', turn: 2,
|
||||
mixed: claim(), answer: type !== 'ready', progress: { total: 2, normal: 1, elite: 1, leaders: ['named-leader'] },
|
||||
fulfilled: false, released: false }];
|
||||
game.revivalOrder = advanced ? { version: 1, turn: 2, original: ['p', 'a'], remaining: ['p', 'a'], completed: [], started: false } : null;
|
||||
game.active = advanced ? 'p' : null;
|
||||
game.auction = phase === 3 ? { bid: 4, bidder: type === 'decision' ? 'p' : 'a', active: type === 'decision' ? 'a' : 'p',
|
||||
remaining: 2, inspectionBlocked: null, card: null } : null;
|
||||
game.decision = type === 'decision' ? { kind: 'auctionPayment', player: 'p' } : null;
|
||||
if (type === 'acceptLeaderRevival') game.revivalRequests.p = { leader: 'named-leader', price: 3 };
|
||||
game.revivalExpressionPromiseGuide = { phase, action: { type, revivalExpressionPromiseEvent: 'controlled-mixed-revival-event',
|
||||
...(type === 'revive' ? { amount: 2, elite: 1, freeElite: 0 } : {}),
|
||||
...(type === 'reviveLeader' ? { leader: 'named-leader' } : {}),
|
||||
...(type === 'acceptLeaderRevival' ? { leaderRevivalPrice: 3 } : {}),
|
||||
...(type === 'decision' ? { karama: true, card: 'controlled-original-karama' } : {}) } };
|
||||
return game;
|
||||
}
|
||||
function editor(initial: Expression, disabled = false, game = projected(), target = 'p') {
|
||||
let value = initial, changes = 0;
|
||||
return {
|
||||
get value() { return value; }, get changes() { return changes; },
|
||||
tree: () => MixedRevivalFields({ value, game, target, disabled, onChange(next) { value = next; changes++; } }),
|
||||
html: () => renderToStaticMarkup(createElement(MixedRevivalFields, { value, game, target, disabled, onChange() {} })),
|
||||
error: () => mixedRevivalInputError(value, game, target),
|
||||
};
|
||||
}
|
||||
function guide(props: Parameters<typeof RevivalExpressionPromiseGuideButton>[0]) {
|
||||
const button = RevivalExpressionPromiseGuideButton(props); assert.ok(button); return button;
|
||||
}
|
||||
const press = (button: ReturnType<typeof guide>) => button.props.onClick();
|
||||
const markup = (game: GameView) => renderToStaticMarkup(createElement(MixedRevivalPromises, { game, busy: false, send() {} }));
|
||||
const pureMarkup = (game: GameView) => renderToStaticMarkup(createElement(RevivalExpressionPromises, { game, busy: false, send() {} }));
|
||||
|
||||
void test('shared mixed editor keeps sixteen original conditions, typed force counters and unique per-leaf IDs without overflow', () => {
|
||||
const e = editor(fact()); click(control(e.tree(), 'or-1'));
|
||||
for (let count = 3; count <= 16; count++) {
|
||||
click(control(e.tree(), 'add-action-1')); minimum(e.tree(), `1.${count}`, count);
|
||||
if (count % 3 === 0) select(e.tree(), `1.${count}`, 'counter', 'elite');
|
||||
}
|
||||
select(e.tree(), '1.2', 'kind', 'leader'); select(e.tree(), '1.2', 'leader', 'second-leader');
|
||||
const full = e.value; assert.ok(full.kind === 'or'); assert.equal(full.terms.length, 16);
|
||||
assert.deepEqual(full.terms[0], fact()); assert.deepEqual(full.terms[1], named('second-leader'));
|
||||
assert.deepEqual(full.terms[2], force(3, 'elite')); assert.equal(e.error(), null);
|
||||
const html = e.html(), ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
|
||||
assert.match(html, /16 \/ 16 conditions/); assert.equal((html.match(/Revival condition type/g) ?? []).length, 15);
|
||||
assert.equal(new Set(ids).size, ids.length);
|
||||
const before = structuredClone(full), changes = e.changes;
|
||||
for (const key of ['add-fact-1', 'add-action-1', 'nested-and-1', 'nested-or-1', 'and-1.1', 'fact-and-1.1', 'or-1']) {
|
||||
assert.equal(control(e.tree(), key).props.disabled, true, key); click(control(e.tree(), key));
|
||||
}
|
||||
assert.equal(e.changes, changes); assert.deepEqual(e.value, before);
|
||||
});
|
||||
|
||||
void test('mixed and embedded fact groups retain every branch through edits, collapse and the shared depth-four envelope', () => {
|
||||
const e = editor({ kind: 'and', terms: [fact(), force(3)] });
|
||||
click(control(e.tree(), 'or-1.2')); select(e.tree(), '1.2', 'meaning', 'and');
|
||||
click(control(e.tree(), 'nested-or-1')); click(control(e.tree(), 'nested-and-1'));
|
||||
assert.ok(e.value.kind === 'and'); assert.equal(e.value.terms.length, 4);
|
||||
click(control(e.tree(), 'fact-or-1.1')); editFact(e.tree(), '1.1.2', { kind: 'stormForecast', compare: 'gte', value: 4 });
|
||||
select(e.tree(), '1.1', 'fact-meaning', 'and'); click(control(e.tree(), 'fact-add-1.1'));
|
||||
editFact(e.tree(), '1.1.3', { kind: 'spice', compare: 'gte', value: 7 });
|
||||
assert.match(e.html(), /Current-fact group 1\.1/); assert.equal(e.error(), null);
|
||||
click(control(e.tree(), 'fact-remove-1.1.3')); click(control(e.tree(), 'fact-remove-1.1.2'));
|
||||
click(control(e.tree(), 'remove-1.2.2')); click(control(e.tree(), 'remove-1.4')); click(control(e.tree(), 'remove-1.3'));
|
||||
assert.deepEqual(e.value, { kind: 'and', terms: [fact(), force(3)] });
|
||||
select(e.tree(), '1.2', 'timing', 'fact'); assert.ok(e.error());
|
||||
select(e.tree(), '1.2', 'timing', 'action'); assert.equal(e.error(), null);
|
||||
click(control(e.tree(), 'remove-1.1')); assert.deepEqual(e.value, force()); assert.ok(e.error());
|
||||
click(control(e.tree(), 'and-1')); assert.equal(e.error(), null);
|
||||
let nested = fact();
|
||||
for (let depth = 0; depth < 4; depth++) nested = { kind: 'and', terms: [nested, force()] };
|
||||
const deep = editor(nested); assert.equal(deep.error(), null); assert.match(deep.html(), /4 \/ 4 levels of grouping/);
|
||||
for (const key of ['and-1', 'or-1.1.1.1.1', 'fact-and-1.1.1.1.1', 'nested-and-1.1.1.1']) {
|
||||
assert.equal(control(deep.tree(), key).props.disabled, true); click(control(deep.tree(), key));
|
||||
}
|
||||
assert.equal(deep.changes, 0);
|
||||
assert.ok(mixedRevivalInputError({ kind: 'and', terms: [nested, force()] }, projected(), 'p'));
|
||||
});
|
||||
|
||||
void test('numeric and named drafts stay repairable while exact action leaves reject groups or metadata and busy callbacks preserve the tree', () => {
|
||||
for (const invalid of [NaN, -1, 0, 0.5, 21, Infinity]) {
|
||||
const e = editor({ kind: 'and', terms: [fact(), force()] });
|
||||
minimum(e.tree(), '1.2', invalid); assert.ok(e.error()); click(control(e.tree(), 'or-1.2'));
|
||||
const raw = e.value; assert.ok(raw.kind === 'and');
|
||||
assert.deepEqual(raw.terms[1], { kind: 'or', terms: [force(invalid), fact()] });
|
||||
assert.match(e.html(), /role="alert"/);
|
||||
minimum(e.tree(), '1.2.1', 2); assert.equal(e.error(), null);
|
||||
select(e.tree(), '1.2.1', 'kind', 'leader'); select(e.tree(), '1.2.1', 'leader', ''); assert.ok(e.error());
|
||||
select(e.tree(), '1.2.1', 'leader', 'named-leader'); assert.equal(e.error(), null);
|
||||
}
|
||||
for (const action of [{ kind: 'and', terms: [{ kind: 'forces', counter: 'total', minimum: 1 }] },
|
||||
{ kind: 'forces', counter: 'total', minimum: 1, turn: 2 }, { kind: 'leader', leader: 'named-leader', fulfilled: true },
|
||||
{ kind: 'forces', counter: 'strength', minimum: 1 }, { kind: 'leader', leader: 'foreign-leader' }])
|
||||
assert.ok(mixedRevivalInputError({ kind: 'and', terms: [fact(), { kind: 'action', action }] }, projected(), 'p'));
|
||||
const frozen = editor({ kind: 'and', terms: [fact(), force()] }, true), before = structuredClone(frozen.value);
|
||||
for (const key of ['and-1', 'add-action-1', 'nested-or-1', 'remove-1.2', 'fact-or-1.1']) click(control(frozen.tree(), key));
|
||||
minimum(frozen.tree(), '1.2', 3); select(frozen.tree(), '1', 'meaning', 'or'); select(frozen.tree(), '1.2', 'kind', 'leader');
|
||||
editFact(frozen.tree(), '1.1', { kind: 'spice', compare: 'gte', value: 4 });
|
||||
assert.equal(frozen.changes, 0); assert.deepEqual(frozen.value, before);
|
||||
});
|
||||
|
||||
void test('leader choices use public respondent faction and Advanced Atreides identity while current facts inspect no secret eligibility', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const game = projected(4, 'revive', advanced);
|
||||
for (const player of game.players) for (const key of ['hand', 'traitors', 'faceDancers', 'leaders', 'kwisatz', 'spice', 'forces', 'reserves', 'tanks', 'elites'])
|
||||
Object.defineProperty(player, key, { get() { throw Error(`No ${key} inspection`); }, configurable: true });
|
||||
for (const key of ['revival', 'deck', 'spiceDeck', 'discard']) Object.defineProperty(game, key, {
|
||||
get() { throw Error(`No private ${key} inspection`); }, configurable: true,
|
||||
});
|
||||
const own = editor({ kind: 'and', terms: [fact({ kind: 'stormForecast', compare: 'gte', value: 3 }), named()] }, false, game);
|
||||
assert.equal(own.error(), null);
|
||||
const ownHTML = own.html(); assert.match(ownHTML, /Named Commander/); assert.match(ownHTML, /Second Commander/);
|
||||
assert.doesNotMatch(ownHTML, /Foreign Commander|Kwisatz Haderach/);
|
||||
assert.match(ownHTML, /Current facts are evaluated when the player answers/);
|
||||
assert.match(ownHTML, /unknown current fact can still allow a definite whole answer/);
|
||||
assert.match(ownHTML, /conservative future commitment/); assert.match(ownHTML, /If no such value is available, the answer is I don’t know/);
|
||||
const other = editor({ kind: 'and', terms: [fact(), named('foreign-leader')] }, false, game, 'a');
|
||||
assert.equal(other.error(), null); assert.match(other.html(), /Foreign Commander/); assert.doesNotMatch(other.html(), /Named Commander|Second Commander/);
|
||||
if (advanced) assert.match(other.html(), /Kwisatz Haderach/); else assert.doesNotMatch(other.html(), /Kwisatz Haderach/);
|
||||
assert.equal(mixedRevivalInputError({ kind: 'and', terms: [fact(), named('kwisatz')] }, game, 'a') === null, advanced);
|
||||
editFact(own.tree(), '1.1', { kind: 'spice', compare: 'gte', value: NaN }); assert.ok(own.error());
|
||||
editFact(own.tree(), '1.1', { kind: 'spice', compare: 'gte', value: 2 }); assert.equal(own.error(), null);
|
||||
}
|
||||
});
|
||||
|
||||
void test('own aggregate-only panel preserves the original question, whole answer and typed cumulative ordinary progress without frozen branches', () => {
|
||||
const game = projected(); game.revivalExpressionPromiseGuide = null;
|
||||
game.mixedRevivalPromises.push(
|
||||
{ ...game.mixedRevivalPromises[0], event: 'no-event', answer: false },
|
||||
{ ...game.mixedRevivalPromises[0], event: 'fulfilled-event', fulfilled: true },
|
||||
{ ...game.mixedRevivalPromises[0], event: 'released-event', released: true },
|
||||
{ ...game.mixedRevivalPromises[0], event: 'foreign-event', player: 'a', mixed: { kind: 'or', terms: [fact({ kind: 'hand', name: 'Poison Blade' }), named('foreign-leader')] } },
|
||||
);
|
||||
for (const promise of game.mixedRevivalPromises) for (const key of ['binding', 'residual', 'factAnswers', 'frozenFacts', 'compiled'])
|
||||
Object.defineProperty(promise, key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
const html = markup(game);
|
||||
assert.match(html, /You answered Yes/); assert.match(html, /You answered No/); assert.match(html, /you hold Shield/);
|
||||
assert.match(html, / OR /); assert.match(html, / AND /); assert.match(html, /Named Commander/);
|
||||
assert.match(html, /Future ordinary progress: 2 physical forces \(1 normal, 1 elite\)/); assert.match(html, /returned leaders: Named Commander/);
|
||||
assert.match(html, /Fulfilled: this whole commitment is complete/); assert.match(html, /Released: this whole commitment is no longer possible/);
|
||||
assert.match(html, /Individual fact answers and any reduced future branch remain private/);
|
||||
assert.match(html, /No negates the whole expression, not each condition separately/);
|
||||
assert.match(html, /cumulative across smaller future ordinary returns/);
|
||||
assert.match(html, /Prior returns, requests alone, Ghola, foreign leaders and special card returns do not count/);
|
||||
assert.match(html, /Original costs, revival cycles, limits and turn order still apply/);
|
||||
assert.doesNotMatch(html, /Poison Blade|Foreign Commander|controlled-mixed-revival-event|fulfilled-event|factAnswers/);
|
||||
assert.equal(MixedRevivalPromises({ game: { ...game, me: 'missing' }, busy: false, send() {} }), null);
|
||||
});
|
||||
|
||||
void test('shared Basic and ordered Advanced guides forward the exact mixed return packet and accept only the current leader price', () => {
|
||||
for (const advanced of [false, true]) for (const type of ['revive', 'reviveLeader', 'reviveKwisatz', 'acceptLeaderRevival', 'ready']) {
|
||||
const game = projected(4, type, advanced), sent: Action[] = [];
|
||||
const button = guide({ game, busy: false, send(action) { sent.push(action); } });
|
||||
assert.equal(button.props.disabled, false); press(button);
|
||||
assert.deepEqual(sent, [game.revivalExpressionPromiseGuide!.action]); assert.equal(sent[0], game.revivalExpressionPromiseGuide!.action);
|
||||
assert.match(markup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
if (type === 'revive') assert.deepEqual(sent[0], { type: 'revive', amount: 2, elite: 1, freeElite: 0,
|
||||
revivalExpressionPromiseEvent: 'controlled-mixed-revival-event' });
|
||||
if (type === 'acceptLeaderRevival') {
|
||||
game.revivalRequests.p.price = 4; press(button); assert.equal(sent.length, 1);
|
||||
game.revivalRequests.p.price = 3; game.revivalRequests.p.declined = true; press(button); assert.equal(sent.length, 1);
|
||||
}
|
||||
if (advanced) { game.revivalOrder!.remaining = ['a', 'p']; game.active = 'a'; press(button); assert.equal(sent.length, 1); }
|
||||
}
|
||||
});
|
||||
|
||||
void test('early mixed preparations retain Charity, ordinary auction payment and owned native silent-zero or pass packets', () => {
|
||||
for (const [phase, type] of [[2, 'charity'], [2, 'ready'], [3, 'passBid'], [3, 'decision']] as const) {
|
||||
const game = projected(phase, type), sent: Action[] = [];
|
||||
const button = guide({ game, busy: false, send(action) { sent.push(action); } });
|
||||
assert.equal(button.props.disabled, false); press(button); assert.equal(sent[0], game.revivalExpressionPromiseGuide!.action);
|
||||
if (type === 'decision') {
|
||||
assert.equal(sent[0].card, 'controlled-original-karama'); assert.equal(sent[0].karama, true);
|
||||
assert.doesNotMatch(markup(game), /controlled-original-karama/); game.decision!.player = 'a';
|
||||
} else if (phase === 2) game.ready.push('p'); else game.auction!.active = 'a';
|
||||
press(button); assert.equal(sent.length, 1);
|
||||
}
|
||||
for (const method of ['silent', 'onceAround'] as const) {
|
||||
const game = projected(3, 'richeseBid'), sent: Action[] = [];
|
||||
const native = createRicheseAuction({ event: 'controlled-mixed-revival-auction', cardId: 'controlled-card', owner: 'a',
|
||||
method, source: 'cache', eligible: ['p', 'a'], order: ['p', 'a'], tieOrder: ['p', 'a'] });
|
||||
game.richeseAuction = { ...projectRicheseAuction(native, 'p'), card: null, claim: null,
|
||||
ownAllyPayment: 0, ownAvailable: 4, allyAvailable: 0, peekKnown: false, inspectionBlocked: null };
|
||||
game.revivalExpressionPromiseGuide = { phase: 3, action: { type: 'richeseBid', event: native.event,
|
||||
amount: method === 'silent' ? 0 : null, allyPayment: 0, revivalExpressionPromiseEvent: 'controlled-mixed-revival-event' } };
|
||||
const button = guide({ game, busy: false, send(action) { sent.push(action); } });
|
||||
assert.equal(button.props.disabled, false); press(button); assert.equal(sent[0], game.revivalExpressionPromiseGuide.action);
|
||||
game.richeseAuction.event = 'later-auction'; press(button); assert.equal(sent.length, 1);
|
||||
game.richeseAuction.event = native.event;
|
||||
if (method === 'silent') game.richeseAuction.submitted.push('p'); else game.richeseAuction.active = 'a';
|
||||
press(button); assert.equal(sent.length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
void test('stale mixed guide callbacks preserve exact event, owner, turn, phase, action counters, order and quiet-window guards without private inspection', () => {
|
||||
const changes: ((game: GameView) => void)[] = [
|
||||
game => { game.me = 'a'; }, game => { game.turn = 3; }, game => { game.phase = 5; }, game => { game.status = 'setup'; },
|
||||
game => { game.ready = ['p']; }, game => { game.revivalOrder!.remaining = ['a', 'p']; game.active = 'a'; },
|
||||
game => { game.revivalOrder!.turn = 1; }, game => { game.active = null; }, game => { game.revivalOrder = null; },
|
||||
game => { game.revivalOrder = { ...game.revivalOrder!, remaining: ['p', 'a'] }; },
|
||||
game => { game.revivalExpressionPromiseGuide = { phase: 4, action: { ...game.revivalExpressionPromiseGuide!.action } }; },
|
||||
game => { game.revivalExpressionPromiseGuide = null; }, game => { game.revivalExpressionPromiseGuide!.phase = 3; },
|
||||
game => { game.revivalExpressionPromiseGuide!.action.amount = 3; }, game => { game.revivalExpressionPromiseGuide!.action.elite = 0; },
|
||||
game => { game.revivalExpressionPromiseGuide!.action.freeElite = 1; },
|
||||
game => { game.mixedRevivalPromises[0].event = 'expired'; }, game => { game.mixedRevivalPromises[0].player = 'a'; },
|
||||
game => { game.mixedRevivalPromises[0].turn = 1; }, game => { game.mixedRevivalPromises[0].fulfilled = true; },
|
||||
game => { game.mixedRevivalPromises[0].released = true; }, game => { game.mixedRevivalPromises = []; },
|
||||
game => { game.phaseOpening = { passed: [] }; }, game => { game.response = { kind: 'guildIncome', owner: 'a', amount: 1, passed: [] }; },
|
||||
game => { game.decision = { kind: 'auctionPayment', player: 'p' }; },
|
||||
game => { game.truthtrance = { stage: 'ask', queue: [{ player: 'a', card: 'controlled-card' }], passed: [], question: null }; },
|
||||
game => { game.automaticContinuationPending = true; }, game => { game.players.find(player => player.id === game.me)!.autopilot = 'Easy'; },
|
||||
game => { game.roomControl = { paused: true, joinLocked: false, revision: 1, updatedAt: null }; },
|
||||
game => { game.roomControl = { paused: false, closed: true, joinLocked: false, revision: 1, updatedAt: null }; },
|
||||
game => { game.techTokens = createTechTokens(); },
|
||||
];
|
||||
for (const change of changes) {
|
||||
const game = projected(4, 'revive', true), sent: Action[] = [], button = guide({ game, busy: false, send(action) { sent.push(action); } });
|
||||
change(game); press(button); assert.deepEqual(sent, []);
|
||||
}
|
||||
const busy = { game: projected(), busy: false, send() { assert.fail('A busy guide cannot send'); } };
|
||||
const button = guide(busy); busy.busy = true; press(button);
|
||||
const replaced = { game: projected(), busy: false, send() { assert.fail('A replaced view cannot send'); } };
|
||||
const original = guide(replaced); replaced.game = { ...replaced.game }; press(original);
|
||||
for (const [phase, type] of [[0, 'ready'], [1, 'ready'], [2, 'revive'], [3, 'revive'], [4, 'charity']] as const) {
|
||||
const game = projected(phase, type), sent: Action[] = [];
|
||||
press(guide({ game, busy: false, send(action) { sent.push(action); } })); assert.deepEqual(sent, []);
|
||||
}
|
||||
for (const type of ['revive', 'reviveLeader', 'reviveKwisatz', 'decision']) {
|
||||
const game = projected(type === 'decision' ? 3 : 4, type), sent: Action[] = [];
|
||||
for (const player of game.players) for (const key of ['hand', 'traitors', 'faceDancers', 'leaders', 'kwisatz', 'spice', 'forces', 'reserves', 'tanks', 'elites'])
|
||||
Object.defineProperty(player, key, { get() { throw Error(`No private ${key} inspection`); }, configurable: true });
|
||||
for (const key of ['revival', 'deck', 'spiceDeck', 'discard']) Object.defineProperty(game, key, {
|
||||
get() { throw Error(`No private ${key} inspection`); }, configurable: true,
|
||||
});
|
||||
if (game.auction) Object.defineProperty(game.auction, 'card', { get() { throw Error('No hidden auction lot inspection'); } });
|
||||
assert.match(markup(game), /Your mixed Revival promises/);
|
||||
press(guide({ game, busy: false, send(action) { sent.push(action); } })); assert.deepEqual(sent, [game.revivalExpressionPromiseGuide!.action]);
|
||||
}
|
||||
});
|
||||
|
||||
void test('mixed panels wait for the actual Revival actor and share one guide with live pure records while historical panels stay passive', () => {
|
||||
for (const phase of [0, 1, 2, 3]) {
|
||||
const game = projected(phase); game.revivalExpressionPromiseGuide = null;
|
||||
assert.match(markup(game), /Your Revival opportunity is still ahead/); assert.doesNotMatch(markup(game), /<button/);
|
||||
}
|
||||
const game = projected(4, 'revive', true); game.revivalOrder!.remaining = ['a', 'p']; game.active = 'a';
|
||||
game.revivalExpressionPromiseGuide = null; assert.match(markup(game), /while the current actor finishes/);
|
||||
game.revivalOrder!.remaining = ['p', 'a']; game.active = 'p'; game.revivalExpressionPromiseGuide = projected().revivalExpressionPromiseGuide;
|
||||
assert.match(markup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
game.revivalExpressionPromises.push({ event: 'controlled-pure-event', player: 'p', asker: 'a', turn: 2,
|
||||
claim: { kind: 'forces', counter: 'total', minimum: 3 }, answer: true, progress: { total: 0, normal: 0, elite: 0, leaders: [] } });
|
||||
assert.doesNotMatch(markup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
assert.match(pureMarkup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
game.revivalExpressionPromises[0].fulfilled = true;
|
||||
assert.match(markup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
assert.doesNotMatch(pureMarkup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
game.revivalExpressionPromises[0].fulfilled = false; game.revivalExpressionPromises[0].turn = 1;
|
||||
assert.match(markup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
assert.doesNotMatch(pureMarkup(game), /data-testid="revival-expression-promise-guide"/);
|
||||
game.mixedRevivalPromises[0].fulfilled = true;
|
||||
assert.doesNotMatch(markup(game), /data-testid="revival-expression-promise-guide"|Your Revival opportunity is still ahead/);
|
||||
game.mixedRevivalPromises[0].fulfilled = false; game.mixedRevivalPromises[0].turn = 1;
|
||||
assert.doesNotMatch(markup(game), /data-testid="revival-expression-promise-guide"|Your Revival opportunity is still ahead/);
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import {
|
||||
actTruthMixedRevival as act, answerTruthMixedRevival as answer, askTruthMixedRevival as ask,
|
||||
completeTruthMixedRevival as complete, createTruthMixedRevivalFixture as fixture,
|
||||
declareTruthMixedRevival as declare, finishTruthMixedRevival as finish,
|
||||
settleTruthMixedRevival as settle, truthMixedRevivalFact as fact,
|
||||
truthMixedRevivalForce as force, truthMixedRevivalGroup as group,
|
||||
truthMixedRevivalInventory as inventory, truthMixedRevivalLeader as leader,
|
||||
truthMixedRevivalPlayer as player, truthMixedRevivalPromise as promise,
|
||||
} from './fixture-truth-mixed-revival';
|
||||
|
||||
function rejected(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
|
||||
}
|
||||
function truthsOnce(game: Game, truths: string[]) {
|
||||
for (const truth of truths) assert.equal(game.discard.filter(card => card.id === truth).length, 1);
|
||||
}
|
||||
|
||||
void test('two original Truths retain scalar force No while mixed OR Yes follows the real alternate leader', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'revival', target: f.target, turn: game.turn, counter: 'total', minimum: 1,
|
||||
} });
|
||||
game = answer(game, f.target, 'no');
|
||||
game = ask(game, f.asker, f.target, group('and', fact(f.spiceFact), group('or', force(), leader(f.leader))));
|
||||
assert.deepEqual(viewGame(game, f.target).truthMixedRevivalAnswers, ['yes', 'no']);
|
||||
game = answer(game, f.target, 'yes');
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'reviveLeader'); game = settle(act(game, f.target, guide.action));
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(game.revivalPromises![0].progress, 0);
|
||||
assert.equal(player(game, f.target).tanks, 1); assert.equal(player(game, f.target).spice, 2);
|
||||
game = finish(game); assert.equal(game.revivalPromises![0].fulfilled, true);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('two original Truths retain scalar leader No while mixed OR Yes returns only the real free force', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'leaderRevival', target: f.target, turn: game.turn, leader: f.leader,
|
||||
} });
|
||||
game = answer(game, f.target, 'no');
|
||||
game = answer(ask(game, f.asker, f.target,
|
||||
group('and', fact(f.spiceFact), group('or', force(), leader(f.leader)))), f.target, 'yes');
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'revive'); game = settle(act(game, f.target, guide.action));
|
||||
assert.equal(promise(game).fulfilled, true); assert.deepEqual(promise(game).progress.leaders, []);
|
||||
assert.equal(player(game, f.target).spice, 5); rejected(game, f.target, f.revivalAction);
|
||||
game = finish(game); assert.equal(game.leaderRevivalPromises![0].fulfilled, true);
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.dead, true);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('two original Truths share one native leader invoice between an earlier named Yes and mixed AND Yes', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'leaderRevival', target: f.target, turn: game.turn, leader: f.leader,
|
||||
} });
|
||||
game = answer(game, f.target, 'yes'); game = answer(ask(game, f.asker, f.target, f.mixed), f.target, 'yes');
|
||||
game = complete(game, f.target);
|
||||
assert.equal(game.leaderRevivalPromises![0].fulfilled, true); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(player(game, f.target).spice, 3); assert.equal(player(game, f.target).leaderRevived, true);
|
||||
assert.equal(player(game, f.target).freeForcesRevived, 1);
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.deaths, 1);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('two original Truths preserve an earlier mixed AND Yes against a contradictory later scalar force No', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = answer(ask(game, f.asker, f.target, f.mixed), f.target, 'yes');
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'revival', target: f.target, turn: game.turn, counter: 'total', minimum: 1,
|
||||
} });
|
||||
assert.deepEqual(viewGame(game, f.target).truthRevivalAnswers, ['yes']);
|
||||
rejected(game, f.target, { type: 'truthAnswer', answer: 'no' });
|
||||
game = answer(game, f.target, 'yes'); game = complete(game, f.target);
|
||||
assert.equal(game.revivalPromises![0].fulfilled, true); assert.equal(game.revivalPromises![0].progress, 1);
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(player(game, f.target).spice, 3);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('earlier original Bribe Yes reserves T4 before a later mixed ordinary AND answer', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'bribe', target: f.target, turn: game.turn, recipient: f.opponent, minimum: 4,
|
||||
} });
|
||||
game = answer(game, f.target, 'yes'); game = ask(game, f.asker, f.target, f.mixed);
|
||||
assert.deepEqual(viewGame(game, f.target).truthMixedRevivalAnswers, ['no']);
|
||||
rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' }); game = answer(game, f.target, 'no');
|
||||
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
|
||||
game = act(game, f.target, guide.action); assert.equal(player(game, f.target).spice, 0);
|
||||
assert.equal(player(game, f.opponent).bribes, 4); game = finish(game);
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(game.bribePromises![0].fulfilled, true);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('earlier mixed T4 fact and AND Yes retain their budget through a real bribe3 free-force bank1 and leader2', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = answer(ask(game, f.asker, f.target, f.mixed), f.target, 'yes');
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'bribe', target: f.target, turn: game.turn, recipient: f.opponent, minimum: 4,
|
||||
} });
|
||||
assert.deepEqual(viewGame(game, f.target).truthBribeAnswers, ['no']); game = answer(game, f.target, 'no');
|
||||
rejected(game, f.target, { type: 'bribe', target: f.opponent, amount: 4 });
|
||||
game = act(game, f.target, { type: 'bribe', target: f.opponent, amount: 3 });
|
||||
assert.equal(player(game, f.target).spice, 1);
|
||||
let guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'revive');
|
||||
game = settle(act(game, f.target, guide.action)); assert.equal(player(game, f.target).spice, 2);
|
||||
assert.equal(promise(game).progress.normal, 1); assert.equal(promise(game).fulfilled, false);
|
||||
guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'reviveLeader');
|
||||
game = settle(act(game, f.target, guide.action)); assert.equal(player(game, f.target).spice, 0);
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(player(game, f.opponent).bribes, 3);
|
||||
game = finish(game); assert.ok(!game.bribePromises![0].fulfilled, 'The actual turn-wide bribe No outlasts Revival');
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('earlier original shipment funding excludes a later mixed AND beyond the original shared purse', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'shipment', target: f.target, claim: { territory: 'arrakeen', minimum: 4 },
|
||||
} });
|
||||
game = answer(game, f.target, 'yes'); game = ask(game, f.asker, f.target, f.mixed);
|
||||
assert.deepEqual(viewGame(game, f.target).truthMixedRevivalAnswers, ['no']);
|
||||
rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' }); game = answer(game, f.target, 'no');
|
||||
assert.equal(game.shipmentPromises![0].answer, true); assert.ok(!game.shipmentPromises![0].released);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('earlier mixed AND reserves one real return invoice before a later original shipment answer', () => {
|
||||
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
||||
game = answer(ask(game, f.asker, f.target, f.mixed), f.target, 'yes');
|
||||
game = act(game, f.asker, { type: 'truthAsk', question: {
|
||||
kind: 'shipment', target: f.target, claim: { territory: 'arrakeen', minimum: 4 },
|
||||
} });
|
||||
assert.deepEqual(viewGame(game, f.target).truthShipmentAnswers, ['no']); game = answer(game, f.target, 'no');
|
||||
game = complete(game, f.target); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(player(game, f.target).spice, 3); assert.ok(!game.shipmentPromises![0].fulfilled);
|
||||
truthsOnce(game, f.truths); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { truthFactAnswer } from '../game/truthtrance';
|
||||
import { truthKnowledgeOf } from '../game/truthtrance-knowledge';
|
||||
import {
|
||||
actTruthMixedRevival as act, advanceTruthMixedRevival as advance,
|
||||
answerTruthMixedRevival as answer, askTruthMixedRevival as ask,
|
||||
bindTruthMixedRevival as bind, completeTruthMixedRevival as complete,
|
||||
createTruthMixedRevivalFixture as fixture, declareTruthMixedRevival as declare,
|
||||
finishTruthMixedRevival as finish, settleTruthMixedRevival as settle,
|
||||
truthMixedRevivalClean as clean, truthMixedRevivalFact as fact,
|
||||
truthMixedRevivalForce as force, truthMixedRevivalGroup as group,
|
||||
truthMixedRevivalInventory as inventory, truthMixedRevivalLeader as leader,
|
||||
truthMixedRevivalPlayer as player, truthMixedRevivalPromise as promise,
|
||||
} from './fixture-truth-mixed-revival';
|
||||
|
||||
const progress0 = { total: 0, normal: 0, elite: 0, leaders: [] };
|
||||
const forceAction: Action = { type: 'revive', amount: 1, elite: 0 };
|
||||
function rejected(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
|
||||
}
|
||||
function truthOnce(game: Game, truth: string) {
|
||||
assert.equal(game.discard.filter(card => card.id === truth).length, 1);
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) void test(`native ${advanced ? 'Advanced' : 'Basic'} mixed Revival freezes original T4 before real force income and named leader debit`, () => {
|
||||
const f = fixture({ advanced }); assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
|
||||
assert.deepEqual(f.game.expansions, ['ix']); assert.equal(f.originalCards.length, 47); assert.deepEqual(f.staging, []);
|
||||
assert.equal(f.sourceActionCount, advanced ? 108 : 102); assert.equal(f.sourceActionCount, f.sourceActions.length);
|
||||
assert.equal(f.sourceCounts.inherited, f.sourceActionCount);
|
||||
assert.deepEqual(f.beforeStartingDeal.deck.slice(0, 4).map(card => card.id), f.deal);
|
||||
assert.equal(f.revivalSource.question.kind, 'revivalExpression'); assert.equal(f.question.kind, 'mixedRevival');
|
||||
assert.equal(player(f.shipment.before, f.target).spice - player(f.shipment.after, f.target).spice, 1);
|
||||
assert.equal(player(f.afterBattle, f.target).leaders.find(disc => disc.id === f.leader)!.dead, true);
|
||||
assert.equal(player(f.game, f.target).leaders.filter(disc => !disc.dead).length, 4);
|
||||
assert.equal(player(f.game, f.target).revivalCycle, 0); assert.equal(f.quote.early, true); assert.equal(f.quote.cost, 2);
|
||||
assert.equal(player(f.game, f.target).tanks, 1); assert.equal(player(f.game, f.target).spice, 4);
|
||||
assert.equal(truthFactAnswer(player(f.game, f.target), f.spiceFact), 'yes');
|
||||
const before = structuredClone(f.game), bound = bind(f); assert.deepEqual(f.game, before);
|
||||
assert.deepEqual(promise(bound.game).mixed, f.mixed); assert.deepEqual(promise(bound.game).progress, progress0);
|
||||
rejected(bound.game, f.target, { type: 'ready' });
|
||||
let game = act(bound.game, f.target, forceAction);
|
||||
if (game.pendingRevival) assert.deepEqual(promise(game).progress, progress0);
|
||||
game = settle(game);
|
||||
assert.deepEqual(promise(game).progress, { total: 1, normal: 1, elite: 0, leaders: [] });
|
||||
assert.equal(promise(game).fulfilled, false); assert.equal(player(game, f.target).spice, 5);
|
||||
assert.equal(player(game, f.target).freeForcesRevived, 1); assert.equal(player(game, f.target).tanks, 0);
|
||||
rejected(game, f.target, { type: 'ready' });
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.phase, 4); assert.equal(guide.action.type, 'reviveLeader'); assert.equal(guide.action.leader, f.leader);
|
||||
assert.equal(guide.action.revivalExpressionPromiseEvent, promise(game).event);
|
||||
game = settle(act(game, f.target, guide.action));
|
||||
assert.deepEqual(promise(game).progress, { total: 1, normal: 1, elite: 0, leaders: [f.leader] });
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(promise(game).released, false);
|
||||
assert.equal(player(game, f.target).spice, 3); assert.equal(truthFactAnswer(player(game, f.target), f.spiceFact), 'no');
|
||||
assert.equal(player(game, f.target).leaderRevived, true);
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.deaths, 1);
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.dead, false);
|
||||
if (advanced) assert.deepEqual(game.revivalOrder!.original, f.game.revivalOrder!.original);
|
||||
truthOnce(game, f.truth); assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native true current fact OR return answers Yes immediately with no native Revival binding', () => {
|
||||
const f = fixture(), mixed = group('or', fact(f.spiceFact), leader(f.leader));
|
||||
const asked = ask(declare(f).game, f.asker, f.target, mixed);
|
||||
assert.deepEqual(viewGame(asked, f.target).truthMixedRevivalAnswers, ['yes']);
|
||||
rejected(asked, f.target, { type: 'truthAnswer', answer: 'no' });
|
||||
let game = answer(asked, f.target, 'yes'); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(game.revivalExpressionPromises?.length ?? 0, 0);
|
||||
assert.equal(viewGame(game, f.target).revivalExpressionPromiseGuide, null);
|
||||
assert.deepEqual(promise(game).mixed, mixed); assert.deepEqual(promise(game).progress, progress0);
|
||||
game = finish(game); assert.equal(player(game, f.target).spice, 4); assert.equal(player(game, f.target).revived, 0);
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.dead, true);
|
||||
truthOnce(game, f.truth); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native false current fact AND return answers No immediately while the real return remains legal', () => {
|
||||
const f = fixture(), mixed = group('and', fact(f.falseSpiceFact), force());
|
||||
assert.equal(truthFactAnswer(player(f.game, f.target), f.falseSpiceFact), 'no');
|
||||
const asked = ask(declare(f).game, f.asker, f.target, mixed);
|
||||
assert.deepEqual(viewGame(asked, f.target).truthMixedRevivalAnswers, ['no']);
|
||||
rejected(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
let game = answer(asked, f.target, 'no'); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(game.revivalExpressionPromises?.length ?? 0, 0);
|
||||
game = settle(act(game, f.target, forceAction)); assert.equal(player(game, f.target).spice, 5);
|
||||
assert.equal(truthFactAnswer(player(game, f.target), f.falseSpiceFact), 'yes');
|
||||
assert.deepEqual(promise(game).progress, progress0); assert.equal(promise(game).fulfilled, true);
|
||||
truthOnce(game, f.truth); assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native false current fact OR return Yes requires the actual ordinary return', () => {
|
||||
const f = fixture(); let game = bind(f, 'yes', group('or', fact(f.falseSpiceFact), leader(f.leader))).game;
|
||||
assert.equal(promise(game).fulfilled, false); assert.deepEqual(promise(game).progress, progress0);
|
||||
rejected(game, f.target, { type: 'ready' }); game = settle(act(game, f.target, f.revivalAction));
|
||||
assert.equal(promise(game).fulfilled, true); assert.deepEqual(promise(game).progress, { ...progress0, leaders: [f.leader] });
|
||||
assert.equal(player(game, f.target).spice, 2); truthOnce(game, f.truth);
|
||||
assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native true current fact AND return No forbids that exact ordinary event until the real deadline', () => {
|
||||
const f = fixture(); let game = bind(f, 'no', group('and', fact(f.spiceFact), force())).game;
|
||||
rejected(game, f.target, forceAction); assert.equal(promise(game).fulfilled, false);
|
||||
game = settle(act(game, f.target, f.revivalAction)); assert.equal(player(game, f.target).spice, 2);
|
||||
assert.equal(truthFactAnswer(player(game, f.target), f.spiceFact), 'no');
|
||||
rejected(game, f.target, forceAction); assert.deepEqual(promise(game).progress, { ...progress0, leaders: [f.leader] });
|
||||
game = finish(game); assert.equal(promise(game).fulfilled, true); assert.equal(player(game, f.target).tanks, 1);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
for (const answerValue of ['yes', 'no'] as const) void test(`native unknown non-Fremen forecast uses the conservative whole ${answerValue} ordinary-return constraint`, () => {
|
||||
const f = fixture(), p = player(f.game, f.target);
|
||||
assert.equal(p.faction, 'tleilaxu'); assert.equal(truthKnowledgeOf(f.game, p).stormForecast, null);
|
||||
assert.equal(truthFactAnswer(p, f.forecastFact, truthKnowledgeOf(f.game, p)), 'unknown');
|
||||
const mixed = group(answerValue === 'yes' ? 'or' : 'and', fact(f.forecastFact), leader(f.leader));
|
||||
const asked = ask(declare(f).game, f.asker, f.target, mixed);
|
||||
assert.deepEqual(viewGame(asked, f.target).truthMixedRevivalAnswers, [answerValue]);
|
||||
rejected(asked, f.target, { type: 'truthAnswer', answer: answerValue === 'yes' ? 'no' : 'yes' });
|
||||
let game = answer(asked, f.target, answerValue); assert.equal(promise(game).fulfilled, false);
|
||||
if (answerValue === 'yes') {
|
||||
game = settle(act(game, f.target, f.revivalAction)); assert.equal(promise(game).fulfilled, true);
|
||||
assert.deepEqual(promise(game).progress.leaders, [f.leader]);
|
||||
} else {
|
||||
rejected(game, f.target, f.revivalAction); game = settle(act(game, f.target, forceAction));
|
||||
assert.equal(promise(game).progress.normal, 1); assert.equal(promise(game).fulfilled, false);
|
||||
}
|
||||
game = finish(game); assert.equal(promise(game).fulfilled, true); truthOnce(game, f.truth);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native mixed OR preserves the manual force or leader alternative instead of pinning its guide', () => {
|
||||
const f = fixture(), mixed = group('or', group('and', fact(f.spiceFact), force()), leader(f.leader));
|
||||
let game = bind(f, 'yes', mixed).game;
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.ok(guide.action.type === 'revive' || guide.action.type === 'reviveLeader');
|
||||
const manual = guide.action.type === 'revive' ? f.revivalAction : forceAction;
|
||||
assert.notEqual(manual.type, guide.action.type); game = settle(act(game, f.target, manual));
|
||||
assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(promise(game).progress.total, manual.type === 'revive' ? 1 : 0);
|
||||
assert.deepEqual(promise(game).progress.leaders, manual.type === 'reviveLeader' ? [f.leader] : []);
|
||||
rejected(game, f.target, guide.action); assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native mixed partial normal returns retain a larger force OR until the real alternate leader return', () => {
|
||||
const f = fixture(), mixed = group('and', fact(f.spiceFact), group('or', force(2, 'normal'), leader(f.leader)));
|
||||
let game = bind(f, 'yes', mixed).game; game = settle(act(game, f.target, forceAction));
|
||||
assert.equal(promise(game).progress.normal, 1); assert.equal(promise(game).fulfilled, false);
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'reviveLeader'); game = settle(act(game, f.target, guide.action));
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(promise(game).progress.total, 1);
|
||||
assert.deepEqual(promise(game).progress.leaders, [f.leader]); assert.equal(player(game, f.target).spice, 3);
|
||||
assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
for (const first of ['forces', 'leader'] as const) void test(`native mixed whole AND No permits ${first} first and rejects only whole completion`, () => {
|
||||
const f = fixture(); let game = bind(f, 'no').game;
|
||||
game = settle(act(game, f.target, first === 'forces' ? forceAction : f.revivalAction));
|
||||
assert.equal(promise(game).fulfilled, false); assert.equal(promise(game).progress.total, first === 'forces' ? 1 : 0);
|
||||
assert.deepEqual(promise(game).progress.leaders, first === 'leader' ? [f.leader] : []);
|
||||
rejected(game, f.target, first === 'forces' ? f.revivalAction : forceAction);
|
||||
game = finish(game); assert.equal(promise(game).fulfilled, true); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native mixed whole OR No forbids both qualifying ordinary returns through the native deadline', () => {
|
||||
const f = fixture(), mixed = group('or', fact(f.falseSpiceFact), force(), leader(f.leader));
|
||||
let game = bind(f, 'no', mixed).game;
|
||||
rejected(game, f.target, forceAction); rejected(game, f.target, f.revivalAction);
|
||||
assert.deepEqual(promise(game).progress, progress0); game = finish(game);
|
||||
assert.equal(promise(game).fulfilled, true); assert.equal(player(game, f.target).spice, 4);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native prior ordinary returns start outside a newly answered mixed interval', () => {
|
||||
const f = fixture(); let game = settle(act(f.game, f.target, forceAction));
|
||||
assert.equal(player(game, f.target).revived, 1); assert.equal(player(game, f.target).spice, 5);
|
||||
const mixed = group('and', fact(f.spiceFact), group('or', force(), leader(f.leader)));
|
||||
game = bind(f, 'yes', mixed, game).game;
|
||||
assert.deepEqual(promise(game).progress, progress0); assert.equal(promise(game).fulfilled, false);
|
||||
game = settle(act(game, f.target, f.revivalAction));
|
||||
assert.deepEqual(promise(game).progress, { ...progress0, leaders: [f.leader] });
|
||||
assert.equal(promise(game).fulfilled, true); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native original Ghola stays outside the mixed ordinary-return progress and No restriction', () => {
|
||||
const f = fixture({ ghola: true }); assert.ok(f.ghola);
|
||||
let game = bind(f, 'no', group('and', fact(f.spiceFact), group('or', force(), leader(f.leader)))).game;
|
||||
game = settle(act(game, f.target, { type: 'card', card: f.ghola, leader: f.leader }));
|
||||
assert.equal(player(game, f.target).leaders.find(disc => disc.id === f.leader)!.dead, false);
|
||||
assert.equal(player(game, f.target).leaderRevived, false); assert.equal(player(game, f.target).spice, 5);
|
||||
assert.deepEqual(promise(game).progress, progress0); assert.equal(promise(game).fulfilled, false);
|
||||
rejected(game, f.target, forceAction); game = finish(game); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(game.discard.filter(card => card.id === f.ghola).length, 1); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native mixed projection exposes only owned raw question aggregate and filters its pure residual', () => {
|
||||
const f = fixture(), game = bind(f).game;
|
||||
for (const actor of [f.target, f.asker, f.opponent]) {
|
||||
const view = viewGame(game, actor); assert.ok(!('mixedRevivalQuestions' in view));
|
||||
assert.deepEqual(view.revivalExpressionPromises, []); assert.equal(view.truthMixedRevivalAnswers, null);
|
||||
assert.deepEqual(view.truthHistory.at(-1)?.question, f.question); assert.equal(view.truthHistory.at(-1)?.answer, 'yes');
|
||||
if (actor !== f.target) {
|
||||
assert.deepEqual(view.mixedRevivalPromises, []); assert.equal(view.revivalExpressionPromiseGuide, null);
|
||||
} else {
|
||||
const row = promise(game);
|
||||
assert.deepEqual(Object.keys(row).sort(), ['answer', 'asker', 'event', 'fulfilled', 'mixed', 'player', 'progress', 'released', 'turn']);
|
||||
assert.deepEqual(row.mixed, f.mixed); assert.deepEqual(row.progress, progress0);
|
||||
assert.equal(row.player, f.target); assert.equal(row.asker, f.asker); assert.equal(row.turn, game.turn);
|
||||
assert.equal(typeof row.answer, 'boolean'); assert.equal(typeof row.fulfilled, 'boolean'); assert.equal(typeof row.released, 'boolean');
|
||||
assert.ok(view.revivalExpressionPromiseGuide?.action.revivalExpressionPromiseEvent);
|
||||
assert.ok(!JSON.stringify(row).includes('"claim"'));
|
||||
}
|
||||
}
|
||||
assert.equal(game.revivalExpressionPromises?.length, 1);
|
||||
assert.equal(game.revivalExpressionPromises![0].event, promise(game).event);
|
||||
});
|
||||
|
||||
void test('native mixed question and guide preserve exact owner turn event and JSON packets', () => {
|
||||
const f = fixture(), declared = declare(f);
|
||||
assert.ok(viewGame(declared.game, f.asker).truthMixedRevivalTargets.includes(f.target));
|
||||
rejected(declared.game, f.opponent, f.questionAction);
|
||||
rejected(declared.game, f.asker, { type: 'truthAsk', question: { ...f.question, turn: f.game.turn + 1 } });
|
||||
rejected(declared.game, f.asker, { type: 'truthAsk', question: { ...f.question,
|
||||
mixed: group('and', fact(f.spiceFact), leader('atreides-0')) } });
|
||||
let game = ask(declared.game, f.asker, f.target, f.mixed);
|
||||
assert.equal(viewGame(game, f.asker).truthMixedRevivalAnswers, null);
|
||||
assert.deepEqual(viewGame(game, f.target).truthMixedRevivalAnswers, ['yes', 'no']);
|
||||
rejected(game, f.asker, { type: 'truthAnswer', answer: 'yes' }); game = answer(game, f.target, 'yes');
|
||||
const guide = viewGame(game, f.target).revivalExpressionPromiseGuide; assert.ok(guide); assert.equal(guide.phase, 4);
|
||||
assert.equal(guide.action.revivalExpressionPromiseEvent, promise(game).event);
|
||||
rejected(game, f.opponent, guide.action);
|
||||
rejected(game, f.target, { ...guide.action, revivalExpressionPromiseEvent: 'stale-mixed-revival-event' });
|
||||
rejected(game, f.target, { ...guide.action, amount: 20, elite: 0 });
|
||||
const wire: Action = JSON.parse(JSON.stringify(guide.action)); game = settle(act(game, f.target, wire));
|
||||
rejected(game, f.target, wire); game = complete(game, f.target);
|
||||
rejected(game, f.target, wire); rejected(finish(game), f.target, wire);
|
||||
truthOnce(game, f.truth); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native mixed post-Storm Yes crosses real Spice Charity Bidding and original ordered Revival', () => {
|
||||
const f = fixture({ advanced: true, window: 'waiting' });
|
||||
assert.equal(f.postStorm.turn, 2); assert.equal(f.postStorm.phase, 1);
|
||||
assert.equal(f.sourceCounts.postStorm, 83); assert.deepEqual(f.postStorm, f.storm.after);
|
||||
assert.equal(f.postStormSourceActions.length, f.postStormActionCount);
|
||||
assert.deepEqual(f.sourceActions.slice(f.postStormActionCount, f.phase3ActionCount), f.bidding.steps);
|
||||
let game = bind(f, 'yes', f.mixed, f.postStorm).game;
|
||||
assert.deepEqual(promise(game).progress, progress0); assert.equal(viewGame(game, f.target).revivalExpressionPromiseGuide, null);
|
||||
const actions: { actor: string; action: Action }[] = [];
|
||||
game = advance(game, state => state.phase === 4 && clean(state) && state.revivalOrder?.remaining[0] === f.target, actions);
|
||||
assert.ok(actions.some(step => step.action.type === 'passBid')); assert.ok(actions.some(step => step.action.type === 'ready'));
|
||||
assert.deepEqual(game.revivalOrder!.original, f.game.revivalOrder!.original);
|
||||
assert.deepEqual(promise(game).progress, progress0); assert.equal(player(game, f.target).spice, 4);
|
||||
game = complete(game, f.target); assert.equal(player(game, f.target).spice, 3);
|
||||
assert.deepEqual(inventory(finish(game)), f.originalCards);
|
||||
});
|
||||
|
||||
void test('native waiting mixed Yes exposes the common guide only at its original owned opportunity', () => {
|
||||
const f = fixture({ advanced: true, window: 'waiting' }); assert.notEqual(f.game.revivalOrder!.remaining[0], f.target);
|
||||
let game = bind(f).game; assert.equal(viewGame(game, f.target).revivalExpressionPromiseGuide, null);
|
||||
rejected(game, f.target, forceAction); rejected(game, f.target, f.revivalAction);
|
||||
game = advance(game, state => clean(state) && state.revivalOrder?.remaining[0] === f.target);
|
||||
assert.deepEqual(game.revivalOrder!.original, f.game.revivalOrder!.original);
|
||||
game = complete(game, f.target); assert.equal(promise(game).fulfilled, true);
|
||||
assert.equal(player(game, f.target).spice, 3); assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
for (const permitted of [false, true]) void test(`native mixed personally priced early leader ${permitted ? 'Yes pays the real quote' : 'No rejects the real acceptance'}`, () => {
|
||||
const f = fixture({ advanced: true, window: 'waiting', bothDead: true });
|
||||
assert.equal(player(f.afterBattle, f.opponent).leaders.find(disc => disc.id === f.opponentLeader)!.dead, true);
|
||||
assert.equal(player(f.game, f.opponent).leaders.filter(disc => !disc.dead).length, 4);
|
||||
assert.equal(f.game.revivalOrder!.remaining[0], f.opponent);
|
||||
let game = act(f.game, f.opponent, { type: 'requestLeaderRevival', leader: f.opponentLeader });
|
||||
game = act(game, f.target, { type: 'quoteLeaderRevival', target: f.opponent, amount: 3 });
|
||||
const before = player(game, f.opponent).spice, payee = player(game, f.target).spice;
|
||||
game = declare(f, game).game;
|
||||
game = ask(game, f.asker, f.opponent, group('and', fact({ kind: 'spice', compare: 'gte', value: 3 }), leader(f.opponentLeader)));
|
||||
game = answer(game, f.opponent, permitted ? 'yes' : 'no');
|
||||
if (permitted) {
|
||||
const guide = viewGame(game, f.opponent).revivalExpressionPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'acceptLeaderRevival'); assert.equal(guide.action.leaderRevivalPrice, 3);
|
||||
assert.equal(guide.action.revivalExpressionPromiseEvent, promise(game, f.opponent).event);
|
||||
rejected(game, f.opponent, { ...guide.action, leaderRevivalPrice: 2 });
|
||||
game = settle(act(game, f.opponent, guide.action));
|
||||
assert.equal(player(game, f.opponent).spice, before - 3); assert.equal(player(game, f.target).spice, payee + 3);
|
||||
assert.equal(player(game, f.opponent).leaderRevived, true); assert.equal(promise(game, f.opponent).fulfilled, true);
|
||||
assert.deepEqual(promise(game, f.opponent).progress.leaders, [f.opponentLeader]);
|
||||
} else {
|
||||
rejected(game, f.opponent, { type: 'acceptLeaderRevival', leaderRevivalPrice: 3 });
|
||||
assert.equal(player(game, f.opponent).spice, before);
|
||||
assert.equal(player(game, f.opponent).leaders.find(disc => disc.id === f.opponentLeader)!.dead, true);
|
||||
}
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('all four minimal policies actually answer mixed Yes and perform exact native ordinary return guides', () => {
|
||||
const f = fixture(), asked = ask(declare(f).game, f.asker, f.target, f.mixed);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
let game = structuredClone(asked);
|
||||
const answering = viewGame(game, f.target); answering.players.find(seat => seat.id === f.target)!.bot = difficulty;
|
||||
const answerAction = botActions(answering).find(action => action.type === 'truthAnswer'); assert.ok(answerAction, difficulty);
|
||||
assert.equal(answerAction.answer, 'yes', difficulty); assert.ok(answering.truthMixedRevivalAnswers?.includes('yes'));
|
||||
game = act(game, f.target, answerAction);
|
||||
for (let count = 0; count < 4 && !promise(game).fulfilled; count++) {
|
||||
const view = viewGame(game, f.target); view.players.find(seat => seat.id === f.target)!.bot = difficulty;
|
||||
const guide = view.revivalExpressionPromiseGuide; assert.ok(guide, difficulty);
|
||||
const action = botActions(view).find(action => action.revivalExpressionPromiseEvent === guide.action.revivalExpressionPromiseEvent);
|
||||
assert.ok(action, difficulty); assert.deepEqual(action, guide.action); game = settle(act(game, f.target, action));
|
||||
}
|
||||
assert.equal(promise(game).fulfilled, true, difficulty); assert.equal(player(game, f.target).spice, 3);
|
||||
assert.equal(player(game, f.target).tanks, 0); assert.equal(player(game, f.target).freeForcesRevived, 1);
|
||||
assert.deepEqual(promise(game).progress, { total: 1, normal: 1, elite: 0, leaders: [f.leader] });
|
||||
truthOnce(game, f.truth); assert.deepEqual(inventory(game), f.originalCards);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user