Files
Dune/tests/semuta-residual-ready-runtime.test.ts
T

106 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { actTruthFlightPoison as act, createTruthFlightPoisonFixture,
truthFlightPoisonAction, truthFlightPoisonInventory, truthFlightPoisonPlayer as player } from './fixture-truth-flight-poison';
/** Genuine bare Basic setup, real cache purchase of Residual Poison, paid
* shipment and opened battle. The only controlled supplement moves the real
* Semuta card from the original Richese cache to Guild's hand; acquiring it
* is not presented as an earned purchase or a separate natural journey. */
function source() {
const f = createTruthFlightPoisonFixture({ effect: 'residualPoison', advanced: false });
const game = structuredClone(f.game), holder = 'guild';
assert.equal(game.semutaPreview, true);
assert.ok(!game.homeworlds && !game.nexusCards && !game.leaderSkills);
const index = game.richeseCache!.findIndex(card => card.id === SEMUTA_DRUG_ID);
assert.ok(index >= 0, 'The original unpurchased Semuta remains in the cache');
assert.ok(player(game, holder).hand.length < 4);
player(game, holder).hand.push(game.richeseCache!.splice(index, 1)[0]);
assert.deepEqual(truthFlightPoisonInventory(game), f.originalCards);
assert.ok(game.battle?.preLeader && !game.battle.preLeader.closed);
assert.deepEqual(game.battle.preLeader.ready, []);
return { ...f, game, holder };
}
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before);
}
function open(f: ReturnType<typeof source>, readyActor: string) {
const ready = act(f.game, readyActor, { type: 'battlePreparationReady', event: f.event });
assert.deepEqual(ready.battle!.preLeader!.ready, [readyActor]);
const before = structuredClone(ready), action = truthFlightPoisonAction(ready, f);
const pending = act(ready, f.target, action);
assert.deepEqual(ready, before);
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'residualPoisonDiscard');
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.deepEqual(pending.battle!.preLeader!.ready, [readyActor], 'The accepted preparation is held during the offer');
const earlier = player(ready, f.asker).leaders.filter(leader => leader.dead).map(leader => leader.id);
const newlyDead = player(pending, f.asker).leaders.filter(leader => leader.dead && !earlier.includes(leader.id));
assert.equal(newlyDead.length, 1, 'Poison chooses and commits exactly one physical death');
assert.equal(pending.discard.filter(card => card.id === f.card).length, 1);
assert.equal(viewGame(pending, f.holder).semutaReaction?.canCommit, true);
assert.deepEqual(truthFlightPoisonInventory(pending), f.originalCards);
return { ready, pending, victim: newlyDead[0].id, event: pending.pendingTreacheryDiscard!.batch.event };
}
function react(game: Game, holder: string, event: string, claim: boolean): Game {
if (claim) return act(game, holder, { type: 'semutaCommit', event });
let current = game;
for (const seat of game.players) {
if (!current.pendingTreacheryDiscard?.reaction) break;
current = act(current, seat.id, { type: 'semutaPass', event });
}
return current;
}
for (const who of ['owner', 'opponent'] as const) for (const claim of [false, true]) {
void test(`Basic ${who}-ready Residual Poison offers Semuta and ${claim ? 'claim' : 'pass'} resumes once`, () => {
const f = source(), readyActor = who === 'owner' ? f.target : f.asker;
const { pending, victim, event } = open(f, readyActor);
const resumed = react(JSON.parse(JSON.stringify(pending)) as Game, f.holder, event, claim);
assert.equal(resumed.pendingTreacheryDiscard ?? null, null);
assert.deepEqual(resumed.battle!.preLeader!.ready, who === 'owner' ? [] : [readyActor],
'Native Poison reopens its own readiness without erasing its opponent’s readiness');
assert.equal(player(resumed, f.asker).leaders.find(leader => leader.id === victim)?.dead, true);
assert.equal(player(resumed, f.asker).leaders.filter(leader => leader.dead).length,
player(pending, f.asker).leaders.filter(leader => leader.dead).length);
assert.equal(player(resumed, f.holder).hand.some(card => card.id === f.card), claim);
assert.equal(resumed.discard.some(card => card.id === SEMUTA_DRUG_ID), claim);
assert.equal(resumed.discard.some(card => card.id === f.card), !claim);
assert.deepEqual(truthFlightPoisonInventory(resumed), f.originalCards);
rejected(resumed, f.holder, { type: 'semutaCommit', event });
});
}
void test('ready Poison offer rejects stale events and corrupt battle custody without mutation', () => {
const f = source(), { pending, event, victim } = open(f, f.asker);
rejected(pending, f.holder, { type: 'semutaCommit', event: `${event}-stale` });
const corrupt = structuredClone(pending);
player(corrupt, f.asker).leaders.find(leader => leader.id === victim)!.dead = false;
rejected(corrupt, f.holder, { type: 'semutaCommit', event });
});
for (const difficulty of DIFFICULTIES) void test(`${difficulty} uses a legal Semuta response after ready Poison`, () => {
const f = source(), { pending, event } = open(f, f.asker);
const view = viewGame(pending, f.holder);
view.players.find(seat => seat.id === f.holder)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action && (action.type === 'semutaCommit' || action.type === 'semutaPass'));
let current = act(pending, f.holder, action);
for (const seat of current.players) {
if (!current.pendingTreacheryDiscard?.reaction) break;
const offer = viewGame(current, seat.id).semutaReaction;
if (offer && !offer.passed) current = act(current, seat.id, { type: 'semutaPass', event });
}
assert.equal(current.pendingTreacheryDiscard ?? null, null);
assert.deepEqual(current.battle!.preLeader!.ready, [f.asker]);
assert.deepEqual(truthFlightPoisonInventory(current), f.originalCards);
});