Files
Dune/tests/semuta-recruits-basic.test.ts
T
Quintine 1ba942138d
Verify and publish checkpoint container / container (push) Successful in 10m20s
Prototype Semuta after Recruits in Basic play
2026-10-28 22:19:12 +11:00

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