179 lines
9.7 KiB
TypeScript
179 lines
9.7 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 { viewGame, type Action, type Game } from '../game/engine';
|
||
import { actTruthCharityPayments as act, createTruthCharityPaymentsFixture,
|
||
declareTruthCharityPayments, truthCharityPaymentsInventory as inventory,
|
||
truthCharityPaymentsPlayer as player } from './fixture-truth-charity-payments';
|
||
let original: ReturnType<typeof createTruthCharityPaymentsFixture> | undefined;
|
||
const source = () => original ??= createTruthCharityPaymentsFixture({source:'phase2',advanced:true});
|
||
|
||
/** Public Advanced selected-Ix setup, original starting cards and actual Amal
|
||
* chronology. A real bribe before asking makes the chosen future actor eligible
|
||
* for the native claim; no wallet, phase, order, readiness or promise is staged. */
|
||
function fixture(options: {poor?:boolean} = {}) {
|
||
const f = source(); let game = structuredClone(f.game);
|
||
assert.equal(game.advanced,true); assert.equal(game.phase,2);
|
||
const queue = game.charityOrder; assert.ok(queue && queue.remaining.length >= 3);
|
||
const target = queue.remaining.find(id => id !== f.asker && id !== queue.remaining[0]); assert.ok(target);
|
||
const amount = player(game,target).spice - 1;
|
||
if (options.poor !== false && amount > 0) game = act(game,target,{type:'bribe',target:f.asker,amount});
|
||
if (options.poor !== false) assert.ok(player(game,target).spice < 2);
|
||
assert.deepEqual(game.charityOrder,queue);
|
||
assert.equal(player(game,target).charityTurn,undefined);
|
||
return {f,game,target,asker:f.asker,queue:structuredClone(queue)};
|
||
}
|
||
function reject(game:Game, actor:string, action:Action) {
|
||
const before = structuredClone(game);
|
||
assert.throws(() => act(game,actor,action));
|
||
assert.deepEqual(game,before);
|
||
}
|
||
function ask(f:ReturnType<typeof fixture>, source:Game = f.game) {
|
||
const declared = declareTruthCharityPayments(f.f,source).game;
|
||
return act(declared,f.asker,{type:'truthAsk',question:{kind:'charity',target:f.target,turn:source.turn}});
|
||
}
|
||
function bind(f:ReturnType<typeof fixture>, answer:'yes'|'no', source:Game = f.game) {
|
||
const asked = ask(f,source);
|
||
assert.ok(viewGame(asked,f.target).truthCharityAnswers?.includes(answer));
|
||
return {asked,game:act(asked,f.target,{type:'truthAnswer',answer})};
|
||
}
|
||
function toTarget(game:Game, target:string):Game {
|
||
for (let count = 0; game.phase === 2 && game.charityOrder?.remaining[0] !== target && count < 6; count++) {
|
||
const actor = game.charityOrder?.remaining[0]; assert.ok(actor);
|
||
game = act(game,actor,{type:'ready'});
|
||
}
|
||
assert.equal(game.phase,2); assert.equal(game.charityOrder?.remaining[0],target);
|
||
return game;
|
||
}
|
||
|
||
void test('Advanced Charity Yes preserves the order and guides only the actual current turn', () => {
|
||
const f = fixture(); const {game: bound} = bind(f,'yes');
|
||
assert.deepEqual(bound.charityOrder,f.queue);
|
||
assert.equal(viewGame(bound,f.target).truthCharityGuide,null);
|
||
assert.ok(viewGame(bound,f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
|
||
reject(bound,f.target,{type:'charity'}); reject(bound,f.target,{type:'ready'});
|
||
let game = toTarget(bound,f.target);
|
||
reject(game,f.target,{type:'ready'});
|
||
const guide = viewGame(game,f.target).truthCharityGuide; assert.ok(guide);
|
||
assert.equal(guide.action.type,'charity');
|
||
const queue = structuredClone(game.charityOrder);
|
||
game = act(game,f.target,guide.action);
|
||
assert.deepEqual(game.charityOrder,queue);
|
||
assert.equal(player(game,f.target).charityTurn,game.turn);
|
||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => row.answer && row.fulfilled));
|
||
game = act(game,f.target,{type:'ready'});
|
||
assert.ok(game.phase !== 2 || !game.charityOrder?.remaining.includes(f.target));
|
||
assert.deepEqual(inventory(game),f.f.originalCards);
|
||
});
|
||
|
||
void test('Advanced Charity No remains binding through the owned turn then completes', () => {
|
||
const f = fixture(); const {game: bound} = bind(f,'no');
|
||
assert.deepEqual(bound.charityOrder,f.queue);
|
||
let game = toTarget(bound,f.target);
|
||
reject(game,f.target,{type:'charity'});
|
||
assert.equal(viewGame(game,f.target).truthCharityGuide,null);
|
||
game = act(game,f.target,{type:'ready'});
|
||
assert.ok(game.phase !== 2 || !game.charityOrder?.remaining.includes(f.target));
|
||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && row.fulfilled));
|
||
assert.equal(player(game,f.target).charityTurn,undefined);
|
||
assert.deepEqual(inventory(game),f.f.originalCards);
|
||
});
|
||
|
||
void test('Advanced Charity accepts a question during the respondent’s own unfinished turn', () => {
|
||
const f = fixture(), current = toTarget(f.game,f.target), queue = structuredClone(current.charityOrder);
|
||
const {game} = bind(f,'yes',current);
|
||
assert.deepEqual(game.charityOrder,queue);
|
||
assert.equal(game.charityOrder?.remaining[0],f.target);
|
||
reject(game,f.target,{type:'ready'});
|
||
const guide = viewGame(game,f.target).truthCharityGuide; assert.ok(guide);
|
||
const completed = act(game,f.target,guide.action);
|
||
assert.equal(player(completed,f.target).charityTurn,completed.turn);
|
||
});
|
||
|
||
void test('Advanced Charity Yes guides a real bribe only once the future target owns its turn', () => {
|
||
const f = fixture({poor:false}); assert.ok(player(f.game,f.target).spice >= 2);
|
||
let {game} = bind(f,'yes');
|
||
assert.equal(viewGame(game,f.target).truthCharityGuide,null);
|
||
assert.deepEqual(game.charityOrder,f.queue);
|
||
game = toTarget(game,f.target);
|
||
reject(game,f.target,{type:'ready'});
|
||
const bribe = viewGame(game,f.target).truthCharityGuide; assert.ok(bribe);
|
||
assert.equal(bribe.action.type,'bribe');
|
||
const before = player(game,f.target).spice;
|
||
game = act(game,f.target,bribe.action);
|
||
assert.ok(player(game,f.target).spice < 2);
|
||
assert.equal(player(game,f.target).spice,before - Number(bribe.action.amount));
|
||
const claim = viewGame(game,f.target).truthCharityGuide; assert.ok(claim);
|
||
assert.equal(claim.action.type,'charity');
|
||
game = act(game,f.target,claim.action);
|
||
assert.equal(player(game,f.target).charityTurn,game.turn);
|
||
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => row.answer && row.fulfilled));
|
||
assert.deepEqual(inventory(game),f.f.originalCards);
|
||
});
|
||
|
||
void test('a No by the final Advanced Charity actor completes on the actual phase transition', () => {
|
||
const base = source(); let game = structuredClone(base.game);
|
||
const queue = game.charityOrder; assert.ok(queue && queue.remaining.length >= 3);
|
||
const target = queue.remaining.at(-1)!;
|
||
let asker = base.asker;
|
||
// If the final actor holds Truthtrance, exchange it with another real hand
|
||
// card so a different player can ask; this conserves the physical inventory.
|
||
if (target === asker) {
|
||
const other = game.players.find(seat => seat.id !== target && seat.hand.length); assert.ok(other);
|
||
const holder = player(game,asker), index = holder.hand.findIndex(card => card.id === base.truths[0]); assert.ok(index >= 0);
|
||
[holder.hand[index],other.hand[0]] = [other.hand[0],holder.hand[index]];
|
||
asker = other.id;
|
||
}
|
||
const amount = player(game,target).spice - 1;
|
||
if (amount > 0) game = act(game,target,{type:'bribe',target:asker,amount});
|
||
const alternate = {...base,asker};
|
||
const declared = declareTruthCharityPayments(alternate,game).game;
|
||
game = act(declared,asker,{type:'truthAsk',question:{kind:'charity',target,turn:game.turn}});
|
||
assert.ok(viewGame(game,target).truthCharityAnswers?.includes('no'));
|
||
game = act(game,target,{type:'truthAnswer',answer:'no'});
|
||
game = toTarget(game,target);
|
||
assert.deepEqual(game.charityOrder?.remaining,[target]);
|
||
reject(game,target,{type:'charity'});
|
||
game = act(game,target,{type:'ready'});
|
||
assert.notEqual(game.phase,2);
|
||
assert.ok(viewGame(game,target).truthCharityPromises.some(row => !row.answer && row.fulfilled));
|
||
assert.deepEqual(inventory(game),base.originalCards);
|
||
});
|
||
|
||
void test('Advanced Charity cannot reopen an already finished ordered turn', () => {
|
||
const f = fixture(); let game = toTarget(f.game,f.target);
|
||
game = act(game,f.target,{type:'ready'});
|
||
if (game.phase !== 2) {
|
||
assert.equal(game.charityOrder ?? null,null);
|
||
return;
|
||
}
|
||
assert.ok(!game.charityOrder?.remaining.includes(f.target));
|
||
const declared = declareTruthCharityPayments(f.f,game).game;
|
||
reject(declared,f.asker,{type:'truthAsk',question:{kind:'charity',target:f.target,turn:game.turn}});
|
||
});
|
||
|
||
void test('all four minimal bots use the native Charity guide on their actual ordered turn', () => {
|
||
const f = fixture(), 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);
|
||
assert.equal(answer.type,'truthAnswer');
|
||
assert.ok(answerView.truthCharityAnswers?.includes(answer.answer as 'yes'|'no'));
|
||
// A separate Yes branch tests the promised legal path for every profile,
|
||
// regardless of its provisional answering strategy.
|
||
let game = act(structuredClone(asked),f.target,{type:'truthAnswer',answer:'yes'});
|
||
const waiting = viewGame(game,f.target); waiting.players.find(row => row.id === f.target)!.bot = difficulty;
|
||
assert.equal(waiting.truthCharityGuide,null);
|
||
assert.ok(!botActions(waiting).some(action => action.type === 'charity' || action.type === 'ready'));
|
||
game = toTarget(game,f.target);
|
||
const view = viewGame(game,f.target); view.players.find(row => row.id === f.target)!.bot = difficulty;
|
||
const guide = view.truthCharityGuide; assert.ok(guide);
|
||
const action = botActions(view).find(candidate => candidate.type === 'charity');
|
||
assert.deepEqual(action,guide.action);
|
||
const completed = act(game,f.target,action!);
|
||
assert.equal(player(completed,f.target).charityTurn,completed.turn);
|
||
}
|
||
});
|