Files
Dune/game/nexus-guild-secret-ally-options.ts
T

127 lines
4.5 KiB
TypeScript

import type { Action, GameView } from './engine';
import { gameTerritories, MOBILE_STRONGHOLD, splitLocation, validGameLocation } from './board';
import { quoteNexusGuildSecretShipment } from './nexus-guild-secret-ally';
import { guildNoFieldSelection } from './guild-no-field-selection';
/** Consume only the current actor's private offer and public faction roster. */
export function nexusGuildSecretAllyCanAct(g: GameView): boolean {
const me = g.players.find((p) => p.id === g.me);
if (
!me ||
me.ally ||
g.nexusCards?.card !== 'guild' ||
g.players.some((p) => p.faction === 'guild')
)
return false;
const offer = g.nexusGuildSecretAlly;
return !!(
offer &&
!offer.blocked &&
offer.event === JSON.stringify(['nexusGuildSecretAlly', g.turn, g.me]) &&
g.status === 'playing' &&
g.phase === 5 &&
g.active === g.me &&
!me.shipped &&
!g.response &&
!g.decision &&
!g.truthtrance &&
!g.phaseOpening &&
!g.automaticContinuationPending &&
!g.nexusCards.waiting.length &&
!g.nexusTraitors?.pending
);
}
/** Price is independent of native faction; route permission is still separate. */
export function nexusGuildSecretAllyQuote(
g: GameView,
destination: string,
amount: number,
noField = false,
) {
if (
!nexusGuildSecretAllyCanAct(g) ||
typeof noField !== 'boolean' ||
!Number.isSafeInteger(amount) ||
amount < 1 ||
amount > (noField ? 21 : 20)
)
return null;
const me = g.players.find((p) => p.id === g.me)!;
if (destination === 'reserves') {
// User ruling 7 October 2026: the Secret Ally return grant overrides the
// Arrakis-to-native-Homeworld restriction for the holder's own reserves.
return quoteNexusGuildSecretShipment('reserves', amount, noField);
}
const world = g.homeworlds?.worlds?.find((w) => w.id === destination);
if (world) {
if (noField || world.native === me.id || world.native === me.ally) return null;
return quoteNexusGuildSecretShipment('homeworld', amount, noField);
}
const to = gameTerritories(g).find((t) => t.id === destination);
if (!to || (destination === MOBILE_STRONGHOLD && me.faction !== 'ixians'))
return null;
return quoteNexusGuildSecretShipment(to.type, amount, noField);
}
/** Add a selected source, never infer card use just from possession. */
export function nexusGuildSecretAllyAction(
g: GameView,
event: string,
action: Action,
): Action | null {
if (
!nexusGuildSecretAllyCanAct(g) ||
event !== g.nexusGuildSecretAlly!.event ||
!['ship', 'guildShip', 'homeworldShip'].includes(action.type) ||
action.nexus !== undefined ||
(action.noField !== undefined && action.type !== 'guildShip') ||
(action.type === 'guildShip' && action.from === 'reserves')
)
return null;
const me = g.players.find((p) => p.id === g.me)!;
const noField = action.noField !== undefined;
let amount: number;
if (noField) {
const marker = guildNoFieldSelection(g, action);
if (!marker || !action.forces || typeof action.forces !== 'object' || Array.isArray(action.forces) ||
!validGameLocation(g, marker.location.territory, marker.location.sector) || marker.location.sector === g.storm)
return null;
let physical = 0;
for (const [key, count] of Object.entries(action.forces)) {
const source = splitLocation(key);
if (!validGameLocation(g, source.territory, source.sector) ||
typeof count !== 'number' || !Number.isSafeInteger(count) || count < 0 ||
count > (me.forces[key] ?? 0)) return null;
if (!count) continue;
if (source.territory !== marker.location.territory || source.sector === g.storm) return null;
physical += count;
}
if (physical > 20) return null;
amount = physical + 1;
} else amount = action.type === 'homeworldShip'
? Object.values(
(action.sources ?? {}) as Record<
string,
{ normal: number; elite: number }
>,
).reduce((n, p) => n + p.normal + p.elite, 0)
: action.forces
? Object.values(action.forces as Record<string, number>).reduce(
(n, p) => n + p,
0,
)
: Number(action.amount);
const destination = String(
action.type === 'homeworldShip' ? action.destination : action.territory,
);
if (!nexusGuildSecretAllyQuote(g, destination, amount, noField)) return null;
if (
action.type === 'ship' &&
(amount > me.reserves ||
!gameTerritories(g).some((t) => t.id === destination))
)
return null;
return { ...action, nexus: event };
}