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