Files
Dune/game/bot-mobility.ts
T

152 lines
4.4 KiB
TypeScript

import type { GameView } from './engine';
import { guildShipmentCost } from './shipment-price';
import { fighterCount, isAdvisor } from './advisors';
import { presenceAt } from './force-presence';
import { EcazOccupancyError } from './ecaz-occupy';
import {
planetologistLeader,
groundMovementRange,
type PlanetologistMoveMode,
} from './planetologist-movement';
import { territoryEntryBlock, strongholdPathBlocked } from './occupancy';
import { isAlliedShipmentVisit } from './allied-separation';
import {
distance,
gameDistance,
location,
MOBILE_STRONGHOLD,
splitLocation,
territory,
} from './board';
type Seat = GameView['players'][number];
/** These checks consume only the same personalized projection as the bot policy. */
function advisorArrival(g: GameView, p: Seat, to: string, source?: string) {
if (
!g.advanced ||
p.faction !== 'beneGesserit' ||
!g.players.some((other) => other.id !== p.id && presenceAt(other, to))
)
return false;
return presenceAt(p, to)
? isAdvisor(p, to)
: !!source && isAdvisor(p, source);
}
export function botEntryAllowed(
g: GameView,
p: Seat,
to: string,
sector: number,
kind: 'move' | 'ship' | 'guildShip',
source?: string,
) {
if (
to === MOBILE_STRONGHOLD &&
(!g.mobileStronghold?.location ||
(kind !== 'move' && p.faction !== 'ixians'))
)
return false;
if (
sector !== 0 &&
sector === g.storm &&
!(kind === 'ship' && g.advanced && p.faction === 'fremen')
)
return false;
const advisors = advisorArrival(
g,
p,
to,
kind === 'move' ? source : undefined,
);
if (
source &&
!advisors &&
p.advisors?.[source]?.lockedTurn === g.turn &&
g.players.some((other) => other.id !== p.id && presenceAt(other, to))
)
return false;
try {
return !territoryEntryBlock(g.players, p.id, to, advisors,
kind === 'ship' && isAlliedShipmentVisit(g, p, to));
} catch (error) {
if (error instanceof EcazOccupancyError) return false;
throw error;
}
}
export function botMovementRange(
g: GameView,
p: Seat,
elite: number,
planetologist?: PlanetologistMoveMode,
) {
return groundMovementRange({
faction: p.faction,
cityOrnithopters: !!(fighterCount(p, 'arrakeen') || fighterCount(p, 'carthag')),
selectedElites: elite,
nativeBlocked: p.faction === 'ixians' ? !!p.ixMovementBlocked : !!p.fremenMovementBlocked,
choamBonus: p.faction === 'choam' ? g.choamMovementBonus : 0,
}, planetologist && planetologistLeader(g, p.id) ? planetologist : undefined);
}
/** Geometry shared by projected controls and policy; range and entry remain separate. */
export function groundMoveRouteDistance(g: GameView, p: Seat, from: string, to: string): number {
const advisors = isAdvisor(p, splitLocation(from).territory);
return gameDistance(g, from, to, key => {
const loc = splitLocation(key);
return (loc.sector !== 0 && loc.sector === g.storm) ||
strongholdPathBlocked(g.players, p.id, loc.territory, advisors);
});
}
export function botGroundMoveAllowed(
g: GameView,
p: Seat,
from: string,
to: string,
elite: number,
planetologist?: PlanetologistMoveMode,
) {
const source = splitLocation(from),
target = splitLocation(to);
if (
from === to ||
(source.sector !== 0 && source.sector === g.storm) ||
!botEntryAllowed(
g,
p,
target.territory,
target.sector,
'move',
source.territory,
)
)
return false;
if (
source.territory !== target.territory &&
g.balisetRestrictions.some(
(b) => b.player === p.id && b.territory === target.territory,
) &&
g.players.some(
(other) =>
other.faction === 'choam' && presenceAt(other, target.territory),
)
)
return false;
return groundMoveRouteDistance(g, p, from, to) <= botMovementRange(g, p, elite, planetologist);
}
/** Fremen reinforcement radius is static territory geometry, not a ground route. */
export function fremenReserveEntry(to: string) {
return territory('the_great_flat').sectors.some((from) =>
territory(to).sectors.some(
(sector) =>
distance(location('the_great_flat', from), location(to, sector)) <= 2,
),
);
}
/** Guild cross-shipping is paid transport, including allied Fremen reserves. */
export function guildTransportCost(to: string, amount: number, halfRate = true) {
return guildShipmentCost(
to === 'reserves' ? 'reserves' : territory(to).type,
amount,
halfRate,
);
}