76 lines
4.6 KiB
TypeScript
76 lines
4.6 KiB
TypeScript
import type { Action, GameView } from './engine';
|
|
import { location, splitLocation } from './board';
|
|
import { botGroundMoveAllowed } from './bot-mobility';
|
|
import { planetologistFlightSelection, type PlanetologistFlight } from './planetologist-flight';
|
|
import { planetologistLeader, planetologistMovementModeSupported,
|
|
planetologistGatherAdvisorEntry, type PlanetologistMoveMode } from './planetologist-movement';
|
|
import { validateCohortSelection } from './ornithopter';
|
|
import { nexusGuildMovementAvailable } from './nexus-guild-cunning-options';
|
|
|
|
type Source = { from: string; elite: number; marker: boolean };
|
|
|
|
/** Minimal legal participation using only current projected transport and stock.
|
|
* One counter per origin leaves distinct residual groups where available. */
|
|
export function botPlanetologistFlightActions(game: GameView,
|
|
targets: readonly { territory: string; sector: number }[],
|
|
transports: readonly PlanetologistFlight[]): Action[] {
|
|
const me = game.players.find(player => player.id === game.me);
|
|
if (!me || game.status !== 'playing' || game.phase !== 5 || game.active !== me.id ||
|
|
game.response || game.decision || game.phaseOpening || game.truthtrance ||
|
|
game.automaticContinuationPending || game.roomControl?.paused || game.roomControl?.closed ||
|
|
!planetologistLeader(game, me.id) || !planetologistMovementModeSupported(game) ||
|
|
(!game.ornithopter?.active && !nexusGuildMovementAvailable(game))) return [];
|
|
const info = game.richeseNoField?.owner === me.id ? game.richeseNoField : null;
|
|
const marker = info?.private?.deployed;
|
|
const result: Action[] = [];
|
|
for (const transport of transports) {
|
|
const flight = planetologistFlightSelection(game, transport);
|
|
if (flight.blocked) continue;
|
|
const sources: Source[] = Object.entries(me.forces).flatMap(([from, total]) => {
|
|
const elite = me.elites?.forces[from] ?? 0;
|
|
const availableElite = Math.min(elite, flight.cohort?.elites[from] ?? elite);
|
|
const availableNormal = Math.min(total - elite, flight.cohort
|
|
? (flight.cohort.forces[from] ?? 0) - (flight.cohort.elites[from] ?? 0) : total - elite);
|
|
return availableNormal > 0 ? [{ from, elite: 0, marker: false }]
|
|
: availableElite > 0 ? [{ from, elite: 1, marker: false }] : [];
|
|
}).slice(0, 12);
|
|
if (marker?.controller === me.id && info?.event && !game.homeworldMobility?.noFieldMovementBlocked) {
|
|
const from = location(marker.location.territory, marker.location.sector);
|
|
if (!flight.cohort || (flight.cohort.noField?.tokenId === marker.tokenId &&
|
|
flight.cohort.noField.event === info.event && flight.cohort.noField.from === from))
|
|
sources.push({ from, elite: 0, marker: true });
|
|
}
|
|
for (const mode of ['range', 'gather'] as PlanetologistMoveMode[]) {
|
|
const groups: Source[][] = mode === 'range' ? sources.map(source => [source])
|
|
: sources.flatMap((first, index) => sources.slice(index + 1)
|
|
.filter(second => splitLocation(first.from).territory !== splitLocation(second.from).territory)
|
|
.map(second => [first, second]));
|
|
let accepted = 0;
|
|
choices: for (const target of targets) for (const group of groups) {
|
|
const to = location(target.territory, target.sector);
|
|
if (!group.every(source => botGroundMoveAllowed(game, me, source.from, to,
|
|
source.elite, mode, flight.rangeOverride))) continue;
|
|
const entry = planetologistGatherAdvisorEntry(game, me,
|
|
group.map(source => splitLocation(source.from).territory), target.territory);
|
|
if (entry.blocked) continue;
|
|
const forces = Object.fromEntries(group.filter(source => !source.marker).map(source => [source.from, 1]));
|
|
const elites = Object.fromEntries(group.filter(source => !source.marker).map(source => [source.from, source.elite]));
|
|
const selectedMarker = group.some(source => source.marker) && marker && info?.event
|
|
? { tokenId: marker.tokenId, event: info.event,
|
|
from: location(marker.location.territory, marker.location.sector) } : undefined;
|
|
if (flight.cohort) {
|
|
try { validateCohortSelection(flight.cohort, me.forces, me.elites?.forces ?? {}, forces, elites, selectedMarker); }
|
|
catch { continue; }
|
|
}
|
|
result.push({ type: 'move', planetologist: mode, forces,
|
|
...(me.elites ? { eliteForces: elites } : {}),
|
|
...(selectedMarker ? { noField: selectedMarker.tokenId, event: selectedMarker.event } : {}),
|
|
...(entry.requiresFlip ? { fighters: true } : {}),
|
|
...flight.fields, territory: target.territory, sector: target.sector });
|
|
if (++accepted === 24) break choices;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|