Files
Dune/tests/semuta-market-ghola-kull.test.ts
T
Quintine bcca08cac5
Verify and publish checkpoint container / container (push) Successful in 11m25s
Support printed Kull during market Ghola
2026-10-29 08:37:50 +11:00

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);
}
});