91 lines
4.4 KiB
TypeScript
91 lines
4.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { choamPowerAction, choamPowerPlays } from '../game/choam-power-options';
|
|
import { biddingPhysicalIds } from './fixture-homeworld-occupied-bidding';
|
|
import { semutaEmperorPlayer } from './fixture-semuta-emperor-share';
|
|
import {
|
|
advanceSemutaHomeworldChoam, applySemutaHomeworldChoamStep,
|
|
createSemutaHomeworldChoamSetup, holdSemutaHomeworldChoamCard,
|
|
} from './fixture-semuta-homeworld-choam';
|
|
import type { RicheseSkillsTechStep } from './fixture-richese-skills-tech';
|
|
|
|
export type SemutaHomeworldChoamReturnPower = 'laLaLa' | 'gamont';
|
|
export type SemutaHomeworldChoamReturnsFixture = {
|
|
game: Game;
|
|
setup: Game;
|
|
owner: string;
|
|
holder: string;
|
|
target: string;
|
|
card: string;
|
|
power: SemutaHomeworldChoamReturnPower;
|
|
action: Action;
|
|
from: string;
|
|
actions: RicheseSkillsTechStep[];
|
|
originalCards: string[];
|
|
};
|
|
|
|
/** Original native setup, first Revival or closing Mentat, no assigned clocks,
|
|
* wallets, Tanks, force positions or Homeworld custody. The selected Atreides
|
|
* counter is still on its original starting board sector. Only the two physical
|
|
* card positions (Semuta and the printed power) are conserved staging. */
|
|
export function createSemutaHomeworldChoamReturnsFixture(
|
|
power: SemutaHomeworldChoamReturnPower, advanced = false,
|
|
): SemutaHomeworldChoamReturnsFixture {
|
|
const native = createSemutaHomeworldChoamSetup(advanced);
|
|
const { owner, holder, target, actions } = native;
|
|
let game = native.game;
|
|
const card = holdSemutaHomeworldChoamCard(game, owner,
|
|
power === 'laLaLa' ? 'La La La' : 'Trip to Gamont');
|
|
const setup = structuredClone(game);
|
|
const originalCards = biddingPhysicalIds(game);
|
|
const from = Object.keys(semutaEmperorPlayer(setup, target).forces)[0];
|
|
assert.ok(from, 'The original Atreides setup has an on-board force');
|
|
game = advanceSemutaHomeworldChoam(game, state => power === 'laLaLa'
|
|
? state.phase === 4 && !state.phaseOpening && !state.response && !state.decision && !state.pendingTreacheryDiscard
|
|
: state.decision?.kind === 'choamMentat', actions);
|
|
assert.equal(game.turn, setup.turn, 'Never advance to a replacement source turn');
|
|
assert.equal(game.phase, power === 'laLaLa' ? 4 : 8);
|
|
assert.deepEqual(semutaEmperorPlayer(game, target).forces,
|
|
semutaEmperorPlayer(setup, target).forces, 'The recalled force is original, not a staged shipment or casualty');
|
|
assert.deepEqual(biddingPhysicalIds(game), originalCards);
|
|
if (power === 'gamont') {
|
|
assert.equal(game.decision!.player, owner);
|
|
assert.equal(game.choamMentatPending, true);
|
|
}
|
|
const view = viewGame(game, owner);
|
|
const play = choamPowerPlays(view, power).find(play => play.source === 'printed' && play.card.id === card);
|
|
assert.ok(play);
|
|
const action = choamPowerAction(view, play, { target, ...(power === 'gamont' ? { from, elite: 0 } : {}) });
|
|
assert.ok(action, 'The original private controls admit the printed power');
|
|
return { game, setup, owner, holder, target, card, power, action, from, actions, originalCards };
|
|
}
|
|
|
|
/** Complete the real Karama response queue, stopping at the cost offer. */
|
|
export function playSemutaHomeworldChoamReturns(
|
|
fixture: SemutaHomeworldChoamReturnsFixture, state = fixture.game,
|
|
): Game {
|
|
let game = applySemutaHomeworldChoamStep(state, fixture.owner, fixture.action);
|
|
for (let n = 0; game.response?.kind === 'choamWorthless' && n < game.players.length; n++) {
|
|
const actor = game.players.find(player => !game.response!.passed.includes(player.id))!.id;
|
|
game = applyAction(game, actor, { type: 'passResponse' });
|
|
}
|
|
assert.notEqual(game.response?.kind, 'choamWorthless');
|
|
return game;
|
|
}
|
|
|
|
export function semutaHomeworldChoamReturnsForces(game: Game) {
|
|
const worlds = viewGame(game, game.host).homeworlds!.worlds!;
|
|
return game.players.map(player => {
|
|
const away = worlds.filter(home => home.native !== player.id)
|
|
.map(home => home.forces[player.id]).filter(Boolean);
|
|
return {
|
|
id: player.id,
|
|
total: player.reserves + player.tanks + Object.values(player.forces).reduce((sum, n) => sum + n, 0)
|
|
+ away.reduce((sum, group) => sum + group.normal + group.elite, 0),
|
|
elite: (player.elites?.reserves ?? 0) + (player.elites?.tanks ?? 0)
|
|
+ Object.values(player.elites?.forces ?? {}).reduce((sum, n) => sum + n, 0)
|
|
+ away.reduce((sum, group) => sum + group.elite, 0),
|
|
};
|
|
});
|
|
}
|