129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
import { CHOAM_NEXUS_EFFECTS, type NexusChoamEffect } from './nexus-choam';
|
|
import type { Card } from './cards';
|
|
import type { Action, GameView } from './engine';
|
|
|
|
export type ChoamPowerEffect = NexusChoamEffect;
|
|
export type ChoamPowerPlay = {
|
|
source: 'printed' | 'nexus';
|
|
effect: ChoamPowerEffect;
|
|
card: Card;
|
|
event?: string;
|
|
blocked: string | null;
|
|
};
|
|
export const CHOAM_POWER_NAMES = CHOAM_NEXUS_EFFECTS;
|
|
export const choamPowerKey = (play: ChoamPowerPlay) =>
|
|
`${play.source}/${play.effect}/${play.card.id}/${play.event ?? ''}`;
|
|
|
|
/** Private server choices keep the physical cost separate from the effect. */
|
|
export function choamPowerPlays(
|
|
game: GameView,
|
|
effect: ChoamPowerEffect,
|
|
): ChoamPowerPlay[] {
|
|
if (game.players.find((player) => player.id === game.me)?.faction !== 'choam')
|
|
return [];
|
|
const state = game.choamWorthless as
|
|
| (NonNullable<GameView['choamWorthless']> & { plays?: ChoamPowerPlay[] })
|
|
| null;
|
|
if (!state) return [];
|
|
// Older personalized fixtures retain the printed-only descriptor.
|
|
const plays =
|
|
state.plays ??
|
|
state.cards
|
|
.filter((card) => card.name === CHOAM_POWER_NAMES[effect])
|
|
.map((card) => ({
|
|
source: 'printed' as const,
|
|
effect,
|
|
card,
|
|
blocked: null,
|
|
}));
|
|
return plays.filter((play) => play.effect === effect);
|
|
}
|
|
|
|
export function choamPowerAction(
|
|
game: GameView,
|
|
requested: ChoamPowerPlay,
|
|
selection: Record<string, unknown> = {},
|
|
): Action | null {
|
|
if (
|
|
Object.keys(selection).some(
|
|
(key) =>
|
|
!['target', 'territory', 'from', 'elite', 'amount'].includes(key),
|
|
)
|
|
)
|
|
return null;
|
|
const owner = game.players.find((player) => player.id === game.me);
|
|
if (
|
|
owner?.faction !== 'choam' ||
|
|
game.status !== 'playing' ||
|
|
game.response ||
|
|
game.truthtrance ||
|
|
game.phaseOpening ||
|
|
game.automaticContinuationPending ||
|
|
game.nexusTraitors?.pending ||
|
|
game.nexusCards?.waiting.length
|
|
)
|
|
return null;
|
|
const play = choamPowerPlays(game, requested.effect).find(
|
|
(candidate) => choamPowerKey(candidate) === choamPowerKey(requested),
|
|
);
|
|
if (!play || play.blocked || play.effect === 'kull') return null;
|
|
const phases = { kulon: 5, laLaLa: 4, gamont: 8, baliset: 5, jubba: 0 };
|
|
if (game.phase !== phases[play.effect]) return null;
|
|
const decisions = {
|
|
kulon: null,
|
|
laLaLa: 'choamFreeRevival',
|
|
gamont: 'choamMentat',
|
|
baliset: 'choamMovement',
|
|
jubba: 'choamStorm',
|
|
};
|
|
if (
|
|
game.decision &&
|
|
(game.decision.kind !== decisions[play.effect] ||
|
|
game.decision.player !== game.me)
|
|
)
|
|
return null;
|
|
if (play.effect === 'jubba' && game.decision?.kind !== 'choamStorm')
|
|
return null;
|
|
if (
|
|
play.effect === 'kulon' &&
|
|
(game.active !== game.me || (owner.moved ?? 0) >= (owner.movesAllowed ?? 1))
|
|
)
|
|
return null;
|
|
if (
|
|
play.source === 'nexus' &&
|
|
(!play.event || owner.ally || game.nexusCards?.card !== 'choam')
|
|
)
|
|
return null;
|
|
if (
|
|
play.source === 'printed' &&
|
|
(play.card.kind !== 'worthless' ||
|
|
play.card.name !== CHOAM_POWER_NAMES[play.effect])
|
|
)
|
|
return null;
|
|
return {
|
|
...selection,
|
|
type: 'card',
|
|
mode: 'choam',
|
|
card: play.card.id,
|
|
...(play.source === 'nexus'
|
|
? { nexus: play.event, effect: play.effect }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
/** Preserve printed powers first, then choose the cheapest legal Nexus fuel. */
|
|
export function choamPowerBotPlay(
|
|
game: GameView,
|
|
effect: ChoamPowerEffect,
|
|
value: (card: Card) => number,
|
|
): ChoamPowerPlay | undefined {
|
|
return choamPowerPlays(game, effect)
|
|
.filter((play) => choamPowerAction(game, play))
|
|
.sort(
|
|
(a, b) =>
|
|
Number(a.source === 'nexus') - Number(b.source === 'nexus') ||
|
|
value(a.card) - value(b.card) ||
|
|
a.card.id.localeCompare(b.card.id),
|
|
)[0];
|
|
}
|