88 lines
5.0 KiB
TypeScript
88 lines
5.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { actTruthFlightPoison as act, createTruthFlightPoisonFixture,
|
|
truthFlightPoisonAction, truthFlightPoisonInventory, truthFlightPoisonPlayer as player,
|
|
} from './fixture-truth-flight-poison';
|
|
|
|
/** Actual public Basic independent-card setup, paid cache purchase, shipment
|
|
* and pre-leader battle. Only Semuta is transferred from the original cache
|
|
* to Guild's hand; it is not claimed as a real purchase. */
|
|
function fixture() {
|
|
const source = createTruthFlightPoisonFixture({ effect: 'residualPoison', independentCards: true });
|
|
const game = structuredClone(source.game), holder = 'guild';
|
|
assert.equal(game.semutaRecruitsPreview, true);
|
|
const index = game.richeseCache!.findIndex(card => card.id === SEMUTA_DRUG_ID);
|
|
assert.ok(index >= 0);
|
|
assert.ok(player(game, holder).hand.length < 4);
|
|
player(game, holder).hand.push(game.richeseCache!.splice(index, 1)[0]);
|
|
assert.deepEqual(truthFlightPoisonInventory(game), source.originalCards);
|
|
assert.ok(game.battle?.preLeader && !game.battle.preLeader.closed);
|
|
return { ...source, game, holder };
|
|
}
|
|
|
|
function open(source: ReturnType<typeof fixture>, who: 'owner' | 'opponent') {
|
|
const readyActor = who === 'owner' ? source.target : source.asker;
|
|
const ready = act(source.game, readyActor, { type: 'battlePreparationReady', event: source.event });
|
|
assert.deepEqual(ready.battle!.preLeader!.ready, [readyActor]);
|
|
const beforeDead = player(ready, source.asker).leaders.filter(leader => leader.dead).map(leader => leader.id);
|
|
const pending = act(ready, source.target, truthFlightPoisonAction(ready, source));
|
|
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'residualPoisonDiscard');
|
|
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
|
assert.deepEqual(pending.battle!.preLeader!.ready, [readyActor]);
|
|
const newlyDead = player(pending, source.asker).leaders.filter(leader => leader.dead && !beforeDead.includes(leader.id));
|
|
assert.equal(newlyDead.length, 1);
|
|
assert.equal(viewGame(pending, source.holder).semutaReaction?.canCommit, true);
|
|
return { 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 });
|
|
for (const seat of game.players) {
|
|
if (!game.pendingTreacheryDiscard?.reaction) break;
|
|
game = act(game, seat.id, { type: 'semutaPass', event });
|
|
}
|
|
return game;
|
|
}
|
|
|
|
for (const who of ['owner', 'opponent'] as const) for (const claim of [false, true]) {
|
|
void test(`independent-card ${who}-ready Residual Poison ${claim ? 'claim' : 'pass'} resumes the original preparation once`, () => {
|
|
const f = fixture(), { pending, victim, event } = open(f, who);
|
|
const dead = player(pending, f.asker).leaders.filter(leader => leader.dead).length;
|
|
const resumed = react(pending, f.holder, event, claim);
|
|
assert.equal(resumed.pendingTreacheryDiscard ?? null, null);
|
|
assert.deepEqual(resumed.battle!.preLeader!.ready, who === 'owner' ? [] : [f.asker]);
|
|
assert.deepEqual(resumed.battle!.plans, pending.battle!.plans);
|
|
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, dead);
|
|
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);
|
|
assert.throws(() => act(resumed, f.holder, { type: 'semutaCommit', event }));
|
|
});
|
|
}
|
|
|
|
void test('all profiles can answer an independent-card Residual Poison offer legally', () => {
|
|
const f = fixture(), { pending, event } = open(f, 'opponent');
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const game = structuredClone(pending);
|
|
const view = viewGame(game, 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 continued = act(game, f.holder, action);
|
|
for (const seat of continued.players) {
|
|
if (!continued.pendingTreacheryDiscard?.reaction) break;
|
|
const offer = viewGame(continued, seat.id).semutaReaction;
|
|
if (offer && !offer.passed) continued = act(continued, seat.id, { type: 'semutaPass', event });
|
|
}
|
|
assert.equal(continued.pendingTreacheryDiscard ?? null, null);
|
|
assert.deepEqual(continued.battle!.preLeader!.ready, [f.asker]);
|
|
assert.deepEqual(truthFlightPoisonInventory(continued), f.originalCards);
|
|
}
|
|
});
|