102 lines
5.1 KiB
TypeScript
102 lines
5.1 KiB
TypeScript
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 does not silently enable this narrow preview', () => {
|
|
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, undefined);
|
|
assert.equal(game.semutaPreview, undefined);
|
|
});
|