Files
Dune/tests/truth-charity-early-runtime.test.ts

144 lines
7.9 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { advanceTruthCharityPayments } from './fixture-truth-charity-payments';
import { actTruthtranceChoamPromises as actChoam, advanceTruthtranceChoamPromises as advanceChoam,
createTruthtranceChoamPromisesFixture as choamFixture, truthtranceChoamPromisesInventory as choamInventory,
truthtranceChoamPromisesPlayer as choamPlayer } from './fixture-truthtrance-choam-promises';
import { act, askTruthCharityEarly as ask, bindTruthCharityEarly as bind, clean,
createTruthCharityEarlyFixture as fixture, declareTruthCharityEarly as declare,
inventory, player, reachEarlyCharity as reach, rejectTruthCharityEarly as reject } from './fixture-truth-charity-early';
for (const phase of [0,1] as const) {
void test(`Basic phase ${phase} Charity Yes survives native openings and uses its real later claim`, () => {
const f = fixture(phase), {asked,game:bound} = bind(f,'yes');
assert.equal(viewGame(asked,f.asker).truthCharityAnswers,null);
assert.deepEqual(bound.ready,asked.ready,'Answering does not reopen readiness in an earlier phase');
assert.equal(viewGame(bound,f.target).truthCharityGuide,null);
assert.ok(viewGame(bound,f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
assert.equal(bound.discard.filter(card => card.id === f.truth).length,1);
const charity = reach(bound);
assert.equal(charity.phase,2);
assert.ok(viewGame(charity,f.target).truthCharityPromises.some(row => row.answer && !row.fulfilled && !row.released));
reject(charity,f.target,{type:'ready'});
const guide = viewGame(charity,f.target).truthCharityGuide; assert.ok(guide);
let game = act(charity,f.target,guide.action);
if (guide.action.type === 'bribe') {
assert.ok(player(game,f.target).spice < 2);
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.originalCards);
});
void test(`Basic phase ${phase} Charity No remains binding until the actual later phase closes`, () => {
const f = fixture(phase), {asked,game:bound} = bind(f,'no');
assert.deepEqual(bound.ready,asked.ready);
assert.equal(viewGame(bound,f.target).truthCharityGuide,null);
assert.ok(viewGame(bound,f.target).truthCharityPromises.some(row => !row.answer && !row.fulfilled && !row.released));
let game = reach(bound);
assert.equal(game.phase,2);
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && !row.fulfilled));
// Earn eligibility by the real ordinary payment, so rejecting a physical
// claim proves the answered No rather than the ordinary wealth threshold.
const amount = player(game,f.target).spice - 1;
assert.ok(amount > 0);
game = act(game,f.target,{type:'bribe',target:f.asker,amount});
assert.equal(player(game,f.target).spice,1);
reject(game,f.target,{type:'charity'});
game = act(game,f.target,{type:'ready'});
for (const seat of game.players) if (game.phase === 2 && !game.ready.includes(seat.id))
game = act(game,seat.id,{type:'ready'});
assert.notEqual(game.phase,2);
assert.equal(player(game,f.target).charityTurn,undefined);
assert.deepEqual(inventory(game),f.originalCards);
});
}
void test('An earlier-phase Charity answer preserves an actual prior Spice Blow Ready', () => {
const f = fixture(1);
const blow = advanceTruthCharityPayments(f.game,g => !!g.spiceWindow && clean(g),[],true);
const ready = act(blow,f.target,{type:'ready'});
assert.ok(ready.ready.includes(f.target));
const {asked,game:bound} = bind(f,'yes',ready);
assert.ok(asked.ready.includes(f.target));
assert.deepEqual(bound.ready,asked.ready,'Only the future Charity Ready may be reopened');
const charity = reach(bound);
assert.equal(charity.phase,2);
assert.ok(viewGame(charity,f.target).truthCharityGuide);
});
void test('Earlier-phase stale turns and foreign answers reject without changes', () => {
const f = fixture(1), declared = declare(f);
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('All four bots answer early and follow the later native Charity guide', () => {
const f = fixture(1), asked = ask(f);
for (const difficulty of DIFFICULTIES) {
const projected = viewGame(asked,f.target);
projected.players.find(row => row.id === f.target)!.bot = difficulty;
const answer = botActions(projected)[0]; assert.ok(answer,difficulty);
assert.equal(answer.type,'truthAnswer');
assert.ok(projected.truthCharityAnswers?.includes(answer.answer as 'yes'|'no'));
let game = act(structuredClone(asked),f.target,answer);
assert.equal(viewGame(game,f.target).truthCharityGuide,null);
game = reach(game);
const own = viewGame(game,f.target); own.players.find(row => row.id === f.target)!.bot = difficulty;
if (answer.answer === 'yes') {
assert.ok(own.truthCharityGuide);
assert.deepEqual(botActions(own)[0],own.truthCharityGuide.action);
game = act(game,f.target,own.truthCharityGuide.action);
const claim = viewGame(game,f.target).truthCharityGuide;
if (claim) game = act(game,f.target,claim.action);
assert.equal(player(game,f.target).charityTurn,game.turn);
} else {
const action = botActions(own)[0]; assert.ok(action,difficulty);
game = act(game,f.target,action);
}
assert.deepEqual(inventory(game),f.originalCards);
}
});
void test('An early CHOAM Charity question does not invent an unanswered opening income', () => {
// Original native Basic four-seat CHOAM setup and unopened physical deals;
// CHOAM's earned opening is deliberately not allowed, canceled or credited.
const f=choamFixture(), source=structuredClone(f.game);
assert.equal(source.phase,0); assert.equal(source.choamCharity,undefined);
assert.equal(choamPlayer(source,f.target).spice,f.rawCash);
let game=actChoam(source,f.asker,{type:'card',card:f.truth});
while (game.truthtrance?.stage === 'priority') {
const actor=game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(actor);
game=actChoam(game,actor.id,{type:'truthPass'});
}
assert.equal(game.truthtrance?.stage,'ask');
game=actChoam(game,f.asker,{type:'truthAsk',question:{kind:'charity',target:f.target,turn:game.turn}});
const own=viewGame(game,f.target);
assert.ok(own.truthCharityAnswers?.includes('no'));
assert.ok(!own.truthCharityAnswers?.includes('yes'),'A not-yet-resolved CHOAM opening is not guaranteed income');
assert.equal(viewGame(game,f.asker).truthCharityAnswers,null);
assert.equal(choamPlayer(game,f.target).spice,f.rawCash);
assert.equal(game.choamCharity,undefined);
const before=structuredClone(game);
assert.throws(() => actChoam(game,f.target,{type:'truthAnswer',answer:'yes'}));
assert.deepEqual(game,before);
game=actChoam(game,f.target,{type:'truthAnswer',answer:'no'});
assert.equal(choamPlayer(game,f.target).spice,f.rawCash);
assert.equal(game.choamCharity,undefined);
assert.ok(viewGame(game,f.target).truthCharityPromises.some(row => !row.answer && !row.fulfilled && !row.released));
const opening=advanceChoam(game,state => state.response?.kind === 'choamCharity');
assert.equal(opening.response?.kind,'choamCharity');
assert.equal(opening.response.owner,f.target);
assert.equal(opening.choamCharity,undefined);
assert.equal(choamPlayer(opening,f.target).spice,f.rawCash);
assert.deepEqual(choamInventory(opening),f.originalInventory);
});