Files
Dune/game/tupile-intelligence.ts
T
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

82 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { FactionId } from './catalog';
import {
homeworldForceGroups,
HomeworldCustodyError,
type HomeworldCustody,
type HomeworldCustodyContext,
} from './homeworld-custody';
import { homeworldPopulations } from './homeworld-population';
/** Qualification/expiry belongs to the occupation lifecycle, not this query. */
export type TupileOccupation = 'unoccupied' | 'occupied' | 'unknown';
export type TupileIntelligenceCategory = 'weapons' | 'defenses';
export type TupileIntelligenceTarget = {
player: string;
faction: FactionId;
/** Public physical contact locations, including either Emperor world. */
contact: string[];
blocked: string | null;
};
/** Public eligibility only. No hand, spice, battle plan, skills or hidden
* No-Field data is accepted or consulted. Used factions persist for the game. */
export function tupileIntelligenceTargets(
context: HomeworldCustodyContext,
custody: HomeworldCustody,
owner: string,
usedFactions: readonly FactionId[],
occupation: TupileOccupation,
): TupileIntelligenceTarget[] {
const worlds = homeworldForceGroups(context, custody);
const choam = context.players.find((p) => p.id === owner);
if (choam?.faction !== 'choam')
throw new HomeworldCustodyError('Only the native CHOAM faction may request Tupile intelligence.');
if (!['unoccupied', 'occupied', 'unknown'].includes(occupation))
throw new HomeworldCustodyError('Tupile intelligence needs an explicit occupation status.');
if (!Array.isArray(usedFactions) || new Set(usedFactions).size !== usedFactions.length ||
usedFactions.some((faction) => faction === 'choam' || !context.players.some((p) => p.faction === faction)))
throw new HomeworldCustodyError('Tupile intelligence usage must name unique seated opposing factions.');
const population = homeworldPopulations(context, custody).find((p) => p.native === owner)!;
const tupile = worlds.find((home) => home.native === owner)!;
const present = (home: typeof tupile, player: string) => {
const group = home.forces[player];
return !!group && group.normal + group.elite > 0;
};
return context.players.filter((p) => p.id !== owner).map((target) => {
const contact = worlds.filter((home) => home.native === target.id && present(home, owner)).map((home) => home.id);
if (present(tupile, target.id)) contact.push(tupile.id);
const blocked = occupation === 'unknown'
? 'Tupile occupation must be resolved before its low advantage can be used.'
: occupation === 'occupied'
? 'Occupied Tupile removes CHOAM’s low-population intelligence advantage.'
: population.side !== 'low'
? 'Tupile intelligence requires at most ten native CHOAM reserves.'
: usedFactions.includes(target.faction)
? 'Tupile intelligence has already been used against this faction in this game.'
: contact.length === 0
? 'CHOAM must be on that faction’s Homeworld, or that faction must be on Tupile.'
: null;
return { player: target.id, faction: target.faction, contact, blocked };
});
}
/** Validate a requested category without reading the answer or consuming use.
* The eventual engine must atomically record one current private disclosure. */
export function quoteTupileIntelligenceRequest(
context: HomeworldCustodyContext,
custody: HomeworldCustody,
owner: string,
usedFactions: readonly FactionId[],
occupation: TupileOccupation,
target: string,
category: TupileIntelligenceCategory,
) {
if (category !== 'weapons' && category !== 'defenses')
throw new HomeworldCustodyError('Choose either the weapon count or the defense count.');
const candidate = tupileIntelligenceTargets(context, custody, owner, usedFactions, occupation)
.find((p) => p.player === target);
if (!candidate || candidate.blocked)
throw new HomeworldCustodyError(candidate?.blocked ?? 'Choose a seated opposing faction for Tupile intelligence.');
return { owner, target: candidate.player, faction: candidate.faction, category, contact: [...candidate.contact] };
}