Files
Dune/tests/fixture-homeworld-occupied-tupile.ts
T

115 lines
5.5 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeHomeworldOccupationGameForAudit,
joinGame, newPlayer, viewGame, type Game,
} from '../game/engine';
import { FACTIONS, type FactionId } from '../game/catalog';
import { botActions } from '../game/bots';
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
import { observeHomeworldOccupation } from '../game/homeworld-occupation-history';
import { biddingPhysicalIds } from './fixture-homeworld-occupied-bidding';
import { assertDefenseInventory, clearDefenseNative } from './fixture-homeworld-occupied-defenses';
export const TUPILE_WORLD = 'homeworld:choam';
export const tupilePlayer = (game: Game, id: string) => game.players.find(player => player.id === id)!;
/** Real selected decks/lobby/fresh audit entry and all original setup choices.
* Later positions are explicitly labelled conserved fixtures, not play traces. */
export function freshTupileFixture(advanced = true, occupierFaction: 'guild' | 'moritani' = 'guild') {
const roster: [string, FactionId][] = [
['native', 'choam'], ['occupier', occupierFaction], ['ally', 'harkonnen'],
['observer', 'beneGesserit'], ['fremen', 'fremen'],
];
const expansions = [...new Set(roster.map(([, faction]) =>
FACTIONS.find(card => card.id === faction)!.expansion))].filter(expansion => expansion !== 'base');
let game = createGame('OCCUPIEDTUPILE', newPlayer('native', 'native', 'choam'), advanced, expansions);
for (const [id, faction] of roster.slice(1)) joinGame(game, newPlayer(id, id, faction));
game = applyAction(game, 'native', { type: 'homeworlds', enabled: true });
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
game = initializeHomeworldOccupationGameForAudit(game);
for (let step = 0; game.status === 'setup' && step < 100; step++) {
let progressed = false;
for (const player of game.players) {
const view = viewGame(game, player.id);
view.players.find(seat => seat.id === player.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (!action) continue;
game = applyAction(game, player.id, action);
progressed = true;
break;
}
assert.ok(progressed, 'original Tupile setup has a legal next choice');
}
assert.equal(game.status, 'playing');
assert.equal(game.homeworldOccupationPreview, true);
assert.deepEqual(game.homeworldOccupationHistory!.qualifications, []);
assertDefenseInventory(game);
assert.equal(game.homeworldTupilePreview, true, 'only the original fresh entry initializes this profile');
const cards = biddingPhysicalIds(game);
const setup = structuredClone(game);
return { game, cards, setup };
}
export function recordTupilePosition(game: Game, label: string, cause: 'change' | 'turnStart' | 'turnEnd' = 'change') {
game.homeworldOccupationHistory = observeHomeworldOccupation(game.homeworldOccupationHistory!,
homeworldContext(game), game.homeworlds!.custody!, game.turn, cause,
`controlled-tupile-${label}-${game.homeworldOccupationHistory!.sources.length}`);
assertDefenseInventory(game);
homeworldGameIntegrity(game);
}
/** Typed transfer from the actual normal/starred reserve pools. Reserves is the
* aggregate pool; starred counters are a subset, not additional armies. */
export function addTupileForces(game: Game, id: string, normal = 1, elite = 0) {
const player = tupilePlayer(game, id);
assert.ok(player.reserves - (player.elites?.reserves ?? 0) >= normal);
assert.ok(elite === 0 || player.elites && player.elites.reserves >= elite);
player.reserves -= normal + elite;
if (elite) player.elites!.reserves -= elite;
const group = (game.homeworlds!.custody!.visitors[TUPILE_WORLD] ??= {})[id] ??= { normal: 0, elite: 0 };
group.normal += normal;
group.elite += elite;
assertDefenseInventory(game);
}
export function removeTupileForces(game: Game, id: string) {
const group = game.homeworlds!.custody!.visitors[TUPILE_WORLD][id];
const player = tupilePlayer(game, id);
player.reserves += group.normal + group.elite;
if (group.elite) player.elites!.reserves += group.elite;
delete game.homeworlds!.custody!.visitors[TUPILE_WORLD][id];
if (!Object.keys(game.homeworlds!.custody!.visitors[TUPILE_WORLD]).length)
delete game.homeworlds!.custody!.visitors[TUPILE_WORLD];
assertDefenseInventory(game);
}
export function occupiedTupileFixture(advanced = true, allied = true) {
const fixture = freshTupileFixture(advanced), game = fixture.game;
clearDefenseNative(game, TUPILE_WORLD);
if (allied) {
tupilePlayer(game, 'occupier').ally = 'ally';
tupilePlayer(game, 'ally').ally = 'occupier';
}
addTupileForces(game, 'occupier');
recordTupilePosition(game, 'original-native-casualties-and-sole-arrival');
assert.deepEqual(biddingPhysicalIds(game), fixture.cards);
return fixture;
}
/** Conserved native repopulation fixture; no counters are created to cross high. */
export function restoreTupileNative(game: Game, amount: number) {
const native = tupilePlayer(game, 'native');
assert.ok(native.tanks >= amount);
native.tanks -= amount;
native.reserves += amount;
assertDefenseInventory(game);
}
/** Original-card discard at the caller-owned atomic settlement boundary. The
* helper itself neither chooses cards nor duplicates the original discard. */
export function discardTupileExcess(game: Game, id: string, limit: number) {
const before = biddingPhysicalIds(game), player = tupilePlayer(game, id);
while (player.hand.length > limit) game.discard.push(player.hand.pop()!);
assert.deepEqual(biddingPhysicalIds(game), before);
}