import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { RECRUITS_CARD_ID } from '../game/recruits'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { actSemutaRecruits as act, createSemutaRecruitsFixture as fixture, semutaRecruitsInventory as inventory, semutaRecruitsPlayer as player, } from './fixture-semuta-recruits'; function rejected(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before); } for (const claim of [false, true]) { void test(`public Basic Richese and Ecaz-card Recruits ${claim ? 'claim' : 'pass'} preserves its real free-revival benefit`, () => { const f = fixture(), before = structuredClone(f.game); assert.ok(f.lobby.lobbyOptions?.ecazTreachery); assert.equal(f.setup.semutaRecruitsPreview, true); assert.equal(f.setup.semutaPreview, undefined); assert.equal(viewGame(f.game, f.owner).semutaRecruitsPreview, true); assert.ok(viewGame(f.game, f.owner).recruitsPreview?.play && !viewGame(f.game, f.owner).recruitsPreview!.play!.blocked); assert.equal(viewGame(f.game, f.owner).revival.freeRemaining, 2); const pending = act(f.game, f.owner, f.play); assert.deepEqual(f.game, before); const frame = pending.pendingTreacheryDiscard; assert.ok(frame); assert.equal(frame.reaction?.stage, 'offer'); assert.equal(frame.continuation.kind, 'ordinaryCardDiscard'); assert.deepEqual(frame.batch.entries.map(entry => entry.card.id), [RECRUITS_CARD_ID]); assert.equal(pending.recruits?.turn, pending.turn); assert.equal(pending.recruits?.player, f.owner); for (const seat of pending.players) { const reaction = viewGame(pending, seat.id).semutaReaction; assert.ok(reaction); assert.deepEqual(reaction.candidates, []); assert.equal(reaction.canCommit, seat.id === f.holder); } rejected(pending, f.owner, {type: 'revive', amount: 3, elite: 0}); const event = frame.batch.event; let resumed: Game = pending; if (claim) { resumed = act(resumed, f.holder, {type: 'semutaCommit', event}); if (resumed.pendingTreacheryDiscard?.reaction?.stage === 'select') resumed = act(resumed, f.holder, {type: 'semutaSelect', event, card: RECRUITS_CARD_ID}); } else { for (const seat of pending.players) resumed = act(resumed, seat.id, {type: 'semutaPass', event}); } assert.equal(resumed.pendingTreacheryDiscard, null); assert.equal(viewGame(resumed, f.owner).revival.freeRemaining, 4); assert.equal(viewGame(resumed, f.owner).revival.limit, 7); assert.equal(player(resumed, f.holder).hand.some(card => card.id === RECRUITS_CARD_ID), claim); assert.equal(resumed.discard.filter(card => card.id === RECRUITS_CARD_ID).length, Number(!claim)); assert.equal(resumed.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, Number(claim)); assert.deepEqual(inventory(resumed), f.originalInventory); rejected(resumed, f.holder, {type: 'semutaCommit', event}); if (claim) rejected(resumed, f.holder, f.play); const old = {reserves: player(resumed, f.owner).reserves, tanks: player(resumed, f.owner).tanks, spice: player(resumed, f.owner).spice}; const revived = act(resumed, f.owner, {type: 'revive', amount: 3, elite: 0}); assert.equal(player(revived, f.owner).tanks, old.tanks - 3); assert.equal(player(revived, f.owner).reserves, old.reserves + 3); assert.equal(player(revived, f.owner).spice, old.spice); assert.equal(player(revived, f.owner).freeForcesRevived, 3); assert.equal(revived.recruits?.turn, resumed.turn); }); } void test('each AI difficulty can legally answer the Recruits discard offer', () => { const f = fixture(); const pending = act(f.game, f.owner, f.play); for (const difficulty of DIFFICULTIES) { const game = structuredClone(pending); player(game, f.holder).bot = difficulty; const action = botActions(viewGame(game, f.holder)).find(item => item.type === 'semutaCommit' || item.type === 'semutaPass'); assert.ok(action); assert.doesNotThrow(() => act(game, f.holder, action)); } }); void test('a changed revival parent cannot resume a saved Recruits discard', () => { const f = fixture(); const pending = act(f.game, f.owner, f.play); const stale = structuredClone(pending); stale.freeRevival.push(f.owner); assert.throws(() => viewGame(stale, f.owner), /completed effect/); assert.ok(viewGame(pending, f.owner).semutaReaction); }); void test('a native CHOAM seat enables the Recruits reaction without enabling other Semuta previews', () => { const f = fixture(), lobby = structuredClone(f.lobby); const index = lobby.players.findIndex(seat => seat.id === 'emperor'); assert.ok(index >= 0); lobby.players[index] = newPlayer('emperor', 'CHOAM', 'choam'); lobby.players[index].ready = true; const game = act(lobby, lobby.host, {type: 'start'}); assert.equal(game.semutaRecruitsPreview, true); assert.equal(game.semutaPreview, undefined); });