Files
Dune/tests/fixture-homeworld-occupied-defenses.ts

119 lines
5.6 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction,
createGame,
initializeHomeworldOccupationGameForAudit,
joinGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import type { FactionId } from '../game/catalog';
import { botActions } from '../game/bots';
import { homeworldContext } from '../game/homeworld-game';
import { homeworldForceGroups } from '../game/homeworld-custody';
import { observeHomeworldOccupation } from '../game/homeworld-occupation-history';
const sum = (counts: Record<string, number>) => Object.values(counts).reduce((a, b) => a + b, 0);
export const defensePlayer = (game: Game, id: string) => game.players.find(p => p.id === id)!;
/** Original lobby and fresh Homeworld setup, completed through original legal
* bot choices. Controlled positions below are fixtures, not natural play. */
export function freshDefenseGame(native: FactionId, advanced = true): Game {
const expansion = native === 'tleilaxu' ? ['ix'] : native === 'moritani' ? ['ecaz'] : [];
let game = createGame('OCCUPIEDDEFENSES', newPlayer('native', native, native), advanced, expansion);
for (const [id, faction] of [
['occupier', 'guild'], ['ally', 'atreides'], ['opponent', 'harkonnen'], ['fremen', 'fremen'],
] as const) joinGame(game, newPlayer(id, id, faction));
game = applyAction(game, 'native', { type: 'homeworlds', enabled: true });
for (const p of game.players) game = applyAction(game, p.id, { type: 'ready' });
game = initializeHomeworldOccupationGameForAudit(game);
for (let step = 0; game.status === 'setup' && step < 80; step++) {
let progressed = false;
for (const p of game.players) {
const view = viewGame(game, p.id);
view.players.find(player => player.id === p.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (!action) continue;
game = applyAction(game, p.id, action);
progressed = true;
break;
}
assert.ok(progressed, 'original fresh setup must have a legal next choice');
}
assert.equal(game.status, 'playing');
assert.equal(game.homeworldOccupationPreview, true);
assert.ok(game.homeworldOccupationHistory);
assert.deepEqual(game.homeworldOccupationHistory.qualifications, []);
assertDefenseInventory(game);
return game;
}
export function recordDefensePosition(game: Game, label: string, cause: 'change' | 'turnEnd' = 'change') {
game.homeworldOccupationHistory = observeHomeworldOccupation(
game.homeworldOccupationHistory!, homeworldContext(game), game.homeworlds!.custody!,
game.turn, cause, `controlled-defense-${label}-${game.homeworldOccupationHistory!.sources.length}`,
);
assertDefenseInventory(game);
}
/** Conserved native casualties: each physical normal/starred reserve counter
* moves to its own Tanks pile; other worlds and Arrakis counters are untouched. */
export function clearDefenseNative(game: Game, world: string) {
const home = homeworldForceGroups(homeworldContext(game), game.homeworlds!.custody!)
.find(home => home.id === world)!;
const forces = home.forces[home.native];
const native = defensePlayer(game, home.native);
native.reserves -= forces.normal + forces.elite;
native.tanks += forces.normal + forces.elite;
if (forces.elite) {
native.elites!.reserves -= forces.elite;
native.elites!.tanks += forces.elite;
}
if (home.secondary) game.homeworlds!.custody!.salusa = { normal: 0, elite: 0 };
}
/** Conserved normal reserve-to-visitor transfer; no qualification is fabricated. */
export function addDefenseVisitor(game: Game, world: string, id = 'occupier') {
defensePlayer(game, id).reserves--;
(game.homeworlds!.custody!.visitors[world] ??= {})[id] = { normal: 1, elite: 0 };
}
export function removeDefenseVisitor(game: Game, world: string, id = 'occupier') {
const visitors = game.homeworlds!.custody!.visitors[world];
const forces = visitors[id];
assert.equal(forces.elite, 0);
defensePlayer(game, id).reserves += forces.normal;
delete visitors[id];
if (!Object.keys(visitors).length) delete game.homeworlds!.custody!.visitors[world];
}
export function qualifyDefensePosition(game: Game, world: string) {
clearDefenseNative(game, world);
addDefenseVisitor(game, world);
recordDefensePosition(game, 'conserved-native-casualties-and-sole-arrival');
const fact = game.homeworldOccupationHistory!.qualifications.find(fact => fact.world === world)!;
assert.equal(fact.player, 'occupier');
assert.equal(fact.cause, 'sole');
assert.equal(fact.turn, game.turn);
}
export function assertDefenseInventory(game: Game) {
const groups = homeworldForceGroups(homeworldContext(game), game.homeworlds!.custody!);
for (const p of game.players) {
const visitors = groups.filter(home => home.native !== p.id).map(home => home.forces[p.id]);
const away = visitors.reduce((n, group) => n + (group ? group.normal + group.elite : 0), 0);
const starredAway = visitors.reduce((n, group) => n + (group?.elite ?? 0), 0);
assert.equal(p.reserves + p.tanks + sum(p.forces) + away, 20, `${p.id} physical counters`);
if (p.elites) {
assert.equal(p.elites.reserves + p.elites.tanks + sum(p.elites.forces) + starredAway,
p.faction === 'emperor' ? 5 : p.faction === 'fremen' ? 3 : 7, `${p.id} starred identity`);
assert.ok(p.elites.reserves <= p.reserves);
assert.ok(p.elites.tanks <= p.tanks);
for (const [key, n] of Object.entries(p.elites.forces)) assert.ok(n <= p.forces[key]);
}
}
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand)];
assert.equal(new Set(cards.map(card => card.id)).size, cards.length, 'original Treachery IDs are unique');
}