mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 12:52:03 +00:00
0e3ced3bfa
## Playtest https://pf-pt-2.openfront.dev/ ## Pathfinding Refactor pt. 2 <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e" /> This is a follow-up to a previous PR introducing pathfinding changes. This time, it introduces a complete refactor of `pathfinding` directory and breakdown into composable pieces. ### Unified PathFinder interface `PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify **all** pathfinding across the application. First one exposes complete path, while stepping variant allows the callee to iterate over the path by calling `.next`. All pathfinders share this one common interface, which makes them easy to use in any scenario - `PathFinding.Water(game).search(from, to)`. `SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to iterate over the path. It handles caching, storing current index and invalidation. This allows the units to not care about the inner workings of the pathfinder and just call `pf.next(current, target)` and receive instructions on what to do next. ### Common entry point All pathfinders are now exposed from common `PathFinding` entrypoint: - `PathFinding.Water` - `PathFinding.Rail` - `PathFinding.Stations` - `PathFinding.Rail` Additional entry point is introduced for pathfinders which need to work both in the worker, but also on the frontend, which lacks `Game` interface. Currently only `UniversalPathFinding.Parabola` is available. ### Spatial Query New module has been introduced close to `pathfinding` - `SpatialQuery`. It aims to resolve any questions game may have about finding tiles meeting criteria. Currently `SpatialQuery.closestShore(player, target)` and `SpatialQuery.closestShoreByWater(player, target)` are available - they help answering questions about naval invasion: "What is the best landing location from user's click?" and "Which our tile should be used to launch the transport ship?". Under the hood they use very similar mechanics to pathfinding, so it felt right to put them close by. ### Modular architecture Pathfinders now support transformers: `MiniMapTransformer`, `ShoreCoercingTransformer`, `ComponentCheckTransformer`, `SmoothingTransformer`. Transformers functions like a middleware in the pathfinding chain. They wrap around the pathfinder and provide additional functionality. This allows the pathfinder to focus on actually finding the path instead of doing unrelated things. Example chain for simple (A*) water pathfinding: ```ts static WaterSimple(game: Game): SteppingPathFinder<TileRef> { const miniMap = game.miniMap(); const pf = new AStarWater(miniMap); return PathFinderBuilder.create(pf) .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap)) .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap)) .buildWithStepper(tileStepperConfig(game)); } ``` The Pathfinder - here `AStarWater` - does not care about the conversion between minimap and main map tiles. It also does not care if the source or destination is a land tile. The transformers take care of that. The pathfinder gets a set of valid coordinates and produces the path - that's it. Modular approach makes working on a particular set of utilities much easier - for example map upscaling is handled consistently across all pathfinders. Additionally, the pathfinders are not tied to the particular map resolution used. Pass them a different map and they will work the same. ### Algorithms Algorithms used are neatly organized inside `src/core/pathfinding/algorithms`. They are prefixed with the algorithm name and suffixed with the use case. File without suffix exposes generic version ready to traverse any graph with adapters. Specialized versions either use an adapter or inline logic when performance is critical - using adapters leads to 20-30% performance loss. The directory includes `A*` and `BFS` but also other useful utils, such as `AbstractGraph` used to generate... an abstract graph on top of the tile map and `ConnectedComponents` helping to identify whether two tiles are connected by a path without actually computing the path. ### Playground The playground have been updated with new algorithms, including tweaked very greedy `A*`. <img width="2175" height="1424" alt="image" src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c" /> ### Tests Yeah, there are some, a little too many if I say so myself. But there are no useless tests. I had to ensure refactored code works somehow reliably. This PR comes with trust me bro guarantee, but I would appreciate someone confirming **naval invasions, nukes (esp. MIRV) and warships**. ### Discord `moleole` GL & HF
934 lines
24 KiB
TypeScript
934 lines
24 KiB
TypeScript
import { Config } from "../configuration/Config";
|
|
import { AbstractGraph } from "../pathfinding/algorithms/AbstractGraph";
|
|
import { PathFinder } from "../pathfinding/types";
|
|
import { AllPlayersStats, ClientID } from "../Schemas";
|
|
import { getClanTag } from "../Util";
|
|
import { GameMap, TileRef } from "./GameMap";
|
|
import {
|
|
GameUpdate,
|
|
GameUpdateType,
|
|
PlayerUpdate,
|
|
UnitUpdate,
|
|
} from "./GameUpdates";
|
|
import { RailNetwork } from "./RailNetwork";
|
|
import { Stats } from "./Stats";
|
|
import { UnitPredicate } from "./UnitGrid";
|
|
|
|
function isEnumValue<T extends Record<string, string | number>>(
|
|
enumObj: T,
|
|
value: unknown,
|
|
): value is T[keyof T] {
|
|
return Object.values(enumObj).includes(value as T[keyof T]);
|
|
}
|
|
|
|
export type PlayerID = string;
|
|
export type Tick = number;
|
|
export type Gold = bigint;
|
|
|
|
export const AllPlayers = "AllPlayers" as const;
|
|
|
|
// export type GameUpdates = Record<GameUpdateType, GameUpdate[]>;
|
|
// Create a type that maps GameUpdateType to its corresponding update type
|
|
type UpdateTypeMap<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
|
|
|
|
// Then use it to create the record type
|
|
export type GameUpdates = {
|
|
[K in GameUpdateType]: UpdateTypeMap<K>[];
|
|
};
|
|
|
|
export interface MapPos {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export enum Difficulty {
|
|
Easy = "Easy",
|
|
Medium = "Medium",
|
|
Hard = "Hard",
|
|
Impossible = "Impossible",
|
|
}
|
|
export const isDifficulty = (value: unknown): value is Difficulty =>
|
|
isEnumValue(Difficulty, value);
|
|
|
|
export type Team = string;
|
|
|
|
export const Duos = "Duos" as const;
|
|
export const Trios = "Trios" as const;
|
|
export const Quads = "Quads" as const;
|
|
export const HumansVsNations = "Humans Vs Nations" as const;
|
|
|
|
export const ColoredTeams: Record<string, Team> = {
|
|
Red: "Red",
|
|
Blue: "Blue",
|
|
Teal: "Teal",
|
|
Purple: "Purple",
|
|
Yellow: "Yellow",
|
|
Orange: "Orange",
|
|
Green: "Green",
|
|
Bot: "Bot",
|
|
Humans: "Humans",
|
|
Nations: "Nations",
|
|
} as const;
|
|
|
|
export enum GameMapType {
|
|
World = "World",
|
|
GiantWorldMap = "Giant World Map",
|
|
Europe = "Europe",
|
|
EuropeClassic = "Europe Classic",
|
|
Mena = "Mena",
|
|
NorthAmerica = "North America",
|
|
SouthAmerica = "South America",
|
|
Oceania = "Oceania",
|
|
BlackSea = "Black Sea",
|
|
Africa = "Africa",
|
|
Pangaea = "Pangaea",
|
|
Asia = "Asia",
|
|
Mars = "Mars",
|
|
BritanniaClassic = "Britannia Classic",
|
|
Britannia = "Britannia",
|
|
GatewayToTheAtlantic = "Gateway to the Atlantic",
|
|
Australia = "Australia",
|
|
Iceland = "Iceland",
|
|
EastAsia = "East Asia",
|
|
BetweenTwoSeas = "Between Two Seas",
|
|
FaroeIslands = "Faroe Islands",
|
|
DeglaciatedAntarctica = "Deglaciated Antarctica",
|
|
FalklandIslands = "Falkland Islands",
|
|
Baikal = "Baikal",
|
|
Halkidiki = "Halkidiki",
|
|
StraitOfGibraltar = "Strait of Gibraltar",
|
|
Italia = "Italia",
|
|
Japan = "Japan",
|
|
Pluto = "Pluto",
|
|
Montreal = "Montreal",
|
|
NewYorkCity = "New York City",
|
|
Achiran = "Achiran",
|
|
BaikalNukeWars = "Baikal (Nuke Wars)",
|
|
FourIslands = "Four Islands",
|
|
Svalmel = "Svalmel",
|
|
GulfOfStLawrence = "Gulf of St. Lawrence",
|
|
Lisbon = "Lisbon",
|
|
Manicouagan = "Manicouagan",
|
|
Lemnos = "Lemnos",
|
|
Sierpinski = "Sierpinski",
|
|
TwoLakes = "Two Lakes",
|
|
StraitOfHormuz = "Strait of Hormuz",
|
|
Surrounded = "Surrounded",
|
|
Didier = "Didier",
|
|
DidierFrance = "Didier France",
|
|
AmazonRiver = "Amazon River",
|
|
}
|
|
|
|
export type GameMapName = keyof typeof GameMapType;
|
|
|
|
/** Maps that have unusual thumbnail dimensions requiring object-fit: cover */
|
|
export function hasUnusualThumbnailSize(map: GameMapType): boolean {
|
|
return map === GameMapType.AmazonRiver;
|
|
}
|
|
|
|
export const mapCategories: Record<string, GameMapType[]> = {
|
|
continental: [
|
|
GameMapType.World,
|
|
GameMapType.GiantWorldMap,
|
|
GameMapType.NorthAmerica,
|
|
GameMapType.SouthAmerica,
|
|
GameMapType.Europe,
|
|
GameMapType.EuropeClassic,
|
|
GameMapType.Asia,
|
|
GameMapType.Africa,
|
|
GameMapType.Oceania,
|
|
],
|
|
regional: [
|
|
GameMapType.BritanniaClassic,
|
|
GameMapType.Britannia,
|
|
GameMapType.BlackSea,
|
|
GameMapType.GatewayToTheAtlantic,
|
|
GameMapType.BetweenTwoSeas,
|
|
GameMapType.Iceland,
|
|
GameMapType.EastAsia,
|
|
GameMapType.Mena,
|
|
GameMapType.Australia,
|
|
GameMapType.FaroeIslands,
|
|
GameMapType.FalklandIslands,
|
|
GameMapType.Baikal,
|
|
GameMapType.Halkidiki,
|
|
GameMapType.StraitOfGibraltar,
|
|
GameMapType.Italia,
|
|
GameMapType.Japan,
|
|
GameMapType.Montreal,
|
|
GameMapType.GulfOfStLawrence,
|
|
GameMapType.Lisbon,
|
|
GameMapType.NewYorkCity,
|
|
GameMapType.Manicouagan,
|
|
GameMapType.Lemnos,
|
|
GameMapType.TwoLakes,
|
|
GameMapType.StraitOfHormuz,
|
|
GameMapType.AmazonRiver,
|
|
],
|
|
fantasy: [
|
|
GameMapType.Pangaea,
|
|
GameMapType.Pluto,
|
|
GameMapType.Mars,
|
|
GameMapType.DeglaciatedAntarctica,
|
|
GameMapType.Achiran,
|
|
GameMapType.BaikalNukeWars,
|
|
GameMapType.FourIslands,
|
|
GameMapType.Svalmel,
|
|
GameMapType.Surrounded,
|
|
],
|
|
arcade: [
|
|
GameMapType.Didier,
|
|
GameMapType.DidierFrance,
|
|
GameMapType.Sierpinski,
|
|
],
|
|
};
|
|
|
|
export enum GameType {
|
|
Singleplayer = "Singleplayer",
|
|
Public = "Public",
|
|
Private = "Private",
|
|
}
|
|
export const isGameType = (value: unknown): value is GameType =>
|
|
isEnumValue(GameType, value);
|
|
|
|
export enum GameMode {
|
|
FFA = "Free For All",
|
|
Team = "Team",
|
|
}
|
|
export const isGameMode = (value: unknown): value is GameMode =>
|
|
isEnumValue(GameMode, value);
|
|
|
|
export enum GameMapSize {
|
|
Compact = "Compact",
|
|
Normal = "Normal",
|
|
}
|
|
|
|
export interface PublicGameModifiers {
|
|
isCompact: boolean;
|
|
isRandomSpawn: boolean;
|
|
}
|
|
|
|
export interface UnitInfo {
|
|
cost: (game: Game, player: Player) => Gold;
|
|
// Determines if its owner changes when its tile is conquered.
|
|
territoryBound: boolean;
|
|
maxHealth?: number;
|
|
damage?: number;
|
|
constructionDuration?: number;
|
|
upgradable?: boolean;
|
|
canBuildTrainStation?: boolean;
|
|
experimental?: boolean;
|
|
}
|
|
|
|
export enum UnitType {
|
|
TransportShip = "Transport",
|
|
Warship = "Warship",
|
|
Shell = "Shell",
|
|
SAMMissile = "SAMMissile",
|
|
Port = "Port",
|
|
AtomBomb = "Atom Bomb",
|
|
HydrogenBomb = "Hydrogen Bomb",
|
|
TradeShip = "Trade Ship",
|
|
MissileSilo = "Missile Silo",
|
|
DefensePost = "Defense Post",
|
|
SAMLauncher = "SAM Launcher",
|
|
City = "City",
|
|
MIRV = "MIRV",
|
|
MIRVWarhead = "MIRV Warhead",
|
|
Train = "Train",
|
|
Factory = "Factory",
|
|
}
|
|
|
|
export enum TrainType {
|
|
Engine = "Engine",
|
|
TailEngine = "TailEngine",
|
|
Carriage = "Carriage",
|
|
}
|
|
|
|
const _structureTypes: ReadonlySet<UnitType> = new Set([
|
|
UnitType.City,
|
|
UnitType.DefensePost,
|
|
UnitType.SAMLauncher,
|
|
UnitType.MissileSilo,
|
|
UnitType.Port,
|
|
UnitType.Factory,
|
|
]);
|
|
|
|
export function isStructureType(type: UnitType): boolean {
|
|
return _structureTypes.has(type);
|
|
}
|
|
|
|
export interface OwnerComp {
|
|
owner: Player;
|
|
}
|
|
|
|
export type TrajectoryTile = {
|
|
tile: TileRef;
|
|
targetable: boolean;
|
|
};
|
|
export interface UnitParamsMap {
|
|
[UnitType.TransportShip]: {
|
|
troops?: number;
|
|
targetTile?: TileRef;
|
|
};
|
|
|
|
[UnitType.Warship]: {
|
|
patrolTile: TileRef;
|
|
};
|
|
|
|
[UnitType.Shell]: Record<string, never>;
|
|
|
|
[UnitType.SAMMissile]: Record<string, never>;
|
|
|
|
[UnitType.Port]: Record<string, never>;
|
|
|
|
[UnitType.AtomBomb]: {
|
|
targetTile?: number;
|
|
trajectory: TrajectoryTile[];
|
|
};
|
|
|
|
[UnitType.HydrogenBomb]: {
|
|
targetTile?: number;
|
|
trajectory: TrajectoryTile[];
|
|
};
|
|
|
|
[UnitType.TradeShip]: {
|
|
targetUnit: Unit;
|
|
lastSetSafeFromPirates?: number;
|
|
};
|
|
|
|
[UnitType.Train]: {
|
|
trainType: TrainType;
|
|
targetUnit?: Unit;
|
|
loaded?: boolean;
|
|
};
|
|
|
|
[UnitType.Factory]: Record<string, never>;
|
|
|
|
[UnitType.MissileSilo]: Record<string, never>;
|
|
|
|
[UnitType.DefensePost]: Record<string, never>;
|
|
|
|
[UnitType.SAMLauncher]: Record<string, never>;
|
|
|
|
[UnitType.City]: Record<string, never>;
|
|
|
|
[UnitType.MIRV]: {
|
|
targetTile?: number;
|
|
};
|
|
|
|
[UnitType.MIRVWarhead]: {
|
|
targetTile?: number;
|
|
};
|
|
}
|
|
|
|
// Type helper to get params type for a specific unit type
|
|
export type UnitParams<T extends UnitType> = UnitParamsMap[T];
|
|
|
|
export type AllUnitParams = UnitParamsMap[keyof UnitParamsMap];
|
|
|
|
export const nukeTypes = [
|
|
UnitType.AtomBomb,
|
|
UnitType.HydrogenBomb,
|
|
UnitType.MIRVWarhead,
|
|
UnitType.MIRV,
|
|
] as UnitType[];
|
|
|
|
export enum Relation {
|
|
Hostile = 0,
|
|
Distrustful = 1,
|
|
Neutral = 2,
|
|
Friendly = 3,
|
|
}
|
|
|
|
export class Nation {
|
|
constructor(
|
|
public readonly spawnCell: Cell | undefined,
|
|
public readonly playerInfo: PlayerInfo,
|
|
) {}
|
|
}
|
|
|
|
export class Cell {
|
|
public index: number;
|
|
|
|
private strRepr: string;
|
|
|
|
constructor(
|
|
public readonly x: number,
|
|
public readonly y: number,
|
|
) {
|
|
this.strRepr = `Cell[${this.x},${this.y}]`;
|
|
}
|
|
|
|
pos(): MapPos {
|
|
return {
|
|
x: this.x,
|
|
y: this.y,
|
|
};
|
|
}
|
|
|
|
toString(): string {
|
|
return this.strRepr;
|
|
}
|
|
}
|
|
|
|
export enum TerrainType {
|
|
Plains,
|
|
Highland,
|
|
Mountain,
|
|
Lake,
|
|
Ocean,
|
|
}
|
|
|
|
export enum PlayerType {
|
|
Bot = "BOT",
|
|
Human = "HUMAN",
|
|
Nation = "NATION",
|
|
}
|
|
|
|
export interface Execution {
|
|
isActive(): boolean;
|
|
activeDuringSpawnPhase(): boolean;
|
|
init(mg: Game, ticks: number): void;
|
|
tick(ticks: number): void;
|
|
}
|
|
|
|
export interface Attack {
|
|
id(): string;
|
|
retreating(): boolean;
|
|
retreated(): boolean;
|
|
orderRetreat(): void;
|
|
executeRetreat(): void;
|
|
target(): Player | TerraNullius;
|
|
attacker(): Player;
|
|
troops(): number;
|
|
setTroops(troops: number): void;
|
|
isActive(): boolean;
|
|
delete(): void;
|
|
// The tile the attack originated from, mostly used for boat attacks.
|
|
sourceTile(): TileRef | null;
|
|
addBorderTile(tile: TileRef): void;
|
|
removeBorderTile(tile: TileRef): void;
|
|
clearBorder(): void;
|
|
borderSize(): number;
|
|
averagePosition(): Cell | null;
|
|
}
|
|
|
|
export interface AllianceRequest {
|
|
accept(): void;
|
|
reject(): void;
|
|
requestor(): Player;
|
|
recipient(): Player;
|
|
createdAt(): Tick;
|
|
status(): "pending" | "accepted" | "rejected";
|
|
}
|
|
|
|
export interface Alliance {
|
|
requestor(): Player;
|
|
recipient(): Player;
|
|
createdAt(): Tick;
|
|
expiresAt(): Tick;
|
|
other(player: Player): Player;
|
|
}
|
|
|
|
export interface MutableAlliance extends Alliance {
|
|
expire(): void;
|
|
other(player: Player): Player;
|
|
bothAgreedToExtend(): boolean;
|
|
addExtensionRequest(player: Player): void;
|
|
id(): number;
|
|
extend(): void;
|
|
onlyOneAgreedToExtend(): boolean;
|
|
}
|
|
|
|
export class PlayerInfo {
|
|
public readonly clan: string | null;
|
|
|
|
constructor(
|
|
public readonly name: string,
|
|
public readonly playerType: PlayerType,
|
|
// null if bot.
|
|
public readonly clientID: ClientID | null,
|
|
// TODO: make player id the small id
|
|
public readonly id: PlayerID,
|
|
public readonly isLobbyCreator: boolean = false,
|
|
) {
|
|
this.clan = getClanTag(name);
|
|
}
|
|
}
|
|
|
|
export function isUnit(unit: unknown): unit is Unit {
|
|
return (
|
|
unit &&
|
|
typeof unit === "object" &&
|
|
"isUnit" in unit &&
|
|
typeof unit.isUnit === "function" &&
|
|
unit.isUnit()
|
|
);
|
|
}
|
|
|
|
export interface Unit {
|
|
isUnit(): this is Unit;
|
|
|
|
// Common properties.
|
|
id(): number;
|
|
type(): UnitType;
|
|
owner(): Player;
|
|
info(): UnitInfo;
|
|
isMarkedForDeletion(): boolean;
|
|
markForDeletion(): void;
|
|
isOverdueDeletion(): boolean;
|
|
delete(displayMessage?: boolean, destroyer?: Player): void;
|
|
tile(): TileRef;
|
|
lastTile(): TileRef;
|
|
move(tile: TileRef): void;
|
|
isActive(): boolean;
|
|
setOwner(owner: Player): void;
|
|
touch(): void;
|
|
hash(): number;
|
|
toUpdate(): UnitUpdate;
|
|
hasTrainStation(): boolean;
|
|
setTrainStation(trainStation: boolean): void;
|
|
wasDestroyedByEnemy(): boolean;
|
|
destroyer(): Player | undefined;
|
|
|
|
// Train
|
|
trainType(): TrainType | undefined;
|
|
isLoaded(): boolean | undefined;
|
|
setLoaded(loaded: boolean): void;
|
|
|
|
// Targeting
|
|
setTargetTile(cell: TileRef | undefined): void;
|
|
targetTile(): TileRef | undefined;
|
|
setTrajectoryIndex(i: number): void;
|
|
trajectoryIndex(): number;
|
|
trajectory(): TrajectoryTile[];
|
|
setTargetUnit(unit: Unit | undefined): void;
|
|
targetUnit(): Unit | undefined;
|
|
setTargetedBySAM(targeted: boolean): void;
|
|
targetedBySAM(): boolean;
|
|
setReachedTarget(): void;
|
|
reachedTarget(): boolean;
|
|
isTargetable(): boolean;
|
|
setTargetable(targetable: boolean): void;
|
|
|
|
// Health
|
|
hasHealth(): boolean;
|
|
retreating(): boolean;
|
|
orderBoatRetreat(): void;
|
|
health(): number;
|
|
modifyHealth(delta: number, attacker?: Player): void;
|
|
|
|
// Troops
|
|
setTroops(troops: number): void;
|
|
troops(): number;
|
|
|
|
// --- UNIT SPECIFIC ---
|
|
|
|
// SAMs & Missile Silos
|
|
launch(): void;
|
|
reloadMissile(): void;
|
|
isInCooldown(): boolean;
|
|
missileTimerQueue(): number[];
|
|
|
|
// Trade Ships
|
|
setSafeFromPirates(): void; // Only for trade ships
|
|
isSafeFromPirates(): boolean; // Only for trade ships
|
|
|
|
// Construction phase on structures
|
|
isUnderConstruction(): boolean;
|
|
setUnderConstruction(underConstruction: boolean): void;
|
|
|
|
// Upgradable Structures
|
|
level(): number;
|
|
increaseLevel(): void;
|
|
decreaseLevel(destroyer?: Player): void;
|
|
|
|
// Warships
|
|
setPatrolTile(tile: TileRef): void;
|
|
patrolTile(): TileRef | undefined;
|
|
}
|
|
|
|
export interface TerraNullius {
|
|
isPlayer(): false;
|
|
id(): null;
|
|
clientID(): ClientID;
|
|
smallID(): number;
|
|
}
|
|
|
|
export interface Embargo {
|
|
createdAt: Tick;
|
|
isTemporary: boolean;
|
|
target: Player;
|
|
}
|
|
|
|
export interface Player {
|
|
// Basic Info
|
|
smallID(): number;
|
|
info(): PlayerInfo;
|
|
name(): string;
|
|
displayName(): string;
|
|
clientID(): ClientID | null;
|
|
id(): PlayerID;
|
|
type(): PlayerType;
|
|
isPlayer(): this is Player;
|
|
toString(): string;
|
|
isLobbyCreator(): boolean;
|
|
|
|
// State & Properties
|
|
isAlive(): boolean;
|
|
isTraitor(): boolean;
|
|
markTraitor(): void;
|
|
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
|
|
lastTileChange(): Tick;
|
|
|
|
isDisconnected(): boolean;
|
|
markDisconnected(isDisconnected: boolean): void;
|
|
|
|
hasSpawned(): boolean;
|
|
setSpawnTile(spawnTile: TileRef): void;
|
|
spawnTile(): TileRef | undefined;
|
|
|
|
// Territory
|
|
tiles(): ReadonlySet<TileRef>;
|
|
borderTiles(): ReadonlySet<TileRef>;
|
|
numTilesOwned(): number;
|
|
conquer(tile: TileRef): void;
|
|
relinquish(tile: TileRef): void;
|
|
|
|
// Resources & Troops
|
|
gold(): Gold;
|
|
addGold(toAdd: Gold, tile?: TileRef): void;
|
|
removeGold(toRemove: Gold): Gold;
|
|
troops(): number;
|
|
setTroops(troops: number): void;
|
|
addTroops(troops: number): void;
|
|
removeTroops(troops: number): number;
|
|
|
|
// Units
|
|
units(...types: UnitType[]): Unit[];
|
|
unitCount(type: UnitType): number;
|
|
unitsConstructed(type: UnitType): number;
|
|
unitsOwned(type: UnitType): number;
|
|
buildableUnits(tile: TileRef | null): BuildableUnit[];
|
|
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
|
|
buildUnit<T extends UnitType>(
|
|
type: T,
|
|
spawnTile: TileRef,
|
|
params: UnitParams<T>,
|
|
): Unit;
|
|
|
|
// Returns the existing unit that can be upgraded,
|
|
// or false if it cannot be upgraded.
|
|
// New units of the same type can upgrade existing units.
|
|
// e.g. if a place a new city here, can it upgrade an existing city?
|
|
findUnitToUpgrade(type: UnitType, targetTile: TileRef): Unit | false;
|
|
canUpgradeUnit(unit: Unit): boolean;
|
|
upgradeUnit(unit: Unit): void;
|
|
captureUnit(unit: Unit): void;
|
|
|
|
// Relations & Diplomacy
|
|
neighbors(): (Player | TerraNullius)[];
|
|
sharesBorderWith(other: Player | TerraNullius): boolean;
|
|
relation(other: Player): Relation;
|
|
allRelationsSorted(): { player: Player; relation: Relation }[];
|
|
updateRelation(other: Player, delta: number): void;
|
|
decayRelations(): void;
|
|
isOnSameTeam(other: Player): boolean;
|
|
// Either allied or on same team.
|
|
isFriendly(other: Player, treatAFKFriendly?: boolean): boolean;
|
|
team(): Team | null;
|
|
clan(): string | null;
|
|
incomingAllianceRequests(): AllianceRequest[];
|
|
outgoingAllianceRequests(): AllianceRequest[];
|
|
alliances(): MutableAlliance[];
|
|
expiredAlliances(): Alliance[];
|
|
allies(): Player[];
|
|
isAlliedWith(other: Player): boolean;
|
|
allianceWith(other: Player): MutableAlliance | null;
|
|
canSendAllianceRequest(other: Player): boolean;
|
|
breakAlliance(alliance: Alliance): void;
|
|
createAllianceRequest(recipient: Player): AllianceRequest | null;
|
|
betrayals(): number;
|
|
|
|
// Targeting
|
|
canTarget(other: Player): boolean;
|
|
target(other: Player): void;
|
|
targets(): Player[];
|
|
transitiveTargets(): Player[];
|
|
|
|
// Communication
|
|
canSendEmoji(recipient: Player | typeof AllPlayers): boolean;
|
|
outgoingEmojis(): EmojiMessage[];
|
|
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
|
|
|
|
// Donation
|
|
canDonateGold(recipient: Player): boolean;
|
|
canDonateTroops(recipient: Player): boolean;
|
|
donateTroops(recipient: Player, troops: number): boolean;
|
|
donateGold(recipient: Player, gold: Gold): boolean;
|
|
canDeleteUnit(): boolean;
|
|
recordDeleteUnit(): void;
|
|
canEmbargoAll(): boolean;
|
|
recordEmbargoAll(): void;
|
|
|
|
// Embargo
|
|
hasEmbargoAgainst(other: Player): boolean;
|
|
tradingPartners(): Player[];
|
|
addEmbargo(other: Player, isTemporary: boolean): void;
|
|
getEmbargoes(): Embargo[];
|
|
stopEmbargo(other: Player): void;
|
|
endTemporaryEmbargo(other: Player): void;
|
|
canTrade(other: Player): boolean;
|
|
|
|
// Attacking.
|
|
canAttack(tile: TileRef): boolean;
|
|
canAttackPlayer(player: Player, treatAFKFriendly?: boolean): boolean;
|
|
isImmune(): boolean;
|
|
|
|
createAttack(
|
|
target: Player | TerraNullius,
|
|
troops: number,
|
|
sourceTile: TileRef | null,
|
|
border: Set<number>,
|
|
): Attack;
|
|
outgoingAttacks(): Attack[];
|
|
incomingAttacks(): Attack[];
|
|
orderRetreat(attackID: string): void;
|
|
executeRetreat(attackID: string): void;
|
|
|
|
// Misc
|
|
toUpdate(): PlayerUpdate;
|
|
playerProfile(): PlayerProfile;
|
|
// WARNING: this operation is expensive.
|
|
bestTransportShipSpawn(tile: TileRef): TileRef | false;
|
|
}
|
|
|
|
export interface Game extends GameMap {
|
|
// Map & Dimensions
|
|
isOnMap(cell: Cell): boolean;
|
|
width(): number;
|
|
height(): number;
|
|
map(): GameMap;
|
|
miniMap(): GameMap;
|
|
forEachTile(fn: (tile: TileRef) => void): void;
|
|
// Zero-allocation neighbor iteration (cardinal only) to avoid creating arrays
|
|
forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void;
|
|
// Zero-allocation neighbor iteration for performance-critical cluster calculation
|
|
// Alternative to neighborsWithDiag() that returns arrays
|
|
// Avoids creating intermediate arrays and uses a callback for better performance
|
|
forEachNeighborWithDiag(
|
|
tile: TileRef,
|
|
callback: (neighbor: TileRef) => void,
|
|
): void;
|
|
|
|
// Player Management
|
|
player(id: PlayerID): Player;
|
|
players(): Player[];
|
|
allPlayers(): Player[];
|
|
playerByClientID(id: ClientID): Player | null;
|
|
playerBySmallID(id: number): Player | TerraNullius;
|
|
hasPlayer(id: PlayerID): boolean;
|
|
addPlayer(playerInfo: PlayerInfo): Player;
|
|
terraNullius(): TerraNullius;
|
|
owner(ref: TileRef): Player | TerraNullius;
|
|
|
|
teams(): Team[];
|
|
|
|
// Alliances
|
|
alliances(): MutableAlliance[];
|
|
expireAlliance(alliance: Alliance): void;
|
|
|
|
// Immunity timer
|
|
isSpawnImmunityActive(): boolean;
|
|
|
|
// Game State
|
|
ticks(): Tick;
|
|
inSpawnPhase(): boolean;
|
|
executeNextTick(): GameUpdates;
|
|
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
|
config(): Config;
|
|
isPaused(): boolean;
|
|
setPaused(paused: boolean): void;
|
|
|
|
// Units
|
|
units(...types: UnitType[]): Unit[];
|
|
unitCount(type: UnitType): number;
|
|
unitInfo(type: UnitType): UnitInfo;
|
|
hasUnitNearby(
|
|
tile: TileRef,
|
|
searchRange: number,
|
|
type: UnitType,
|
|
playerId?: PlayerID,
|
|
includeUnderConstruction?: boolean,
|
|
): boolean;
|
|
nearbyUnits(
|
|
tile: TileRef,
|
|
searchRange: number,
|
|
types: UnitType | UnitType[],
|
|
predicate?: UnitPredicate,
|
|
includeUnderConstruction?: boolean,
|
|
): Array<{ unit: Unit; distSquared: number }>;
|
|
|
|
addExecution(...exec: Execution[]): void;
|
|
displayMessage(
|
|
message: string,
|
|
type: MessageType,
|
|
playerID: PlayerID | null,
|
|
goldAmount?: bigint,
|
|
params?: Record<string, string | number>,
|
|
): void;
|
|
displayIncomingUnit(
|
|
unitID: number,
|
|
message: string,
|
|
type: MessageType,
|
|
playerID: PlayerID | null,
|
|
): void;
|
|
|
|
displayChat(
|
|
message: string,
|
|
category: string,
|
|
target: PlayerID | undefined,
|
|
playerID: PlayerID | null,
|
|
isFrom: boolean,
|
|
recipient: string,
|
|
): void;
|
|
|
|
// Nations
|
|
nations(): Nation[];
|
|
|
|
numTilesWithFallout(): number;
|
|
stats(): Stats;
|
|
|
|
addUpdate(update: GameUpdate): void;
|
|
railNetwork(): RailNetwork;
|
|
conquerPlayer(conqueror: Player, conquered: Player): void;
|
|
miniWaterHPA(): PathFinder<number> | null;
|
|
miniWaterGraph(): AbstractGraph | null;
|
|
getWaterComponent(tile: TileRef): number | null;
|
|
hasWaterComponent(tile: TileRef, component: number): boolean;
|
|
}
|
|
|
|
export interface PlayerActions {
|
|
canAttack: boolean;
|
|
buildableUnits: BuildableUnit[];
|
|
canSendEmojiAllPlayers: boolean;
|
|
canEmbargoAll?: boolean;
|
|
interaction?: PlayerInteraction;
|
|
}
|
|
|
|
export interface BuildableUnit {
|
|
canBuild: TileRef | false;
|
|
// unit id of the existing unit that can be upgraded, or false if it cannot be upgraded.
|
|
canUpgrade: number | false;
|
|
type: UnitType;
|
|
cost: Gold;
|
|
}
|
|
|
|
export interface PlayerProfile {
|
|
relations: Record<number, Relation>;
|
|
alliances: number[];
|
|
}
|
|
|
|
export interface PlayerBorderTiles {
|
|
borderTiles: ReadonlySet<TileRef>;
|
|
}
|
|
|
|
export interface PlayerInteraction {
|
|
sharedBorder: boolean;
|
|
canSendEmoji: boolean;
|
|
canSendAllianceRequest: boolean;
|
|
canBreakAlliance: boolean;
|
|
canTarget: boolean;
|
|
canDonateGold: boolean;
|
|
canDonateTroops: boolean;
|
|
canEmbargo: boolean;
|
|
allianceExpiresAt?: Tick;
|
|
}
|
|
|
|
export interface EmojiMessage {
|
|
message: string;
|
|
senderID: number;
|
|
recipientID: number | typeof AllPlayers;
|
|
createdAt: Tick;
|
|
}
|
|
|
|
export enum MessageType {
|
|
ATTACK_FAILED,
|
|
ATTACK_CANCELLED,
|
|
ATTACK_REQUEST,
|
|
CONQUERED_PLAYER,
|
|
MIRV_INBOUND,
|
|
NUKE_INBOUND,
|
|
HYDROGEN_BOMB_INBOUND,
|
|
NAVAL_INVASION_INBOUND,
|
|
SAM_MISS,
|
|
SAM_HIT,
|
|
CAPTURED_ENEMY_UNIT,
|
|
UNIT_CAPTURED_BY_ENEMY,
|
|
UNIT_DESTROYED,
|
|
ALLIANCE_ACCEPTED,
|
|
ALLIANCE_REJECTED,
|
|
ALLIANCE_REQUEST,
|
|
ALLIANCE_BROKEN,
|
|
ALLIANCE_EXPIRED,
|
|
SENT_GOLD_TO_PLAYER,
|
|
RECEIVED_GOLD_FROM_PLAYER,
|
|
RECEIVED_GOLD_FROM_TRADE,
|
|
SENT_TROOPS_TO_PLAYER,
|
|
RECEIVED_TROOPS_FROM_PLAYER,
|
|
CHAT,
|
|
RENEW_ALLIANCE,
|
|
}
|
|
|
|
// Message categories used for filtering events in the EventsDisplay
|
|
export enum MessageCategory {
|
|
ATTACK = "ATTACK",
|
|
NUKE = "NUKE",
|
|
ALLIANCE = "ALLIANCE",
|
|
TRADE = "TRADE",
|
|
CHAT = "CHAT",
|
|
}
|
|
|
|
// Ensures that all message types are included in a category
|
|
export const MESSAGE_TYPE_CATEGORIES: Record<MessageType, MessageCategory> = {
|
|
[MessageType.ATTACK_FAILED]: MessageCategory.ATTACK,
|
|
[MessageType.ATTACK_CANCELLED]: MessageCategory.ATTACK,
|
|
[MessageType.ATTACK_REQUEST]: MessageCategory.ATTACK,
|
|
[MessageType.CONQUERED_PLAYER]: MessageCategory.ATTACK,
|
|
[MessageType.MIRV_INBOUND]: MessageCategory.NUKE,
|
|
[MessageType.NUKE_INBOUND]: MessageCategory.NUKE,
|
|
[MessageType.HYDROGEN_BOMB_INBOUND]: MessageCategory.NUKE,
|
|
[MessageType.NAVAL_INVASION_INBOUND]: MessageCategory.ATTACK,
|
|
[MessageType.SAM_MISS]: MessageCategory.ATTACK,
|
|
[MessageType.SAM_HIT]: MessageCategory.ATTACK,
|
|
[MessageType.CAPTURED_ENEMY_UNIT]: MessageCategory.ATTACK,
|
|
[MessageType.UNIT_CAPTURED_BY_ENEMY]: MessageCategory.ATTACK,
|
|
[MessageType.UNIT_DESTROYED]: MessageCategory.ATTACK,
|
|
[MessageType.ALLIANCE_ACCEPTED]: MessageCategory.ALLIANCE,
|
|
[MessageType.ALLIANCE_REJECTED]: MessageCategory.ALLIANCE,
|
|
[MessageType.ALLIANCE_REQUEST]: MessageCategory.ALLIANCE,
|
|
[MessageType.ALLIANCE_BROKEN]: MessageCategory.ALLIANCE,
|
|
[MessageType.ALLIANCE_EXPIRED]: MessageCategory.ALLIANCE,
|
|
[MessageType.RENEW_ALLIANCE]: MessageCategory.ALLIANCE,
|
|
[MessageType.SENT_GOLD_TO_PLAYER]: MessageCategory.TRADE,
|
|
[MessageType.RECEIVED_GOLD_FROM_PLAYER]: MessageCategory.TRADE,
|
|
[MessageType.RECEIVED_GOLD_FROM_TRADE]: MessageCategory.TRADE,
|
|
[MessageType.SENT_TROOPS_TO_PLAYER]: MessageCategory.TRADE,
|
|
[MessageType.RECEIVED_TROOPS_FROM_PLAYER]: MessageCategory.TRADE,
|
|
[MessageType.CHAT]: MessageCategory.CHAT,
|
|
} as const;
|
|
|
|
/**
|
|
* Get the category of a message type
|
|
*/
|
|
export function getMessageCategory(messageType: MessageType): MessageCategory {
|
|
return MESSAGE_TYPE_CATEGORIES[messageType];
|
|
}
|
|
|
|
export interface NameViewData {
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
}
|