169 lines
11 KiB
TypeScript
169 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||
import { botActions } from '../game/bots';
|
||
import type { Difficulty } from '../game/bot-profiles';
|
||
import { leaderSkillBattleBonus } from '../game/leader-skill-combat';
|
||
import {
|
||
acceptE3HarkonnenCapture as accept, actE3Capture as act, advanceE3Capture as advance,
|
||
cancelE3HarkonnenCapture as cancel, continueE3HarkonnenCaptiveBattle as useCaptive,
|
||
createE3HarkonnenCaptureFixture as fixture, declineE3HarkonnenCapture as decline,
|
||
e3CaptureClean as clean, e3CaptureInventory as inventory, e3CapturePlayer as p,
|
||
e3CaptureReload as reload, type E3CaptureStep,
|
||
} from './fixture-e3-harkonnen-capture';
|
||
|
||
function reject(game: Game, actor: string, action: Action) {
|
||
const before = reload(game);
|
||
assert.throws(() => applyAction(game, actor, action));
|
||
assert.deepEqual(game, before);
|
||
}
|
||
function skillCount(game: Game, skill: string) {
|
||
return game.leaderSkills!.deck.filter(card => card === skill).length +
|
||
game.leaderSkills!.assignments.filter(row => row.skill === skill).length +
|
||
Object.values(game.leaderSkills!.offers).flatMap(offer => offer.cards).filter(card => card === skill).length;
|
||
}
|
||
|
||
for (const native of ['ecaz', 'moritani'] as const) {
|
||
void test(`native Advanced ${native} all14 source earns two real battles and the original Harkonnen capture offer`, () => {
|
||
const f = fixture(native);
|
||
assert.equal(f.afterSetup.advanced, true);
|
||
assert.deepEqual(f.afterSetup.players.map(player => player.faction), ['guild', 'harkonnen', native]);
|
||
assert.deepEqual(f.afterSetup.expansions, ['ecaz']);
|
||
assert.equal(f.sourceCounts.setup, f.setupActions.length);
|
||
assert.equal(f.sourceCounts.firstBattle, f.firstBattleActions.length);
|
||
assert.equal(f.sourceCounts.source, f.actions.length);
|
||
assert.ok(f.setupActions.some(step => step.actor === native && step.action.type === 'leaderSkill' &&
|
||
step.action.skill === 'warmaster' && step.action.leader === f.captive));
|
||
assert.deepEqual(f.shipments.map(row => ({ actor: row.actor, amount: row.action.amount, cost: row.cost })),
|
||
[{ actor: 'harkonnen', amount: 1, cost: 2 }, { actor: 'guild', amount: 1, cost: 1 }]);
|
||
assert.equal(f.movement.action.from, 'carthag:11');
|
||
assert.equal(f.movement.action.territory, 'tsimpo');
|
||
assert.equal(p(f.beforeFirstBattle, 'harkonnen').forces['imperial_basin:9'], 1);
|
||
assert.equal(p(f.beforeFirstBattle, 'harkonnen').forces['tsimpo:11'], 1);
|
||
assert.equal(p(f.beforeFirstBattle, 'guild').forces['tsimpo:11'], 1);
|
||
assert.equal(f.game.decision?.kind, 'captureOffer');
|
||
assert.equal(p(f.game, native).tanks, 6);
|
||
assert.equal(p(f.game, native).leaders.find(leader => leader.id === f.captive)?.dead, false);
|
||
assert.deepEqual(f.game.leaderSkills!.assignments.find(row => row.owner === native), f.assignment);
|
||
assert.deepEqual(inventory(f.game), f.original);
|
||
assert.equal(!!f.game.moritaniAssassinate, native === 'moritani');
|
||
});
|
||
|
||
void test(`actual trained ${native} capture keeps its original assignment and exposes only the captive lower band`, () => {
|
||
const f = fixture(native), actions: E3CaptureStep[] = [];
|
||
reject(f.game, 'guild', { type: 'decision', accept: true });
|
||
const game = accept(f, 'keep', f.game, actions);
|
||
const leader = p(game, native).leaders.find(leader => leader.id === f.captive)!;
|
||
assert.equal(leader.capturedBy, 'harkonnen'); assert.equal(leader.concealed, undefined);
|
||
assert.equal(leader.dead, false);
|
||
assert.deepEqual(game.leaderSkills!.assignments.find(row => row.owner === native), f.assignment);
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.equal(game.leaderSkills!.offers[native], undefined);
|
||
for (const actor of [native, 'harkonnen']) {
|
||
const row = viewGame(game, actor).leaderSkills!.assignments.find(row => row.leader === f.captive)!;
|
||
assert.ok(row); assert.equal(row.owner, native); assert.equal(row.controller, 'harkonnen');
|
||
assert.equal(row.captured, true); assert.equal(row.faceUp, false);
|
||
const otherLeader = p(game, actor).leaders.find(own => own.id !== f.captive && !own.dead)!;
|
||
assert.equal(leaderSkillBattleBonus({ assignments: [row],
|
||
selectedLeader: { id: otherLeader.id, kind: 'disc' }, weapon: p(game, 'harkonnen').hand.find(card => card.id === f.worthless),
|
||
defense: undefined, skilledLeaderSurvives: true }).bonus, 0,
|
||
'The captive upper band cannot strengthen either faction’s other leader');
|
||
}
|
||
assert.ok(actions.some(step => step.actor === 'harkonnen' && step.action.accept === true));
|
||
assert.ok(actions.some(step => step.actor === 'harkonnen' && step.action.mode === 'keep'));
|
||
if (native === 'moritani') assert.ok([...f.actions, ...actions].some(step =>
|
||
step.actor === native && step.action.type === 'decision' && step.action.decline === true),
|
||
'Original Moritani optional aftermath is declined through its native control');
|
||
assert.deepEqual(inventory(game), f.original);
|
||
});
|
||
|
||
void test(`captured ${native} Warmaster supplies exactly lower three then returns alive with its same assignment`, () => {
|
||
const f = fixture(native), captureActions: E3CaptureStep[] = [], battleActions: E3CaptureStep[] = [];
|
||
const kept = accept(f, 'keep', f.game, captureActions);
|
||
const result = useCaptive(f, kept, 'alive', battleActions), game = result.game;
|
||
assert.equal(result.plans[0].action.leader, f.captive);
|
||
assert.equal(result.plans[0].action.weapon, f.worthless);
|
||
assert.equal(result.plans[0].action.defense, null, 'The held Harkonnen Shield supplies no native Prana bonus');
|
||
const bonuses = game.log.filter(entry => entry.automatic?.name === 'Leader Skill' && entry.text.includes('Warmaster'));
|
||
assert.ok(bonuses.some(entry => entry.text.includes('gained 3 battle strength')));
|
||
assert.equal(bonuses.some(entry => entry.text.includes('gained 1 battle strength')), false);
|
||
const leader = p(game, native).leaders.find(leader => leader.id === f.captive)!;
|
||
assert.equal(leader.dead, false); assert.equal(leader.capturedBy, undefined); assert.equal(leader.usedAt, 'tsimpo');
|
||
assert.deepEqual(game.leaderSkills!.assignments.find(row => row.owner === native), f.assignment);
|
||
assert.equal(game.leaderSkills!.deck.includes('warmaster'), false);
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.equal(viewGame(game, native).leaderSkills!.assignments.find(row => row.leader === f.captive)?.controller, native);
|
||
assert.ok(p(game, 'harkonnen').hand.some(card => card.id === f.shield));
|
||
assert.ok(p(game, 'harkonnen').hand.some(card => card.id === f.worthless));
|
||
assert.ok(captureActions.length > 0 && battleActions.length > 0);
|
||
assert.deepEqual(inventory(game), f.original);
|
||
});
|
||
|
||
void test(`actual Guild poison kills the captive ${native} trainer and returns its one physical skill`, () => {
|
||
const f = fixture(native), kept = accept(f);
|
||
const result = useCaptive(f, kept, 'death'), game = result.game;
|
||
assert.equal(result.plans[1].action.weapon, f.poison);
|
||
assert.equal(result.plans[0].action.defense, null);
|
||
const leader = p(game, native).leaders.find(leader => leader.id === f.captive)!;
|
||
assert.equal(leader.dead, true); assert.equal(leader.deaths, 1); assert.equal(leader.capturedBy, undefined);
|
||
assert.equal(game.leaderSkills!.assignments.some(row => row.leader === f.captive), false);
|
||
assert.equal(game.leaderSkills!.deck.filter(skill => skill === 'warmaster').length, 1);
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.equal(game.discard.filter(card => card.id === f.worthless).length, 1);
|
||
assert.ok(p(game, 'harkonnen').hand.some(card => card.id === f.shield));
|
||
assert.equal(game.log.filter(entry => entry.automatic?.name === 'Leader Skill lost' && entry.text.includes('Warmaster')).length, 1);
|
||
assert.deepEqual(inventory(game), f.original);
|
||
});
|
||
|
||
void test(`native execution of the known trained ${native} captive pays two and returns the skill once`, () => {
|
||
const f = fixture(native), before = p(f.game, 'harkonnen').spice;
|
||
const game = accept(f, 'execute');
|
||
const leader = p(game, native).leaders.find(leader => leader.id === f.captive)!;
|
||
assert.equal(leader.dead, true); assert.ok(leader.deaths >= 2); assert.equal(leader.capturedBy, undefined);
|
||
assert.equal(p(game, 'harkonnen').spice, before + 2);
|
||
assert.equal(game.leaderSkills!.assignments.some(row => row.leader === f.captive), false);
|
||
assert.equal(game.leaderSkills!.deck.filter(skill => skill === 'warmaster').length, 1);
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.deepEqual(inventory(game), f.original);
|
||
});
|
||
}
|
||
|
||
void test('both actual native capture offers may be declined or canceled with the original dealt Karama', () => {
|
||
for (const native of ['ecaz', 'moritani'] as const) {
|
||
const f = fixture(native);
|
||
for (const mode of ['decline', 'cancel'] as const) {
|
||
const game = mode === 'decline' ? decline(f) : cancel(f);
|
||
const leader = p(game, native).leaders.find(leader => leader.id === f.captive)!;
|
||
assert.equal(leader.dead, false); assert.equal(leader.capturedBy, undefined);
|
||
assert.deepEqual(game.leaderSkills!.assignments.find(row => row.owner === native), f.assignment);
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.equal(game.discard.filter(card => card.id === f.karama).length, mode === 'cancel' ? 1 : 0);
|
||
assert.equal(p(game, 'harkonnen').spice, p(f.game, 'harkonnen').spice);
|
||
assert.deepEqual(inventory(game), f.original);
|
||
}
|
||
}
|
||
});
|
||
|
||
void test('four minimal profiles make legal original capture and captive-disposition choices for both native trainers', () => {
|
||
for (const native of ['ecaz', 'moritani'] as const) {
|
||
const f = fixture(native);
|
||
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] satisfies Difficulty[]) {
|
||
let game = reload(f.game);
|
||
const own = viewGame(game, 'harkonnen'); own.players.find(row => row.id === 'harkonnen')!.bot = difficulty;
|
||
const proposal = botActions(own).find(action => action.type === 'decision' && typeof action.accept === 'boolean');
|
||
assert.ok(proposal, `${native}/${difficulty} supplies its original capture choice`);
|
||
game = act(game, 'harkonnen', proposal, [], f.captive);
|
||
if (proposal.accept) {
|
||
game = advance(game, state => state.decision?.kind === 'capturedLeader', [], f.captive);
|
||
const current = viewGame(game, 'harkonnen'); current.players.find(row => row.id === 'harkonnen')!.bot = difficulty;
|
||
const disposition = botActions(current).find(action => action.type === 'decision' && ['keep', 'execute'].includes(String(action.mode)));
|
||
assert.ok(disposition, `${native}/${difficulty} disposes of the actual emitted captive`);
|
||
game = act(game, 'harkonnen', disposition);
|
||
}
|
||
game = advance(game, state => !state.battle && clean(state));
|
||
assert.equal(skillCount(game, 'warmaster'), 1);
|
||
assert.deepEqual(inventory(game), f.original);
|
||
}
|
||
}
|
||
});
|