Files
OpenFrontIO/src/core/game/Game.ts
T
Evan 62e15d2794 Cut worker→main bandwidth ~3.3× by switching PlayerUpdate to deltas (#3967)
## Description:

Cut worker→main bandwidth ~3.3× by switching PlayerUpdate from a full
per-tick snapshot to a field-level diff. PlayerImpl.toUpdate() now
caches the last sent update and returns only changed fields, or null if
nothing changed. The client-side applyStateUpdate() merges instead of
overwriting.

Per-tick total dropped from ~297 KB to ~89 KB; the Player bucket alone
went from 258 KB/tick to 50 KB/tick. Diff/apply logic lives in a new
GameUpdateUtils.ts module with unit tests.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2026-05-18 17:07:40 -07:00

1102 lines
28 KiB
TypeScript

import { Config } from "../configuration/Config";
import { AbstractGraph } from "../pathfinding/algorithms/AbstractGraph";
import { PathFinder } from "../pathfinding/types";
import { AllPlayersStats, ClientID } from "../Schemas";
import { formatPlayerDisplayName } from "../Util";
import { GameMap, TileRef } from "./GameMap";
import {
GameUpdate,
GameUpdateType,
PlayerUpdate,
UnitUpdate,
} from "./GameUpdates";
import { MotionPlanRecord } from "./MotionPlans";
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 type WarshipState = {
state: "patrolling" | "retreating" | "docked";
patrolTile?: TileRef;
retreatPort?: TileRef;
isInCombat?: boolean;
lastCombatTick: number;
};
export type TransportShipState = {
isRetreating: boolean;
troops: number;
};
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 interface SpawnArea {
x: number;
y: number;
width: number;
height: number;
}
export type TeamGameSpawnAreas = Record<string, SpawnArea[]>;
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",
Tourney1 = "Tourney 2 Teams",
Tourney2 = "Tourney 3 Teams",
Tourney3 = "Tourney 4 Teams",
Tourney4 = "Tourney 8 Teams",
Passage = "Passage",
Sierpinski = "Sierpinski",
TheBox = "The Box",
TwoLakes = "Two Lakes",
StraitOfHormuz = "Strait of Hormuz",
Surrounded = "Surrounded",
Didier = "Didier",
DidierFrance = "Didier France",
AmazonRiver = "Amazon River",
BosphorusStraits = "Bosphorus Straits",
BeringStrait = "Bering Strait",
Yenisei = "Yenisei",
TradersDream = "Traders Dream",
Hawaii = "Hawaii",
Alps = "Alps",
NileDelta = "Nile Delta",
Arctic = "Arctic",
SanFrancisco = "San Francisco",
Aegean = "Aegean",
MilkyWay = "MilkyWay",
MareNostrum = "Mare Nostrum",
Dyslexdria = "Dyslexdria",
GreatLakes = "Great Lakes",
StraitOfMalacca = "Strait Of Malacca",
Luna = "Luna",
Conakry = "Conakry",
Caucasus = "Caucasus",
LosAngeles = "Los Angeles",
BeringSea = "Bering Sea",
Antarctica = "Antarctica",
ArchipelagoSea = "ArchipelagoSea",
BajaCalifornia = "Baja California",
MiddleEast = "Middle East",
TaiwanStrait = "Taiwan Strait",
DanishStraits = "Danish Straits",
NorthwestPassage = "Northwest Passage",
}
export type GameMapName = keyof typeof GameMapType;
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,
GameMapType.Antarctica,
],
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,
GameMapType.BosphorusStraits,
GameMapType.BeringStrait,
GameMapType.Yenisei,
GameMapType.Hawaii,
GameMapType.Alps,
GameMapType.NileDelta,
GameMapType.Arctic,
GameMapType.SanFrancisco,
GameMapType.Aegean,
GameMapType.MareNostrum,
GameMapType.GreatLakes,
GameMapType.StraitOfMalacca,
GameMapType.Conakry,
GameMapType.Caucasus,
GameMapType.LosAngeles,
GameMapType.BeringSea,
GameMapType.ArchipelagoSea,
GameMapType.BajaCalifornia,
GameMapType.MiddleEast,
GameMapType.TaiwanStrait,
GameMapType.DanishStraits,
GameMapType.NorthwestPassage,
],
fantasy: [
GameMapType.Pangaea,
GameMapType.Pluto,
GameMapType.Mars,
GameMapType.DeglaciatedAntarctica,
GameMapType.Achiran,
GameMapType.BaikalNukeWars,
GameMapType.FourIslands,
GameMapType.Svalmel,
GameMapType.Surrounded,
GameMapType.TradersDream,
GameMapType.Passage,
GameMapType.MilkyWay,
GameMapType.Dyslexdria,
GameMapType.Luna,
],
arcade: [
GameMapType.TheBox,
GameMapType.Didier,
GameMapType.DidierFrance,
GameMapType.Sierpinski,
],
tournament: [
GameMapType.Tourney1,
GameMapType.Tourney2,
GameMapType.Tourney3,
GameMapType.Tourney4,
],
};
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 enum RankedType {
OneVOne = "1v1",
}
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;
isCrowded?: boolean;
isHardNations?: boolean;
startingGold?: number;
goldMultiplier?: number;
isAlliancesDisabled?: boolean;
isPortsDisabled?: boolean;
isNukesDisabled?: boolean;
isSAMsDisabled?: boolean;
isPeaceTime?: boolean;
isWaterNukes?: boolean;
}
export interface UnitInfo {
cost: (game: Game, player: Player) => Gold;
maxHealth?: number;
damage?: number;
constructionDuration?: number;
upgradable?: boolean;
}
function unitTypeGroup<T extends readonly UnitType[]>(types: T) {
return {
types,
has(type: UnitType): type is T[number] {
return (types as readonly UnitType[]).includes(type);
},
};
}
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",
}
export const Nukes = unitTypeGroup([
UnitType.AtomBomb,
UnitType.HydrogenBomb,
UnitType.MIRVWarhead,
UnitType.MIRV,
] as const);
export const BuildableAttacks = unitTypeGroup([
UnitType.AtomBomb,
UnitType.HydrogenBomb,
UnitType.MIRV,
UnitType.Warship,
] as const);
export const Structures = unitTypeGroup([
UnitType.City,
UnitType.DefensePost,
UnitType.SAMLauncher,
UnitType.MissileSilo,
UnitType.Port,
UnitType.Factory,
] as const);
export const BuildMenus = unitTypeGroup([
...Structures.types,
...BuildableAttacks.types,
] as const);
export const PlayerBuildable = unitTypeGroup([
...BuildMenus.types,
UnitType.TransportShip,
] as const);
export type PlayerBuildableUnitType = (typeof PlayerBuildable.types)[number];
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 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;
clusteredPositions(): TileRef[];
}
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;
agreedToExtend(player: Player): boolean;
}
export class PlayerInfo {
public readonly displayName: string;
constructor(
public readonly name: string,
public readonly playerType: PlayerType,
// null if tribe.
public readonly clientID: ClientID | null,
// TODO: make player id the small id
public readonly id: PlayerID,
public readonly isLobbyCreator: boolean = false,
public readonly clanTag: string | null = null,
) {
this.displayName = formatPlayerDisplayName(this.name, this.clanTag);
}
}
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;
warshipState(): WarshipState;
updateWarshipState(update: Partial<WarshipState>): void;
transportShipState(): TransportShipState;
updateTransportShipState(update: Partial<TransportShipState>): 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;
}
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,
units?: readonly PlayerBuildableUnitType[],
): BuildableUnit[];
canBuild(
type: UnitType,
targetTile: TileRef,
validTiles?: TileRef[] | null,
): 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
nearby(): (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;
incomingAllianceRequests(): AllianceRequest[];
outgoingAllianceRequests(): AllianceRequest[];
alliances(): MutableAlliance[];
expiredAlliances(): Alliance[];
allies(): Player[];
isAlliedWith(other: Player): boolean;
allianceWith(other: Player): MutableAlliance | null;
allianceInfo(other: Player): AllianceInfo | null;
canSendAllianceRequest(other: Player): boolean;
breakAlliance(alliance: Alliance): void;
removeAllAlliances(): 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 | null;
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[];
teamSpawnArea(team: Team): SpawnArea | undefined;
// Alliances
alliances(): MutableAlliance[];
expireAlliance(alliance: Alliance): void;
// Immunity timer
isSpawnImmunityActive(): boolean;
isNationSpawnImmunityActive(): boolean;
elapsedGameSeconds(): number;
// Game State
ticks(): Tick;
inSpawnPhase(): boolean;
endSpawnPhase(): void;
executeNextTick(): GameUpdates;
drainPackedTileUpdates(): Uint32Array;
recordMotionPlan(record: MotionPlanRecord): void;
drainPackedMotionPlans(): Uint32Array | null;
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
getWinner(): Player | Team | null;
config(): Config;
isPaused(): boolean;
setPaused(paused: boolean): void;
// Units
unit(id: number): Unit | undefined;
units(...types: UnitType[]): Unit[];
unitCount(type: UnitType): number;
unitInfo(type: UnitType): UnitInfo;
hasUnitNearby(
tile: TileRef,
searchRange: number,
type: UnitType,
playerId?: PlayerID,
includeUnderConstruction?: boolean,
): boolean;
anyUnitNearby(
tile: TileRef,
searchRange: number,
types: readonly UnitType[],
predicate: (unit: Unit) => boolean,
playerId?: PlayerID,
includeUnderConstruction?: boolean,
): boolean;
nearbyUnits(
tile: TileRef,
searchRange: number,
types: UnitType | readonly 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;
/**
* Returns the set of water components that `player` shares with at least one
* valid trade partner (cached). Used by nation AI for port-placement
* heuristics. `null` means no usable water body for ports.
*/
sharedWaterComponents(player: Player): Set<number> | null;
/** Incremented each time the water navigation graph is rebuilt (e.g. after nuke terrain change). */
waterGraphVersion(): number;
/** Queue a land tile for conversion to water (batched every few ticks). Tile must be unowned. */
queueWaterConversion(tile: TileRef): void;
}
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: PlayerBuildableUnitType;
cost: Gold;
overlappingRailroads: number[];
ghostRailPaths: TileRef[][];
}
export interface PlayerProfile {
relations: Record<number, Relation>;
alliances: number[];
}
export interface PlayerBorderTiles {
borderTiles: ReadonlySet<TileRef>;
}
export interface AllianceInfo {
expiresAt: Tick;
inExtensionWindow: boolean;
myPlayerAgreedToExtend: boolean;
otherAgreedToExtend: boolean;
canExtend: boolean;
}
export interface PlayerInteraction {
sharedBorder: boolean;
canSendEmoji: boolean;
canSendAllianceRequest: boolean;
canBreakAlliance: boolean;
canTarget: boolean;
canDonateGold: boolean;
canDonateTroops: boolean;
canEmbargo: boolean;
allianceInfo?: AllianceInfo;
}
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;
}