Support Truthtrance charity commitments in Basic play
This commit is contained in:
1 parent
6a31315c95
commit
72ff9db5e8
18 files changed
+815
-5
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import {
|
||||
bindCharityPromise, charityPromiseAnswers, CharityPromiseError,
|
||||
guardCharityClaim, guardCharityReady, liveCharityPromises, reconcileCharityPromises, validateCharityPromises,
|
||||
type CharityPromise,
|
||||
} from '../game/charity-promises';
|
||||
|
||||
const ctx = { player: 'emperor', asker: 'atreides', turn: 3 };
|
||||
const empty: CharityPromise[] = [];
|
||||
const bind = (rows: readonly CharityPromise[], answer: boolean, canClaim = true) =>
|
||||
bindCharityPromise(rows, ctx.player, ctx.asker, ctx.turn, answer, canClaim);
|
||||
|
||||
void test('Charity answer availability follows actual eligibility and incompatible earlier answers', () => {
|
||||
assert.deepEqual(charityPromiseAnswers(empty, ctx.player, ctx.turn, true), ['yes', 'no']);
|
||||
assert.deepEqual(charityPromiseAnswers(empty, ctx.player, ctx.turn, false), ['no']);
|
||||
assert.deepEqual(charityPromiseAnswers(bind(empty, true), ctx.player, ctx.turn, true), ['yes']);
|
||||
assert.deepEqual(charityPromiseAnswers(bind(empty, false), ctx.player, ctx.turn, true), ['no']);
|
||||
assert.throws(() => bind(bind(empty, true), false), CharityPromiseError);
|
||||
assert.throws(() => bind(bind(empty, false), true), CharityPromiseError);
|
||||
assert.throws(() => bind(empty, true, false), CharityPromiseError);
|
||||
assert.throws(() => bindCharityPromise(empty, ctx.player, ctx.player, ctx.turn, false, true), CharityPromiseError);
|
||||
assert.deepEqual(empty, []);
|
||||
});
|
||||
|
||||
void test('a Yes blocks ready until the actual claim and a No blocks the actual claim', () => {
|
||||
const yes = bind(bind(empty, true), true);
|
||||
assert.throws(() => guardCharityReady(yes, ctx.player, ctx.turn, true), CharityPromiseError);
|
||||
assert.doesNotThrow(() => guardCharityReady(yes, ctx.player, ctx.turn, false));
|
||||
const claimed = guardCharityClaim(yes, ctx.player, ctx.turn);
|
||||
assert.equal(claimed.every(row => row.fulfilled && !row.released), true);
|
||||
assert.equal(yes.every(row => !row.fulfilled), true, 'the original input is unchanged');
|
||||
assert.doesNotThrow(() => guardCharityReady(claimed, ctx.player, ctx.turn, true));
|
||||
assert.throws(() => guardCharityClaim(bind(empty, false), ctx.player, ctx.turn), CharityPromiseError);
|
||||
});
|
||||
|
||||
void test('an external loss releases Yes, while a voluntary loss or phase exit rejects unchanged', () => {
|
||||
const yes = bind(empty, true);
|
||||
for (const phaseOpen of [true, false]) {
|
||||
assert.throws(() => reconcileCharityPromises(yes, ctx.player, ctx.turn,
|
||||
{ phaseOpen, canClaim: false, change: 'voluntary' }), CharityPromiseError);
|
||||
assert.equal(yes[0].released, false);
|
||||
const released = reconcileCharityPromises(yes, ctx.player, ctx.turn,
|
||||
{ phaseOpen, canClaim: false, change: 'external' });
|
||||
assert.equal(released[0].released, true);
|
||||
assert.equal(released[0].fulfilled, false);
|
||||
}
|
||||
});
|
||||
|
||||
void test('No survives Basic ready while the phase is open and finishes at phase close', () => {
|
||||
const no = bind(empty, false);
|
||||
const ready = reconcileCharityPromises(no, ctx.player, ctx.turn,
|
||||
{ phaseOpen: true, canClaim: true, change: 'voluntary' });
|
||||
assert.deepEqual(liveCharityPromises(ready, ctx.player, ctx.turn), no);
|
||||
assert.throws(() => guardCharityClaim(ready, ctx.player, ctx.turn), CharityPromiseError);
|
||||
const closed = reconcileCharityPromises(ready, ctx.player, ctx.turn,
|
||||
{ phaseOpen: false, canClaim: false, change: 'voluntary' });
|
||||
assert.equal(closed[0].fulfilled, true);
|
||||
assert.equal(closed[0].released, false);
|
||||
assert.deepEqual(liveCharityPromises(closed, ctx.player, ctx.turn), []);
|
||||
});
|
||||
|
||||
void test('promises for other players, turns and completed answers are unaffected', () => {
|
||||
const yes = bind(empty, true);
|
||||
const other = bindCharityPromise(yes, 'guild', 'atreides', 3, true, true);
|
||||
const later = bindCharityPromise(other, 'emperor', 'atreides', 4, false, true);
|
||||
const settled = guardCharityClaim(later, ctx.player, ctx.turn);
|
||||
assert.equal(settled[0].fulfilled, true);
|
||||
assert.deepEqual(settled.slice(1), later.slice(1));
|
||||
assert.deepEqual(charityPromiseAnswers(settled, ctx.player, ctx.turn, true), ['yes', 'no']);
|
||||
assert.throws(() => liveCharityPromises(settled, ctx.player, 0), CharityPromiseError);
|
||||
});
|
||||
|
||||
void test('every answer has a stable unique receipt event through claim, release and closure', () => {
|
||||
const yes = bindCharityPromise(empty, ctx.player, ctx.asker, ctx.turn, true, true, 'event-yes');
|
||||
assert.equal(yes[0].event, 'event-yes');
|
||||
assert.equal(guardCharityClaim(yes, ctx.player, ctx.turn)[0].event, 'event-yes');
|
||||
assert.equal(reconcileCharityPromises(yes, ctx.player, ctx.turn,
|
||||
{ phaseOpen: true, canClaim: false, change: 'external' })[0].event, 'event-yes');
|
||||
const no = bindCharityPromise(yes, 'guild', ctx.asker, ctx.turn, false, true, 'event-no');
|
||||
assert.equal(reconcileCharityPromises(no, 'guild', ctx.turn,
|
||||
{ phaseOpen: false, canClaim: false, change: 'voluntary' })[1].event, 'event-no');
|
||||
assert.throws(() => bindCharityPromise(yes, 'guild', ctx.asker, ctx.turn, true, true, 'event-yes'), CharityPromiseError);
|
||||
assert.throws(() => validateCharityPromises([{ ...yes[0], event: '' }]), CharityPromiseError);
|
||||
assert.throws(() => validateCharityPromises([{ ...yes[0], fulfilled: true, released: true }]), CharityPromiseError);
|
||||
assert.ok(bind(empty, false)[0].event, 'normal binding assigns an event');
|
||||
});
|
||||
@@ -355,6 +355,7 @@ void test('charity projections expose the payer and own entitlement without leak
|
||||
incomeCanceled: false,
|
||||
incomeHomeworld: 0,
|
||||
orderBlocked: null,
|
||||
truthBlocked: null,
|
||||
});
|
||||
assert.equal(v.players[0].spice, undefined);
|
||||
assert.equal(v.players[0].handLimit, 5);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { charityQuote } from '../game/charity';
|
||||
import { type Action, type Game, viewGame } from '../game/engine';
|
||||
import { type TruthQuestion } from '../game/truthtrance';
|
||||
import {
|
||||
actTruthCharityPayments as act,
|
||||
createTruthCharityPaymentsFixture,
|
||||
declareTruthCharityPayments,
|
||||
truthCharityPaymentsInventory as inventory,
|
||||
truthCharityPaymentsPlayer as player,
|
||||
} from './fixture-truth-charity-payments';
|
||||
|
||||
export { act, inventory, player };
|
||||
|
||||
/** Original Basic classic factions with the selected Ix deck and original
|
||||
* Truth cards, genuine Amal and Charity chronology. The only additional
|
||||
* preparation is a real four-spice bribe, leaving the target at one spice;
|
||||
* no wallet, cards, phase or readiness are installed by the fixture. */
|
||||
export function createTruthCharityCommitmentFixture() {
|
||||
const base = createTruthCharityPaymentsFixture({ source: 'phase2', advanced: false });
|
||||
assert.equal(base.game.phase, 2);
|
||||
assert.equal(player(base.game, base.target).spice, 5);
|
||||
const game = act(base.game, base.target, { type: 'bribe', target: base.asker, amount: 4 });
|
||||
assert.equal(player(game, base.target).spice, 1);
|
||||
assert.equal(charityQuote(game, player(game, base.target)).total, 1);
|
||||
assert.deepEqual(inventory(game), base.originalCards);
|
||||
return { ...base, game, unprepared: structuredClone(base.game), source: structuredClone(game),
|
||||
charityQuestion: { kind: 'charity', target: base.target, turn: game.turn } satisfies TruthQuestion };
|
||||
}
|
||||
export type TruthCharityCommitmentFixture = ReturnType<typeof createTruthCharityCommitmentFixture>;
|
||||
|
||||
export function askTruthCharityCommitment(f: TruthCharityCommitmentFixture, source = f.game, copy: 0 | 1 = 0) {
|
||||
const declared = declareTruthCharityPayments(f, source, copy).game;
|
||||
return act(declared, f.asker, { type: 'truthAsk', question: f.charityQuestion });
|
||||
}
|
||||
export function bindTruthCharityCommitment(f: TruthCharityCommitmentFixture, answer: 'yes' | 'no', source = f.game, copy: 0 | 1 = 0) {
|
||||
const asked = askTruthCharityCommitment(f, source, copy);
|
||||
assert.ok(viewGame(asked, f.target).truthCharityAnswers?.includes(answer));
|
||||
return { asked, game: act(asked, f.target, { type: 'truthAnswer', answer }) };
|
||||
}
|
||||
export function rejectTruthCharityCommitment(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, actor, action));
|
||||
assert.deepEqual(game, before, 'Rejected actions preserve the input');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { Children, createElement, isValidElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { viewGame, type Action, type GameView } from '../game/engine';
|
||||
import { act, askTruthCharityCommitment as ask, bindTruthCharityCommitment as bind,
|
||||
createTruthCharityCommitmentFixture as fixture, inventory, player, rejectTruthCharityCommitment as reject } from './fixture-truth-charity-commitment';
|
||||
import { declareTruthCharityPayments } from './fixture-truth-charity-payments';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
load(url, context, next) {
|
||||
if (!url.endsWith('.module.css')) return next(url, context);
|
||||
const css = readFileSync(new URL(url), 'utf8');
|
||||
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
|
||||
return { format: 'module', source: `export default ${JSON.stringify(classes)}`, shortCircuit: true };
|
||||
},
|
||||
});
|
||||
const { GameTable } = await import('../components/game-table');
|
||||
const { Truthtrance } = await import('../components/truthtrance');
|
||||
const { TruthCharityPromise } = await import('../components/truth-charity-promise');
|
||||
aliases.deregister();
|
||||
const table = (game: GameView) => renderToStaticMarkup(createElement(GameTable, { game, busy: false, send: async () => {}, onExit() {} }));
|
||||
const truth = (game: GameView) => renderToStaticMarkup(createElement(Truthtrance, { game, busy: false, act() {} }));
|
||||
|
||||
void test('Basic native Charity question, private answer controls and guided claim appear in the real table', () => {
|
||||
const f = fixture(); const declared = declareTruthCharityPayments(f).game;
|
||||
const asker = viewGame(declared, f.asker);
|
||||
assert.ok(asker.truthCharityTargets.includes(f.target));
|
||||
assert.match(truth(asker), /<option value="charity">[^<]*[Cc]harity/);
|
||||
assert.match(table(asker), /[Cc]harity/);
|
||||
// SSR confirms the selection is present; engine actions supply the question.
|
||||
const asked = ask(f), answerView = viewGame(asked, f.target);
|
||||
assert.equal(answerView.truthtrance?.question?.kind, 'charity');
|
||||
assert.ok(answerView.truthCharityAnswers?.includes('yes'));
|
||||
assert.ok(answerView.truthCharityAnswers?.includes('no'));
|
||||
assert.equal(viewGame(asked, f.asker).truthCharityAnswers, null);
|
||||
assert.match(truth(answerView), /Answer Yes/);
|
||||
assert.match(truth(answerView), /Answer No/);
|
||||
assert.match(table(answerView), /Answer Yes/);
|
||||
const bound = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
const own = viewGame(bound, f.target), guide = own.truthCharityGuide;
|
||||
assert.ok(guide); assert.equal(guide.action.type, 'charity');
|
||||
assert.match(table(own), /[Cc]harity/);
|
||||
const claimed = act(bound, f.target, guide.action);
|
||||
assert.equal(player(claimed, f.target).charityTurn, claimed.turn);
|
||||
assert.deepEqual(inventory(claimed), f.originalCards);
|
||||
});
|
||||
|
||||
void test('Basic No answer keeps its own readable Charity restriction and blocks the physical claim', () => {
|
||||
const f = fixture(), bound = bind(f, 'no').game;
|
||||
const own = viewGame(bound, f.target);
|
||||
assert.match(table(own), /[Cc]harity/);
|
||||
assert.throws(() => act(bound, f.target, { type: 'charity' }));
|
||||
assert.deepEqual(inventory(bound), f.originalCards);
|
||||
});
|
||||
|
||||
void test('A live Charity bribe guide sends the native packet and cannot replay in a No continuation', () => {
|
||||
const f = fixture(), asked = ask(f, f.unprepared);
|
||||
const yes = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
const own = viewGame(yes, f.target), sent: Action[] = [];
|
||||
assert.equal(own.truthCharityGuide?.action.type, 'bribe');
|
||||
const panel = TruthCharityPromise({ game: own, busy: false, send(action) { sent.push(action); } });
|
||||
assert.ok(panel);
|
||||
type GuideProps = { 'data-testid'?: string; disabled?: boolean; onClick?: () => void };
|
||||
const button = Children.toArray(panel.props.children).find(node =>
|
||||
isValidElement<GuideProps>(node) && node.props['data-testid'] === 'charity-promise-guide');
|
||||
assert.ok(isValidElement<GuideProps>(button));
|
||||
assert.equal(button.props.disabled, false);
|
||||
assert.ok(button.props.onClick); button.props.onClick();
|
||||
assert.deepEqual(sent, [own.truthCharityGuide!.action]);
|
||||
const paid = act(yes, f.target, sent[0]);
|
||||
assert.ok(player(paid, f.target).spice < 2);
|
||||
const no = act(asked, f.target, { type: 'truthAnswer', answer: 'no' });
|
||||
reject(no, f.target, sent[0]);
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { charityQuote } from '../game/charity';
|
||||
import { declareTruthCharityPayments } from './fixture-truth-charity-payments';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { act, askTruthCharityCommitment as ask, bindTruthCharityCommitment as bind,
|
||||
createTruthCharityCommitmentFixture as fixture, inventory, player, rejectTruthCharityCommitment as reject } from './fixture-truth-charity-commitment';
|
||||
|
||||
void test('Basic eligible Charity Yes reopens ready and requires one actual native claim', () => {
|
||||
const f = fixture();
|
||||
const source = act(f.game, f.target, { type: 'ready' });
|
||||
assert.ok(source.ready.includes(f.target));
|
||||
const { asked, game: bound } = bind(f, 'yes', source);
|
||||
assert.equal(viewGame(asked, f.asker).truthCharityAnswers, null);
|
||||
assert.ok(!bound.ready.includes(f.target));
|
||||
assert.equal(bound.discard.filter(card => card.id === f.truth).length, 1);
|
||||
assert.equal(player(bound, f.target).charityTurn, undefined);
|
||||
reject(bound, f.target, { type: 'ready' });
|
||||
const guide = viewGame(bound, f.target).truthCharityGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type, 'charity');
|
||||
const claimed = act(bound, f.target, guide.action);
|
||||
assert.equal(player(claimed, f.target).charityTurn, claimed.turn);
|
||||
assert.equal(player(claimed, f.target).spice, 2);
|
||||
assert.equal(charityQuote(claimed, player(claimed, f.target)).total, 0);
|
||||
reject(claimed, f.target, { type: 'charity' });
|
||||
const ready = act(claimed, f.target, { type: 'ready' });
|
||||
assert.ok(ready.ready.includes(f.target));
|
||||
assert.deepEqual(inventory(ready), f.originalCards);
|
||||
});
|
||||
|
||||
void test('Basic Charity No prohibits the claim through ready and normal phase close', () => {
|
||||
const f = fixture(); const source = act(f.game, f.target, { type: 'ready' });
|
||||
const { game: bound } = bind(f, 'no', source);
|
||||
assert.ok(!bound.ready.includes(f.target));
|
||||
reject(bound, f.target, { type: 'charity' });
|
||||
let game = act(bound, f.target, { type: 'ready' });
|
||||
assert.ok(game.ready.includes(f.target));
|
||||
reject(game, f.target, { type: 'charity' });
|
||||
// A second actual Truthtrance answer reopens concurrent Basic readiness.
|
||||
// The first No must still forbid claiming through the same Charity phase.
|
||||
game = bind(f, 'no', game, 1).game;
|
||||
assert.ok(!game.ready.includes(f.target));
|
||||
reject(game, f.target, { type: 'charity' });
|
||||
game = act(game, f.target, { type: 'ready' });
|
||||
for (const p of game.players) if (!game.ready.includes(p.id) && game.phase === 2)
|
||||
game = act(game, p.id, { type: 'ready' });
|
||||
assert.notEqual(game.phase, 2);
|
||||
assert.equal(player(game, f.target).charityTurn, undefined);
|
||||
assert.equal(player(game, f.target).spice, 1);
|
||||
assert.deepEqual(inventory(game), f.originalCards);
|
||||
});
|
||||
|
||||
void test('Basic stale Charity questions and wrong answerers cannot change the native phase', () => {
|
||||
const f = fixture();
|
||||
const declared = declareTruthCharityPayments(f).game;
|
||||
reject(declared, f.asker, { type: 'truthAsk', question: { ...f.charityQuestion, turn: f.game.turn + 1 } });
|
||||
const asked = ask(f);
|
||||
reject(asked, f.asker, { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.deepEqual(inventory(asked), f.originalCards);
|
||||
});
|
||||
|
||||
void test('Basic all four minimal bot profiles answer and use an allowed Charity continuation', () => {
|
||||
const f = fixture(); const asked = ask(f);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const answerView = viewGame(asked, f.target);
|
||||
answerView.players.find(row => row.id === f.target)!.bot = difficulty;
|
||||
const answer = botActions(answerView)[0]; assert.ok(answer, difficulty);
|
||||
assert.equal(answer.type, 'truthAnswer');
|
||||
assert.ok(answerView.truthCharityAnswers?.includes(answer.answer as 'yes' | 'no'));
|
||||
const bound = act(structuredClone(asked), f.target, answer);
|
||||
if (answer.answer === 'yes') {
|
||||
const own = viewGame(bound, f.target); own.players.find(row => row.id === f.target)!.bot = difficulty;
|
||||
assert.ok(own.truthCharityGuide);
|
||||
const choice = botActions(own)[0]; assert.deepEqual(choice, own.truthCharityGuide.action);
|
||||
const after = act(bound, f.target, choice);
|
||||
assert.equal(player(after, f.target).charityTurn, after.turn);
|
||||
assert.deepEqual(inventory(after), f.originalCards);
|
||||
} else {
|
||||
reject(bound, f.target, { type: 'charity' });
|
||||
const own = viewGame(bound, f.target); own.players.find(row => row.id === f.target)!.bot = difficulty;
|
||||
const choice = botActions(own)[0]; assert.ok(choice, difficulty);
|
||||
const after = act(bound, f.target, choice);
|
||||
assert.deepEqual(inventory(after), f.originalCards);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('Basic Charity Yes at five spice guides an actual bribe and claim; its guide cannot replay against No', () => {
|
||||
const f = fixture();
|
||||
// This is the actual pre-bribe native source, with five spendable spice.
|
||||
assert.equal(player(f.unprepared, f.target).spice, 5);
|
||||
const asked = ask(f, f.unprepared);
|
||||
assert.ok(viewGame(asked, f.target).truthCharityAnswers?.includes('yes'));
|
||||
const yes = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
reject(yes, f.target, { type: 'ready' });
|
||||
const bribe = viewGame(yes, f.target).truthCharityGuide; assert.ok(bribe);
|
||||
assert.equal(bribe.action.type, 'bribe');
|
||||
assert.ok(typeof bribe.action.amount === 'number' && bribe.action.amount >= 4);
|
||||
const paid = act(yes, f.target, bribe.action);
|
||||
assert.ok(player(paid, f.target).spice < 2);
|
||||
assert.ok(viewGame(paid, f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
|
||||
reject(paid, f.target, bribe.action);
|
||||
const claim = viewGame(paid, f.target).truthCharityGuide; assert.ok(claim);
|
||||
assert.equal(claim.action.type, 'charity');
|
||||
const claimed = act(paid, f.target, claim.action);
|
||||
assert.equal(player(claimed, f.target).charityTurn, claimed.turn);
|
||||
assert.ok(viewGame(claimed, f.target).truthCharityPromises.some(row => row.answer && row.fulfilled));
|
||||
assert.deepEqual(inventory(claimed), f.originalCards);
|
||||
|
||||
// The same actual question has a separate valid No continuation; a normal
|
||||
// bribe remains legal here, so rejecting the old Yes packet requires its
|
||||
// original guide identity, not simply a no-bribes rule.
|
||||
const no = act(asked, f.target, { type: 'truthAnswer', answer: 'no' });
|
||||
reject(no, f.target, bribe.action);
|
||||
const ordinaryBribe = act(no, f.target, { type: 'bribe', target: f.asker, amount: 1 });
|
||||
assert.equal(player(ordinaryBribe, f.target).spice, 4);
|
||||
assert.deepEqual(inventory(ordinaryBribe), f.originalCards);
|
||||
});
|
||||
|
||||
void test('A controlled external spice gain retains Charity Yes while an actual bribe route remains', () => {
|
||||
const f = fixture(); const bound = bind(f, 'yes').game;
|
||||
assert.equal(player(bound, f.target).spice, 1);
|
||||
// Controlled supplement: transfer three actual spice between two seated
|
||||
// wallets, representing an external gain. The native fixture makes Emperor
|
||||
// the respondent, so it cannot receive its own allied Emperor gift. No card,
|
||||
// phase, readiness or promise is manufactured; the sum stays unchanged.
|
||||
const changed = structuredClone(bound);
|
||||
const donor = changed.players.find(p => p.id !== f.target && p.spice >= 3); assert.ok(donor);
|
||||
const total = changed.players.reduce((sum, p) => sum + p.spice, 0);
|
||||
donor.spice -= 3; player(changed, f.target).spice += 3;
|
||||
assert.equal(changed.players.reduce((sum, p) => sum + p.spice, 0), total);
|
||||
const observer = changed.players.find(p => p.id !== f.target && !changed.ready.includes(p.id)); assert.ok(observer);
|
||||
const reconciled = act(changed, observer.id, { type: 'ready' });
|
||||
assert.equal(player(reconciled, f.target).spice, 4);
|
||||
assert.ok(viewGame(reconciled, f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
|
||||
reject(reconciled, f.target, { type: 'ready' });
|
||||
const bribe = viewGame(reconciled, f.target).truthCharityGuide; assert.ok(bribe);
|
||||
assert.equal(bribe.action.type, 'bribe');
|
||||
const paid = act(reconciled, f.target, bribe.action);
|
||||
assert.ok(player(paid, f.target).spice < 2);
|
||||
const claim = viewGame(paid, f.target).truthCharityGuide; assert.ok(claim);
|
||||
assert.equal(claim.action.type, 'charity');
|
||||
const completed = act(paid, f.target, claim.action);
|
||||
assert.equal(player(completed, f.target).charityTurn, completed.turn);
|
||||
assert.deepEqual(inventory(completed), f.originalCards);
|
||||
});
|
||||
Reference in new issue
Block a user