Files
Dune/tests/ix-homeworld-revival-bonus-runtime.test.ts

137 lines
8.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { MOBILE_LOCATION } from '../game/board';
import { forceRevivalQuote } from '../game/revival';
import { quoteIxSuboidBonus } from '../game/ix-homeworld-revival';
import {
createIxHomeworldRevivalBonusFixture as fixture,
reviveIxHomeworldRevivalBonus as revive,
ixHomeworldRevivalBonusPlayer as player,
ixHomeworldRevivalBonusInventory as inventory,
ixHomeworldRevivalBonusPopulation as population,
ixHomeworldRevivalBonusNativeForces as nativeForces,
finishIxHomeworldRevivalBonus as finish,
} from './fixture-ix-homeworld-revival-bonus';
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} actual paid Cyborg return adds two real Suboids beyond the ordinary three-force quota`, () => {
const f = fixture({ advanced }), before = structuredClone(player(f.game, f.owner));
const source = structuredClone(f.game), homeBefore = nativeForces(f.game, f.owner);
assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 4);
assert.equal(population(f.game, f.owner).population, 9);
assert.equal(population(f.game, f.owner).side, 'high');
assert.equal(before.tanks, 5); assert.equal(before.elites?.tanks, 1);
assert.equal(f.afterBattle.lastBattleContext?.winner, f.opponent);
assert.equal(player(f.afterBattle, f.opponent).tanks, 0);
assert.equal(f.shipment.steps[0].action.type, 'ship');
assert.ok(f.actions.some(step => step.action.type === 'homeworlds' && step.action.enabled === true));
assert.ok(f.actions.some(step => step.action.type === 'start'));
assert.equal(f.sourceActionCount, f.actions.length);
assert.equal(viewGame(f.game, f.owner).revival.ixSuboidBonusEnabled, true);
assert.equal(viewGame(f.game, f.opponent).revival.ixSuboidBonusEnabled, false);
const quote = forceRevivalQuote(f.game, before, 3, 1, 0);
assert.deepEqual(quote, { free: 1, normalCost: 5, cost: 5 });
assert.equal(quoteIxSuboidBonus({ enabled: true, amount: 3, elite: 1, free: quote.free,
freeElite: 0, normalTanks: before.tanks - before.elites!.tanks }), 2);
const result = revive(f.game, f), after = player(result.game, f.owner);
assert.deepEqual(f.game, source, 'The original source remains available for the other native branches');
assert.equal(after.reserves, before.reserves + 5);
assert.equal(after.elites?.reserves, before.elites!.reserves + 1);
assert.equal(after.tanks, 0); assert.equal(after.elites?.tanks, 0);
assert.equal(after.revived, 3, 'Only the declared three forces consume normal revival allowance');
assert.equal(after.freeForcesRevived, 1, 'Additional Suboids do not consume the ordinary free quota');
assert.equal(after.elites?.revived, 1);
assert.equal(before.spice - after.spice, 5);
assert.deepEqual(nativeForces(result.game, f.owner), { normal: homeBefore.normal + 4, elite: homeBefore.elite + 1 });
assert.equal(after.forces[MOBILE_LOCATION], 6);
assert.equal(after.elites?.forces[MOBILE_LOCATION], 3);
assert.equal(viewGame(result.game, f.owner).revival.forcesRemaining, 0);
assert.deepEqual(inventory(result.game), f.originalInventory);
const movement = finish(result.game);
assert.equal(movement.turn, 2); assert.equal(movement.phase, 5);
assert.equal(player(movement, f.owner).reserves, after.reserves);
assert.equal(player(movement, f.owner).tanks, 0);
assert.deepEqual(inventory(movement), f.originalInventory);
});
}
void test('an actual earlier free Suboid remains completed and the later paid Cyborg bonus uses no extra ordinary slots', () => {
const f = fixture({ priorFree: true }), before = structuredClone(player(f.game, f.owner));
assert.ok(f.priorFree);
assert.equal(player(f.beforePriorFree, f.owner).tanks, 5);
assert.equal(before.tanks, 4); assert.equal(before.revived, 1); assert.equal(before.freeForcesRevived, 1);
const result = revive(f.game, f), after = player(result.game, f.owner);
assert.equal(after.reserves - before.reserves, 4);
assert.equal(after.elites!.reserves - before.elites!.reserves, 1);
assert.equal(after.tanks, 0); assert.equal(after.elites?.tanks, 0);
assert.equal(before.spice - after.spice, 5);
assert.equal(after.revived, 3); assert.equal(after.freeForcesRevived, 1);
assert.deepEqual(inventory(result.game), f.originalInventory);
});
void test('a genuine free Cyborg return gives no extra Suboids or paid invoice', () => {
const f = fixture(), before = structuredClone(player(f.game, f.owner));
const result = revive(f.game, f, f.freeCyborg), after = player(result.game, f.owner);
assert.equal(after.reserves, before.reserves + 1);
assert.equal(after.elites?.reserves, before.elites!.reserves + 1);
assert.equal(after.tanks, 4); assert.equal(after.elites?.tanks, 0);
assert.equal(after.spice, before.spice);
assert.equal(after.revived, 1); assert.equal(after.freeForcesRevived, 1);
assert.deepEqual(inventory(result.game), f.originalInventory);
});
void test('ordinary Suboid revivals create no bonus and a later paid Cyborg returns only the two still in Tanks', () => {
const f = fixture(), before = structuredClone(player(f.game, f.owner));
const suboids = revive(f.game, f, f.suboidAction).game;
const middle = structuredClone(player(suboids, f.owner));
assert.equal(middle.reserves, before.reserves + 2);
assert.equal(middle.elites?.reserves, before.elites?.reserves);
assert.equal(middle.tanks, 3); assert.equal(middle.elites?.tanks, 1);
assert.equal(before.spice - middle.spice, 2);
assert.equal(middle.revived, 2); assert.equal(middle.freeForcesRevived, 1);
const paidCyborg: Action = { type: 'revive', amount: 1, elite: 1, freeElite: 0 };
const result = revive(suboids, f, paidCyborg), after = player(result.game, f.owner);
assert.equal(after.reserves - middle.reserves, 3);
assert.equal(after.elites!.reserves - middle.elites!.reserves, 1);
assert.equal(middle.spice - after.spice, 3);
assert.equal(after.tanks, 0); assert.equal(after.revived, 3); assert.equal(after.freeForcesRevived, 1);
assert.deepEqual(inventory(result.game), f.originalInventory);
});
void test('the original privately drafted Ghola returns its Cyborg without the paid-revival bonus or normal allowance use', () => {
const f = fixture({ ghola: true }), before = structuredClone(player(f.game, f.owner));
assert.ok(f.ghola && f.gholaAction);
assert.equal(f.startingDraft.steps[0].action.card, f.ghola);
assert.ok(before.hand.some(card => card.id === f.ghola));
const result = revive(f.game, f, f.gholaAction), after = player(result.game, f.owner);
assert.equal(after.reserves, before.reserves + 1);
assert.equal(after.elites?.reserves, before.elites!.reserves + 1);
assert.equal(after.tanks, 4); assert.equal(after.elites?.tanks, 0);
assert.equal(after.spice, before.spice);
assert.equal(after.revived, before.revived);
assert.equal(after.freeForcesRevived, before.freeForcesRevived);
assert.equal(after.hand.some(card => card.id === f.ghola), false);
assert.equal(result.game.discard.filter(card => card.id === f.ghola).length, 1);
assert.deepEqual(inventory(result.game), f.originalInventory);
});
void test('all four existing Basic profiles retain legal original revival choices without strategic tuning', () => {
const f = fixture();
for (const difficulty of DIFFICULTIES) {
const view = viewGame(f.game, f.owner); view.players.find(player => player.id === f.owner)!.bot = difficulty;
const action = botActions(view).find(action => action.type === 'revive');
assert.ok(action, `${difficulty} proposes an ordinary revival from the genuine native source`);
const result = revive(structuredClone(f.game), f, action);
const before = player(f.game, f.owner), after = player(result.game, f.owner);
assert.ok(after.tanks < before.tanks);
assert.ok(after.spice >= 0 && after.spice <= before.spice);
assert.ok(after.revived > before.revived && after.revived <= 3);
assert.equal(population(result.game, f.owner).side, 'high');
assert.deepEqual(inventory(result.game), f.originalInventory);
assert.equal(finish(result.game).phase, 5);
}
});