149 lines
8.3 KiB
TypeScript
149 lines
8.3 KiB
TypeScript
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);
|
|
});
|