Files
Dune/game/homeworld-benefits.ts
T

163 lines
5.5 KiB
TypeScript

import type { Game } from './engine';
import { homeworldContext } from './homeworld-game';
import { homeworldPopulations } from './homeworld-population';
type HomeworldBenefitContext = Pick<
Game,
'advanced' | 'players' | 'homeworlds'
>;
export type HomeworldRevivalOpening = {
turn: number;
tleilaxu: { player: string; low: boolean };
};
export class HomeworldBenefitError extends Error {
constructor(message: string) {
super(message);
this.name = 'HomeworldBenefitError';
}
}
/** Current native population only. The caller owns benefit timing and all
* ordinary eligibility; Salusa never supplies another low-population bonus. */
export function homeworldLowBonus(
context: HomeworldBenefitContext,
playerId: string,
): 0 | 1 {
if (!context.homeworlds?.custody) return 0;
return homeworldPopulations(
homeworldContext(context),
context.homeworlds.custody,
).some(
(home) =>
home.native === playerId &&
home.card !== 'salusa_secundus' &&
home.side === 'low',
)
? 1
: 0;
}
/** Salusa's threshold counts Sardaukar at Salusa, not all Imperial reserves.
* Its high advantage affects the faction's Sardaukar wherever they fight. */
export function homeworldSardaukarFreeSupport(
context: HomeworldBenefitContext,
playerId: string,
): boolean {
if (!context.advanced || !context.homeworlds?.custody) return false;
if (
!context.players.some((p) => p.id === playerId && p.faction === 'emperor')
)
return false;
return homeworldPopulations(
homeworldContext(context),
context.homeworlds.custody,
).some(
(home) =>
home.native === playerId &&
home.card === 'salusa_secundus' &&
home.side === 'high',
);
}
/** Current population can change before sealed plans. The caller renews an
* impossible inspection answer after the return. The remaining preview boundary
* protects sealed dials/support and binding Truthtrance promises from repricing;
* it does not freeze population or forbid Ghola for a whole battle.
* Inspect submission presence, never an opponent's sealed plan contents. */
export function homeworldSardaukarGholaBlock(
context: HomeworldBenefitContext & Pick<Game, 'battle'>,
playerId: string,
): string | null {
if (
!context.advanced ||
!context.homeworlds?.custody ||
!context.battle ||
![context.battle.attacker, context.battle.defender].includes(playerId) ||
!context.players.some((p) => p.id === playerId && p.faction === 'emperor')
)
return null;
const crossing = homeworldPopulations(
homeworldContext(context),
context.homeworlds.custody,
).some(
(home) =>
home.native === playerId &&
home.card === 'salusa_secundus' &&
home.population === 1,
);
if (!crossing) return null;
const battle = context.battle;
const committed = battle.revealed || Object.keys(battle.plans).length > 0 ||
battle.truthPromises?.some((promise) => !promise.released);
return committed
? 'Ghola raising Salusa to high population after a sealed plan or during a binding battle promise is not yet supported. Finish this battle first.'
: null;
}
const validTurn = (turn: unknown): turn is number =>
typeof turn === 'number' && Number.isSafeInteger(turn) && turn > 0;
/** Capture before any revival-opening effect can change native population.
* A saved receipt owns this phase's Tleilaxu free-revival income restriction. */
export function snapshotHomeworldRevival(
context: HomeworldBenefitContext & Pick<Game, 'turn'>,
): HomeworldRevivalOpening | null {
if (!context.homeworlds?.custody) return null;
const tleilaxu = context.players.find(
(player) => player.faction === 'tleilaxu',
);
if (!tleilaxu) return null;
if (!validTurn(context.turn))
throw new HomeworldBenefitError(
'The Homeworld revival opening needs a valid current turn.',
);
return {
turn: context.turn,
tleilaxu: {
player: tleilaxu.id,
low: homeworldLowBonus(context, tleilaxu.id) === 1,
},
};
}
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
/** Read only the saved opening decision. Never infer a missing receipt from
* current populations: earlier revivals may already have crossed a threshold.
* This answers only the free-revival income gate, not Ghola purchase income. */
export function tleilaxuHomeworldFreeIncomeBlocked(
context: HomeworldBenefitContext &
Pick<Game, 'turn' | 'phase'> & {
homeworldRevival?: HomeworldRevivalOpening | null;
},
): boolean {
if (context.phase !== 4 || !context.homeworlds) return false;
const tleilaxu = context.players.filter(
(player) => player.faction === 'tleilaxu',
);
if (!tleilaxu.length) return false;
const receipt: unknown = context.homeworldRevival;
if (
tleilaxu.length !== 1 ||
!validTurn(context.turn) ||
!record(receipt) ||
Object.keys(receipt).length !== 2 ||
!Object.hasOwn(receipt, 'turn') ||
!Object.hasOwn(receipt, 'tleilaxu') ||
receipt.turn !== context.turn ||
!record(receipt.tleilaxu) ||
Object.keys(receipt.tleilaxu).length !== 2 ||
!Object.hasOwn(receipt.tleilaxu, 'player') ||
!Object.hasOwn(receipt.tleilaxu, 'low') ||
typeof receipt.tleilaxu.player !== 'string' ||
!receipt.tleilaxu.player.trim() ||
receipt.tleilaxu.player !== tleilaxu[0].id ||
typeof receipt.tleilaxu.low !== 'boolean'
)
throw new HomeworldBenefitError(
'The current Tleilaxu Homeworld revival-opening receipt is missing, stale or malformed.',
);
return receipt.tleilaxu.low;
}