122 lines
7.6 KiB
TypeScript
122 lines
7.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { createSemutaMarketGholaFixture as fixture, actSemutaMarketGhola as act,
|
|
semutaMarketGholaPlayer as player, semutaMarketGholaInventory as inventory,
|
|
passSemutaMarketGhola as pass, claimSemutaMarketGhola as claim,
|
|
allowSemutaMarketGholaIncome as allowIncome } from './fixture-semuta-market-ghola';
|
|
|
|
/** Resolve the existing public responses, but never choose a new market action. */
|
|
function finishOriginalSale(source: Game): Game {
|
|
let game = source;
|
|
for (let count = 0; count < 30; count++) {
|
|
if (game.pendingTreacheryDiscard?.reaction?.stage === 'offer') { game = pass(game); continue; }
|
|
if (game.response) {
|
|
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id));
|
|
assert.ok(seat, 'An unresolved response needs an unpassed seat');
|
|
game = act(game, seat.id, {type:'passResponse'}); continue;
|
|
}
|
|
assert.equal(game.pendingKull ?? null, null);
|
|
assert.equal(game.pendingChoamMarketGhola ?? null, null);
|
|
return game;
|
|
}
|
|
throw Error('The original income and market must finish');
|
|
}
|
|
|
|
for (const claims of [false, true]) for (const kullOutcome of ['decline', 'success', 'counter'] as const)
|
|
void test(`default Kull: Semuta ${claims ? 'claims' : 'passes'} market Ghola; Kull ${kullOutcome} preserves income and sale`, () => {
|
|
const f = fixture({withKull:true}); assert.ok(f.kull); assert.ok(f.kullCounter);
|
|
const beforeIncome = player(f.source, f.incomeOwner).spice, beforeSale = player(f.source, f.owner).spice;
|
|
assert.equal(f.game.kullPreview, true);
|
|
let game = claims ? claim(f.game, f.claimant, f.ghola) : pass(f.game);
|
|
assert.equal(game.response?.kind, 'revivalIncome');
|
|
assert.deepEqual(game.pendingChoamMarketGhola?.response, f.response);
|
|
assert.ok(viewGame(game, f.counterOwner).responseControls?.cancelCards.includes(f.counter));
|
|
game = act(game, f.counterOwner, {type:'card', card:f.counter, mode:'cancel'});
|
|
const reaction = viewGame(game, f.owner).kullReaction; assert.ok(reaction);
|
|
assert.ok(reaction.plays.some(play => play.source === 'printed' && play.card.id === f.kull));
|
|
game = kullOutcome === 'decline'
|
|
? act(game, f.owner, {type:'kullDecision', event:reaction.event, decline:true})
|
|
: act(game, f.owner, {type:'kullDecision', event:reaction.event, source:'printed', card:f.kull});
|
|
if (kullOutcome === 'counter') {
|
|
assert.ok(viewGame(game, f.incomeOwner).responseControls?.cancelCards.includes(f.kullCounter));
|
|
game = act(game, f.incomeOwner, {type:'card', card:f.kullCounter, mode:'cancel'});
|
|
}
|
|
if (kullOutcome === 'success') {
|
|
for (let count = 0; !game.pendingTreacheryDiscard && game.response && count < game.players.length; count++) {
|
|
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id));
|
|
assert.ok(seat); game = act(game, seat.id, {type:'passResponse'});
|
|
}
|
|
}
|
|
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
|
assert.equal(game.pendingTreacheryDiscard?.continuation.kind,
|
|
kullOutcome === 'decline' ? 'karamaIncomeDiscard' :
|
|
kullOutcome === 'success' ? 'kullSuccessDiscard' : 'kullCounterDiscard');
|
|
game = pass(game);
|
|
if (kullOutcome === 'counter') {
|
|
assert.equal(game.pendingTreacheryDiscard?.continuation.kind, 'karamaIncomeDiscard');
|
|
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
|
}
|
|
game = finishOriginalSale(game);
|
|
assert.equal(player(game, f.incomeOwner).spice, beforeIncome + Number(kullOutcome === 'success'));
|
|
assert.equal(player(game, f.owner).spice, beforeSale + 2);
|
|
assert.equal(player(game, f.owner).tanks, player(f.source, f.owner).tanks - 1);
|
|
assert.equal(player(game, f.claimant).hand.some(card => card.id === f.ghola), claims);
|
|
assert.equal(game.discard.filter(card => card.id === f.sale).length, 1);
|
|
assert.equal(player(game, f.counterOwner).hand.some(card => card.id === f.counter), kullOutcome === 'success');
|
|
assert.equal(game.discard.filter(card => card.id === f.kull).length, Number(kullOutcome === 'success'));
|
|
assert.deepEqual(inventory(game), f.originalInventory);
|
|
});
|
|
|
|
for (const kullOutcome of ['decline', 'success', 'counter'] as const)
|
|
void test(`resumed sale cancellation: Kull ${kullOutcome} offers its physical cost to Semuta`, () => {
|
|
const f = fixture({withKull:true}); assert.ok(f.kull); assert.ok(f.kullCounter);
|
|
const beforeIncome = player(f.source, f.incomeOwner).spice, beforeSale = player(f.source, f.owner).spice;
|
|
let game = allowIncome(pass(f.game));
|
|
assert.equal(game.response?.kind, 'choamSale');
|
|
assert.deepEqual(game.response, f.response);
|
|
assert.ok(viewGame(game, f.counterOwner).responseControls?.cancelCards.includes(f.counter));
|
|
game = act(game, f.counterOwner, {type:'card', card:f.counter, mode:'cancel'});
|
|
const reaction = viewGame(game, f.owner).kullReaction; assert.ok(reaction);
|
|
game = kullOutcome === 'decline'
|
|
? act(game, f.owner, {type:'kullDecision', event:reaction.event, decline:true})
|
|
: act(game, f.owner, {type:'kullDecision', event:reaction.event, source:'printed', card:f.kull});
|
|
if (kullOutcome === 'counter') {
|
|
assert.ok(viewGame(game, f.incomeOwner).responseControls?.cancelCards.includes(f.kullCounter));
|
|
game = act(game, f.incomeOwner, {type:'card', card:f.kullCounter, mode:'cancel'});
|
|
}
|
|
const cost = kullOutcome === 'decline' ? f.counter : kullOutcome === 'success' ? f.kull : f.kullCounter;
|
|
for (let count = 0; !game.pendingTreacheryDiscard && game.response && count < game.players.length; count++) {
|
|
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(seat);
|
|
game = act(game, seat.id, {type:'passResponse'});
|
|
}
|
|
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
|
assert.ok(game.pendingTreacheryDiscard?.batch.entries.some(entry => entry.card.id === cost));
|
|
game = claim(game, f.claimant, cost);
|
|
game = finishOriginalSale(game);
|
|
assert.equal(player(game, f.incomeOwner).spice, beforeIncome + 1);
|
|
assert.equal(player(game, f.owner).spice, beforeSale + (kullOutcome === 'success' ? 2 : 0));
|
|
assert.equal(player(game, f.owner).hand.some(card => card.id === f.sale), kullOutcome !== 'success');
|
|
assert.equal(game.discard.filter(card => card.id === f.sale).length, Number(kullOutcome === 'success'));
|
|
assert.equal(player(game, f.claimant).hand.filter(card => card.id === cost).length, 1);
|
|
assert.equal(game.discard.filter(card => card.id === f.semuta).length, 1);
|
|
assert.equal(player(game, f.owner).tanks, player(f.source, f.owner).tanks - 1);
|
|
assert.deepEqual(inventory(game), f.originalInventory);
|
|
});
|
|
|
|
void test('all minimal bot profiles can answer Kull against the resumed sale', () => {
|
|
const f = fixture({withKull:true});
|
|
let game = allowIncome(pass(f.game));
|
|
assert.equal(game.response?.kind, 'choamSale');
|
|
game = act(game, f.counterOwner, {type:'card', card:f.counter, mode:'cancel'});
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, f.owner); view.players.find(seat => seat.id === f.owner)!.bot = difficulty;
|
|
const action = botActions(view).find(candidate => candidate.type === 'kullDecision'); assert.ok(action, difficulty);
|
|
const continued = act(game, f.owner, action);
|
|
assert.notEqual(continued.pendingKull?.stage, 'offer');
|
|
assert.deepEqual(inventory(continued), f.originalInventory);
|
|
}
|
|
});
|