124 lines
6.0 KiB
TypeScript
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 };
|
|
}
|