Files
Dune/tests/kull-revival-semuta.test.ts
Quintine 71009d73e3
Verify and publish checkpoint container / container (push) Failing after 6m28s
Support Kull during Basic Guild and revival responses
2026-10-28 20:57:25 +11:00

126 lines
6.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { act, createKullRevivalSemutaFixture, finishRevival, inventory, player } from './fixture-kull-revival-semuta';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
type Fixture = ReturnType<typeof createKullRevivalSemutaFixture>;
const held = (game: Game, seat: string, card: string) => player(game, seat).hand.some(row => row.id === card);
function attempt(f: Fixture) {
const game = act(reload(f.game), f.actor, { type: 'card', mode: 'cancel', card: f.karama });
assert.equal(game.pendingKull?.stage, 'offer');
const choice = viewGame(game, f.reactor).kullReaction; assert.ok(choice);
assert.ok(choice.plays.some(row => row.source === 'printed' && row.card.id === f.kull && !row.blocked));
assert.deepEqual(game.pendingRevival, f.source.pending);
assert.ok(held(game, f.actor, f.karama));
return game;
}
function declare(f: Fixture, game: Game) {
const choice = viewGame(game, f.reactor).kullReaction; assert.ok(choice);
const next = act(game, f.reactor, { type: 'kullDecision', event: choice.event, source: 'printed', card: f.kull });
assert.equal(next.pendingKull?.stage, 'counter');
assert.deepEqual(next.pendingRevival, f.source.pending);
return next;
}
function passCounter(game: Game) {
for (let count = 0; count < 16 && !game.pendingTreacheryDiscard; count++) {
assert.ok(game.response, 'The native Kull counter remains open.');
const seat = game.players.find(row => !game.response!.passed.includes(row.id)); assert.ok(seat);
game = act(game, seat.id, { type: 'passResponse' });
}
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
return game;
}
function react(game: Game, f: Fixture, card: string, claim: boolean) {
const pending = game.pendingTreacheryDiscard; assert.ok(pending);
const event = pending.batch.event, hadSemuta = held(game, f.holder, f.semuta);
assert.deepEqual(pending.batch.entries.map(entry => entry.card.id), [card]);
assert.deepEqual(game.pendingRevival, f.source.pending);
if (claim) {
assert.equal(viewGame(game, f.holder).semutaReaction?.canCommit, true);
game = act(game, f.holder, { type: 'semutaCommit', event });
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select')
game = act(game, f.holder, { type: 'semutaSelect', event, card });
} else {
for (let count = 0; game.pendingTreacheryDiscard?.batch.event === event && count < game.players.length; count++) {
const reaction = game.pendingTreacheryDiscard!.reaction; assert.ok(reaction?.stage === 'offer');
const seat = game.players.find(row => !reaction.passed.includes(row.id)); assert.ok(seat);
game = act(game, seat.id, { type: 'semutaPass', event });
}
}
assert.notEqual(game.pendingTreacheryDiscard?.batch.event, event);
assert.equal(held(game, f.holder, card), claim);
assert.equal(held(game, f.holder, f.semuta), claim ? false : hadSemuta);
assert.deepEqual(inventory(game), f.originalCards);
assert.throws(() => act(game, f.holder, { type: 'semutaCommit', event }));
return game;
}
/** Resolve only the original native revival and its Tleilaxu income response.
* No subsequent request, payment, quota or phase transition is fabricated. */
function assertOriginalReturn(game: Game, f: Fixture, canceled: boolean) {
const prior = player(f.game, f.source.recipient);
const otherPrior = player(f.game, f.source.tleilaxu);
const nativeCost = canceled ? 6 : 3;
game = finishRevival(game);
const current = player(game, f.source.recipient);
assert.equal(game.pendingRevival ?? null, null);
assert.equal(game.response ?? null, null);
assert.equal(current.reserves, prior.reserves + 3);
assert.equal(current.tanks, prior.tanks - 3);
assert.equal(current.revived, prior.revived + 3);
assert.equal(current.freeForcesRevived ?? 0, prior.freeForcesRevived ?? 0);
assert.equal(current.spice, prior.spice - nativeCost);
assert.equal(player(game, f.source.tleilaxu).spice, otherPrior.spice + nativeCost);
assert.equal(game.phase, f.game.phase);
assert.equal(game.turn, f.game.turn);
assert.deepEqual(inventory(game), f.originalCards);
return game;
}
for (const claim of [false, true]) {
void test(`Basic Kull protects its original native revival and Semuta ${claim ? 'claims' : 'passes'} Kull`, () => {
const f = createKullRevivalSemutaFixture();
const pending = passCounter(declare(f, attempt(f)));
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'kullSuccessDiscard');
assert.ok(held(pending, f.actor, f.karama));
const game = react(pending, f, f.kull, claim);
assert.equal(game.pendingKull, null);
assert.ok(held(game, f.actor, f.karama));
assert.ok(viewGame(game, f.actor).karamaBlocked);
assertOriginalReturn(game, f, false);
});
}
void test('declining Kull permits the original Revival cancellation and Semuta can claim its spent Karama', () => {
const f = createKullRevivalSemutaFixture();
let game = attempt(f);
game = act(game, f.reactor, { type: 'kullDecision', event: game.pendingKull!.event, decline: true });
assert.equal(game.pendingTreacheryDiscard?.continuation.kind, 'karamaRevivalDiscard');
game = react(game, f, f.karama, true);
assert.equal(game.pendingKull, null);
assert.ok(held(game, f.reactor, f.kull));
assertOriginalReturn(game, f, true);
});
for (const claim of [false, true]) {
void test(`a distinct Kull counter preserves the native Revival request while Semuta ${claim ? 'claims' : 'passes'} it`, () => {
const f = createKullRevivalSemutaFixture();
const declared = declare(f, attempt(f));
assert.ok(viewGame(declared, f.counterOwner).responseControls?.cancelCards.includes(f.counter));
let game = act(declared, f.counterOwner, { type: 'card', mode: 'cancel', card: f.counter });
assert.equal(game.pendingTreacheryDiscard?.continuation.kind, 'kullCounterDiscard');
assert.ok(held(game, f.actor, f.karama));
game = react(game, f, f.counter, claim);
assert.ok(held(game, f.reactor, f.kull));
if (game.pendingTreacheryDiscard) game = react(game, f, f.karama, !claim);
assert.equal(game.pendingKull, null);
assert.equal(held(game, f.actor, f.karama), false);
assertOriginalReturn(game, f, true);
});
}