Files
Dune/tests/truth-charity-ordered-runtime.test.ts
T
Quintine 68c7434a20
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Truthtrance in ordered Charity
2026-10-29 08:57:58 +11:00

179 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});