144 lines
7.9 KiB
TypeScript
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);
|
|
});
|