128 lines
8.4 KiB
TypeScript
128 lines
8.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { initializeHomeworldOccupationGameForAudit, viewGame, type Game } from '../game/engine';
|
|
import { MOBILE_STRONGHOLD } from '../game/board';
|
|
import { treacheryDeck } from '../game/cards';
|
|
import { nativeShipmentSources } from '../game/homeworld-options';
|
|
import { forceRevivalQuote } from '../game/revival';
|
|
import { ownedTech } from '../game/tech-tokens';
|
|
import {
|
|
createMixedPhaseModulesFixture, createMixedPhaseModulesProgramme,
|
|
mixedPhaseModulesClean, mixedPhaseModulesInventory, mixedPhaseModulesPlayer as player,
|
|
type MixedPhaseModulesOptions,
|
|
} from './fixture-mixed-phase-modules';
|
|
|
|
function freshWorlds(game: Game) {
|
|
assert.ok(game.homeworlds); assert.equal(game.homeworlds.custody, null);
|
|
assert.equal(game.homeworldOccupationPreview, true);
|
|
assert.equal(game.pendingHomeworldOccupiedIncome, null);
|
|
assert.deepEqual(game.homeworldOccupiedIncomeHistory, []);
|
|
assert.ok(game.homeworldOccupiedPercentageLedger);
|
|
if (game.players.some(seat => seat.faction === 'choam')) {
|
|
assert.equal(game.homeworldTupilePreview, true); assert.ok(game.homeworldTupileState);
|
|
assert.equal(game.pendingHomeworldTupileCleanup, null);
|
|
}
|
|
if (game.players.some(seat => seat.faction === 'ecaz')) {
|
|
assert.equal(game.homeworldEcazVictoryPreview, true); assert.equal(game.ecazOccupyPreview, true);
|
|
}
|
|
}
|
|
|
|
/** Source/final component facts only, without a save/custody campaign. Native
|
|
* World pools are the physical reserve locations, rather than extra counters. */
|
|
function components(game: Game) {
|
|
const worlds = viewGame(game, game.host).homeworlds?.worlds; assert.ok(worlds?.length);
|
|
for (const seat of game.players) {
|
|
const native: number = worlds.filter(world => world.native === seat.id)
|
|
.reduce((total, world) => total + world.forces[seat.id].normal + world.forces[seat.id].elite, 0);
|
|
const visitors: number = worlds.filter(world => world.native !== seat.id)
|
|
.reduce((total, world) => total + (world.forces[seat.id]?.normal ?? 0) + (world.forces[seat.id]?.elite ?? 0), 0);
|
|
assert.equal(native, seat.reserves);
|
|
assert.equal(seat.reserves + seat.tanks + visitors + Object.values(seat.forces).reduce((a, b) => a + b, 0), 20);
|
|
}
|
|
}
|
|
|
|
void test('the explicit legacy occupation Skills entry does not inherit clean World phase-module admission', () => {
|
|
const f = createMixedPhaseModulesFixture({ factions: ['atreides', 'emperor', 'guild'], expansions: [],
|
|
advanced: true, homeworlds: true, tech: true });
|
|
const before = structuredClone(f.lobby);
|
|
assert.throws(() => initializeHomeworldOccupationGameForAudit(f.lobby, true), /Occupation requires/);
|
|
assert.deepEqual(f.lobby, before);
|
|
});
|
|
|
|
void test('public bare Basic Worlds start mixed selected families and classic no-deck seats with original occupation state', () => {
|
|
const entries: MixedPhaseModulesOptions[] = [
|
|
{ factions: ['ecaz', 'ixians', 'tleilaxu'], expansions: ['ix', 'choam', 'ecaz'] },
|
|
{ factions: ['atreides', 'emperor'], expansions: [] },
|
|
];
|
|
for (const selected of entries) {
|
|
const f = createMixedPhaseModulesFixture({ ...selected, homeworlds: true });
|
|
assert.equal(f.initial.homeworlds, undefined); assert.deepEqual(f.lobby.homeworlds, { custody: null });
|
|
const option = f.actions.findIndex(step => step.action.type === 'homeworlds' && step.action.enabled === true);
|
|
const ready = f.actions.findIndex(step => step.action.type === 'ready');
|
|
assert.ok(option >= 0 && option < ready); assert.equal(f.actions[option].actor, f.initial.host);
|
|
assert.equal(f.setup.status, 'setup'); freshWorlds(f.setup);
|
|
assert.equal(f.game.advanced, false); assert.equal(f.game.phase, 1); assert.equal(f.game.turn, 1);
|
|
assert.equal(f.homeworlds, true); assert.equal(f.independent, false);
|
|
assert.ok(!f.game.techTokens && !f.game.strongholdCards && !f.game.ecazTreachery &&
|
|
!f.game.leaderSkills && !f.game.nexusCards && !f.game.discoveryEnabled && !f.game.semutaPreview);
|
|
assert.deepEqual(f.game.expansions, selected.expansions);
|
|
assert.equal(f.originalInventory.length, treacheryDeck(selected.expansions).length);
|
|
assert.deepEqual(mixedPhaseModulesInventory(f.game), f.originalInventory);
|
|
components(f.afterSetup); assert.deepEqual(f.staging, []);
|
|
}
|
|
});
|
|
|
|
for (const advanced of [false, true]) void test(`public all-six ${advanced ? 'Advanced World independent cards, Tech and Strongholds' : 'Basic World Tech'} earns native shipments, revival and delayed payouts`, () => {
|
|
const f = createMixedPhaseModulesProgramme({ advanced, homeworlds: true, independent: advanced });
|
|
freshWorlds(f.setup); components(f.afterSetup);
|
|
assert.equal(f.setup.deck.length, advanced ? 50 : 47); assert.equal(f.setup.richeseCache?.length, 10);
|
|
assert.equal(f.originalInventory.length, advanced ? 60 : 57);
|
|
assert.equal(f.setup.ecazTreachery === true, advanced);
|
|
assert.ok(!f.game.leaderSkills && !f.game.nexusCards && !f.game.discoveryEnabled && !f.game.semutaPreview);
|
|
for (const [actor, receipt, key] of [
|
|
['choam', f.choamShipment, f.choamLocation], ['moritani', f.moritaniShipment, f.casualtyLocation],
|
|
] as const) {
|
|
const action = receipt.steps[0].action;
|
|
assert.equal(action.type, 'ship'); assert.equal(action.amount, 1); assert.equal(action.elite, 0);
|
|
assert.deepEqual(action.homeworldSources, nativeShipmentSources(viewGame(receipt.before, actor), 1, 0));
|
|
assert.equal(player(receipt.after, actor).reserves, player(receipt.before, actor).reserves - 1);
|
|
assert.equal(player(receipt.after, actor).forces[key], 1);
|
|
assert.equal(player(receipt.before, actor).spice - player(receipt.after, actor).spice, actor === 'choam' ? 1 : 2);
|
|
const earned = ownedTech(receipt.after.techTokens, 'ixians').length;
|
|
assert.equal(receipt.after.techTokens?.heighliners.spice, earned);
|
|
assert.equal(receipt.after.techTokens?.heighliners.triggeredTurn, 1);
|
|
assert.equal(player(receipt.after, 'ixians').spice, player(receipt.before, 'ixians').spice);
|
|
}
|
|
const shipmentIncome = f.shipmentPayout.before.techTokens!.heighliners.spice;
|
|
assert.ok(shipmentIncome > 0); assert.equal(f.shipmentPayout.after.techTokens?.heighliners.spice, 0);
|
|
assert.equal(player(f.shipmentPayout.after, 'ixians').spice - player(f.shipmentPayout.before, 'ixians').spice, shipmentIncome);
|
|
if (advanced) {
|
|
assert.equal(f.mentat.before.strongholdCards?.claimedTurn, 0);
|
|
assert.equal(f.mentat.after.strongholdCards?.claimedTurn, 1);
|
|
assert.equal(f.mentat.after.strongholdCards?.owners.carthag, 'moritani');
|
|
assert.equal(f.mentat.after.strongholdCards?.owners.sietch_tabr, 'choam');
|
|
assert.equal(f.mentat.after.strongholdCards?.owners[MOBILE_STRONGHOLD], 'ixians');
|
|
}
|
|
assert.equal(f.storm.before.stormMovementSource?.kind, advanced ? 'card' : 'dials');
|
|
assert.equal(f.storm.before.stormPending, advanced ? 1 : 2);
|
|
assert.equal(player(f.storm.after, 'moritani').tanks, 1);
|
|
assert.equal(player(f.storm.after, 'moritani').forces[f.casualtyLocation], undefined);
|
|
const view = viewGame(f.revival.before, 'moritani');
|
|
assert.ok(view.revival.freeRemaining >= 1); assert.equal(view.revival.tleilaxuHomeworldIncomeBlocked, false);
|
|
const quote = forceRevivalQuote(f.revival.before, player(f.revival.before, 'moritani'), 1, 0);
|
|
assert.equal(quote.free, 1); assert.equal(quote.cost, 0);
|
|
assert.deepEqual(f.revival.steps[0], { actor: 'moritani', action: { type: 'revive', amount: 1, elite: 0 } });
|
|
if (advanced) assert.equal(f.revival.before.revivalOrder?.remaining[0], 'moritani');
|
|
assert.equal(player(f.revival.after, 'moritani').tanks, 0);
|
|
assert.equal(player(f.revival.after, 'moritani').reserves - player(f.revival.before, 'moritani').reserves, 1);
|
|
assert.equal(player(f.revival.after, 'moritani').spice, player(f.revival.before, 'moritani').spice - quote.cost);
|
|
assert.equal(player(f.revival.after, 'tleilaxu').spice - player(f.revival.before, 'tleilaxu').spice, 1);
|
|
assert.equal(f.revival.after.techTokens?.axlotl.spice, ownedTech(f.revival.after.techTokens, 'tleilaxu').length);
|
|
const revivalIncome = f.revivalPayout.before.techTokens!.axlotl.spice;
|
|
assert.ok(revivalIncome > 0); assert.equal(f.revivalPayout.after.techTokens?.axlotl.spice, 0);
|
|
assert.equal(player(f.revivalPayout.after, 'tleilaxu').spice - player(f.revivalPayout.before, 'tleilaxu').spice, revivalIncome);
|
|
assert.equal(f.game.phase, 5); assert.equal(f.game.turn, 2); assert.ok(mixedPhaseModulesClean(f.game));
|
|
assert.deepEqual(mixedPhaseModulesInventory(f.game), f.originalInventory); components(f.game);
|
|
assert.deepEqual(f.staging, []);
|
|
});
|