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

124 lines
6.0 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeHomeworldOccupationGameForAudit, joinGame,
newPlayer, viewGame, type Game,
} from '../game/engine';
import { botActions } from '../game/bots';
import { FACTIONS, type FactionId } from '../game/catalog';
import { HOMEWORLD_CARDS, type HomeworldId } from '../game/homeworld-cards';
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
import { observeHomeworldOccupation } from '../game/homeworld-occupation-history';
/** Original selected decks, fresh Homeworld occupation audit setup and real
* setup actions. Only the later Collection positions/alliance are controlled
* test positions; the original history observes them, not fabricated facts. */
export function occupiedIncomeFixture(cardId: HomeworldId = 'caladan', advanced = false, allied = true) {
const card = HOMEWORLD_CARDS.find(card => card.id === cardId)!;
const nativeFaction = card.faction;
const used = [nativeFaction];
const choose = (choices: FactionId[]) => {
const faction = choices.find(faction => !used.includes(faction))!;
used.push(faction);
return faction;
};
const factions = [nativeFaction, choose(['guild', 'atreides']),
choose(['harkonnen', 'emperor']), choose(['fremen', 'beneGesserit'])];
const expansion = FACTIONS.find(faction => faction.id === nativeFaction)!.expansion;
let game = createGame('OCCUPIEDINCOME', newPlayer('native', 'Native', nativeFaction), advanced,
expansion === 'base' ? [] : [expansion]);
for (const [index, faction] of factions.slice(1).entries())
joinGame(game, newPlayer(['owner', 'ally', 'competitor'][index], faction, 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 < 120; step++) {
let next: Game | undefined;
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) {
next = applyAction(game, player.id, action);
break;
}
}
assert.ok(next, 'Original setup must make progress.');
game = next;
}
assert.equal(game.status, 'playing');
const setup = structuredClone(game);
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
// Controlled, conserved placement. Do not discard/deal or reset card IDs.
for (const player of game.players) {
Object.assign(player, { forces: {}, reserves: 20, tanks: 0, ally: null });
if (player.elites) Object.assign(player.elites, {
reserves: player.faction === 'fremen' ? 3 : player.faction === 'ixians' ? 7 : 5,
tanks: 0, forces: {},
});
}
const custody = game.homeworlds!.custody!;
custody.visitors = {};
if (advanced && game.players.some(player => player.faction === 'emperor'))
custody.salusa = { normal: 0, elite: 5 };
const native = game.players.find(player => player.id === 'native')!;
native.reserves = 0;
native.tanks = 20;
if (native.elites) {
native.elites.tanks = native.elites.reserves;
native.elites.reserves = 0;
}
if (nativeFaction === 'emperor' && advanced) custody.salusa = { normal: 0, elite: 0 };
const owner = game.players.find(player => player.id === 'owner')!;
if (allied) {
owner.ally = 'ally';
game.players.find(player => player.id === 'ally')!.ally = owner.id;
}
const world = `homeworld:${nativeFaction}${cardId === 'salusa_secundus' ? ':salusa' : ''}`;
custody.visitors[world] = { owner: { normal: 1, elite: 0 } };
owner.reserves--;
observeControlledIncomePosition(game, `controlled-${cardId}-sole`);
homeworldGameIntegrity(game);
assert.deepEqual([...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand)].map(card => card.id).sort(), cards);
return { game, world, cards, setup };
}
export function observeControlledIncomePosition(game: Game, event: string): void {
game.homeworldOccupationHistory = observeHomeworldOccupation(game.homeworldOccupationHistory!,
homeworldContext(game), game.homeworlds!.custody!, game.turn, 'change', event);
homeworldGameIntegrity(game);
}
/** Real original phases/actions into Movement; only the conserved position is
* controlled. Never jump the phase clock to obtain a Collection decision. */
export function occupiedIncomeMovementFixture(cardId: HomeworldId = 'caladan', advanced = false, allied = true) {
const fixture = occupiedIncomeFixture(cardId, advanced, allied);
let game = fixture.setup;
for (let step = 0; !(game.phase === 5 && !game.response && !game.decision && !game.phaseOpening) && step < 500; step++) {
let next: Game | undefined;
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;
if (action.type === 'stormDial') action.amount = 0;
next = applyAction(game, player.id, action);
break;
}
assert.ok(next, 'Original phases must reach the controlled Movement boundary.');
game = next;
}
assert.equal(game.phase, 5);
for (const player of game.players) {
assert.equal(player.tanks, 0, 'The position cannot resurrect previously lost counters.');
const position = fixture.game.players.find(seat => seat.id === player.id)!;
player.forces = structuredClone(position.forces);
player.reserves = position.reserves;
player.tanks = position.tanks;
player.ally = position.ally;
if (player.elites) player.elites = structuredClone(position.elites!);
}
game.homeworlds!.custody = structuredClone(fixture.game.homeworlds!.custody!);
observeControlledIncomePosition(game, `controlled-${cardId}-real-movement-boundary`);
return { ...fixture, game };
}