Files
OpenFrontIO/src/core/game/Game.ts
T
Aotumuri 8d6a0bce2c Add Japan as map (#384)
## Description:
There are fewer bots on the mainland, so taking control there can make
you more powerful.
To balance that, no rivers have been placed there, meaning you won’t
gain trade advantages from them.
The battlefield is split into two fronts, allowing for exciting
amphibious assaults with nuclear warfare.

I'm not sure about the white line on the map, but I don't think it's a
problem.

<img width="382" alt="スクリーンショット 2025-03-31 21 21 26"
src="https://github.com/user-attachments/assets/e1428ad8-febc-4eee-9c73-f27dd6fc207a"
/>
<img width="541" alt="スクリーンショット 2025-03-31 21 22 01"
src="https://github.com/user-attachments/assets/e2e63664-dde6-4d0b-a9d3-4f759d8924c6"
/>
<img width="441" alt="スクリーンショット 2025-03-31 21 22 15"
src="https://github.com/user-attachments/assets/eb386896-ed02-463b-ba2e-1b991dba8acb"
/>

![スクリーンショット 2025-03-31 21 23
20](https://github.com/user-attachments/assets/aa7685a6-74d7-4d89-9595-fe1731b43162)


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

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

<DISCORD USERNAME>
aotumuri
2025-03-31 09:55:04 -07:00

520 lines
12 KiB
TypeScript

import { Config } from "../configuration/Config";
import { GameEvent } from "../EventBus";
import { PlayerView } from "./GameView";
import { ClientID, GameConfig, GameID, AllPlayersStats } from "../Schemas";
import { GameMap, GameMapImpl, TileRef } from "./GameMap";
import {
GameUpdate,
GameUpdateType,
PlayerUpdate,
UnitUpdate,
} from "./GameUpdates";
import { Stats } from "./Stats";
export type PlayerID = string;
export type Tick = number;
export type Gold = 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 enum TeamName {
Red = "Red",
Blue = "Blue",
Bot = "Bot",
}
export enum GameMapType {
World = "World",
Europe = "Europe",
Mena = "Mena",
NorthAmerica = "North America",
SouthAmerica = "South America",
Oceania = "Oceania",
BlackSea = "Black Sea",
Africa = "Africa",
Pangaea = "Pangaea",
Asia = "Asia",
Mars = "Mars",
Britannia = "Britannia",
GatewayToTheAtlantic = "Gateway to the Atlantic",
Australia = "Australia",
Iceland = "Iceland",
Japan = "Japan",
}
export enum GameType {
Singleplayer = "Singleplayer",
Public = "Public",
Private = "Private",
}
export enum GameMode {
FFA = "Free For All",
Team = "Team",
}
export interface Team {
name: TeamName;
}
export interface UnitInfo {
cost: (player: Player | PlayerView) => Gold;
// Determines if its owner changes when its tile is conquered.
territoryBound: boolean;
maxHealth?: number;
damage?: number;
constructionDuration?: number;
}
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",
Construction = "Construction",
}
export const nukeTypes = [
UnitType.AtomBomb,
UnitType.HydrogenBomb,
UnitType.MIRVWarhead,
UnitType.MIRV,
] as UnitType[];
export type NukeType = (typeof nukeTypes)[number];
export enum Relation {
Hostile = 0,
Distrustful = 1,
Neutral = 2,
Friendly = 3,
}
export class Nation {
constructor(
public readonly flag: string,
public readonly name: string,
public readonly cell: Cell,
public readonly strength: number,
) {}
}
export class Cell {
public index: number;
private strRepr: string;
constructor(
public readonly x,
public readonly y,
) {
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",
FakeHuman = "FAKEHUMAN",
}
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;
}
export interface AllianceRequest {
accept(): void;
reject(): void;
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
}
export interface Alliance {
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
other(player: Player): Player;
}
export interface MutableAlliance extends Alliance {
expire(): void;
other(player: Player): Player;
}
export class PlayerInfo {
public readonly clan: string | null;
constructor(
public readonly flag: string,
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 nation?: Nation | null,
) {
// Compute clan from name
if (!name.startsWith("[") || !name.includes("]")) {
this.clan = null;
} else {
const clanMatch = name.match(/^\[([A-Z]{2,5})\]/);
this.clan = clanMatch ? clanMatch[1] : null;
}
}
}
// Some units have info specific to them
export interface UnitSpecificInfos {
dstPort?: Unit; // Only for trade ships
detonationDst?: TileRef; // Only for nukes
warshipTarget?: Unit;
cooldownDuration?: number;
}
export interface Unit {
id(): number;
// Properties
type(): UnitType;
troops(): number;
owner(): Player;
info(): UnitInfo;
// Location
tile(): TileRef;
lastTile(): TileRef;
move(tile: TileRef): void;
// State
isActive(): boolean;
hasHealth(): boolean;
health(): number;
modifyHealth(delta: number): void;
setWarshipTarget(target: Unit): void; // warship only
warshipTarget(): Unit;
setCooldown(triggerCooldown: boolean): void;
ticksLeftInCooldown(cooldownDuration: number): Tick;
isCooldown(): boolean;
setDstPort(dstPort: Unit): void;
dstPort(): Unit; // Only for trade ships
detonationDst(): TileRef; // Only for nukes
setMoveTarget(cell: TileRef): void;
moveTarget(): TileRef | null;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
// Mutations
setTroops(troops: number): void;
delete(displayerMessage?: boolean): void;
// Only for Construction type
constructionType(): UnitType | null;
setConstructionType(type: UnitType): void;
// Updates
toUpdate(): UnitUpdate;
}
export interface TerraNullius {
isPlayer(): false;
id(): PlayerID; // always zero, maybe make it TerraNulliusID?
clientID(): ClientID;
smallID(): number;
}
export interface Player {
// Basic Info
smallID(): number;
info(): PlayerInfo;
name(): string;
displayName(): string;
clientID(): ClientID;
id(): PlayerID;
type(): PlayerType;
isPlayer(): this is Player;
toString(): string;
// State & Properties
isAlive(): boolean;
isTraitor(): boolean;
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
lastTileChange(): Tick;
// Territory
tiles(): ReadonlySet<TileRef>;
borderTiles(): ReadonlySet<TileRef>;
numTilesOwned(): number;
conquer(tile: TileRef): void;
relinquish(tile: TileRef): void;
// Resources & Population
gold(): Gold;
population(): number;
workers(): number;
troops(): number;
targetTroopRatio(): number;
addGold(toAdd: Gold): void;
removeGold(toRemove: Gold): void;
addWorkers(toAdd: number): void;
removeWorkers(toRemove: number): void;
setTargetTroopRatio(target: number): void;
setTroops(troops: number): void;
addTroops(troops: number): void;
removeTroops(troops: number): number;
// Units
units(...types: UnitType[]): Unit[];
unitsIncludingConstruction(type: UnitType): Unit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit(
type: UnitType,
troops: number,
tile: TileRef,
unitSpecificInfos?: UnitSpecificInfos,
): Unit;
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): boolean;
team(): Team | null;
clan(): string | null;
incomingAllianceRequests(): AllianceRequest[];
outgoingAllianceRequests(): AllianceRequest[];
alliances(): MutableAlliance[];
allies(): Player[];
isAlliedWith(other: Player): boolean;
allianceWith(other: Player): MutableAlliance | null;
canSendAllianceRequest(other: Player): boolean;
breakAlliance(alliance: Alliance): void;
createAllianceRequest(recipient: Player): AllianceRequest;
// 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
canDonate(recipient: Player): boolean;
donate(recipient: Player, troops: number): void;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
tradingPartners(): Player[];
addEmbargo(other: PlayerID): void;
stopEmbargo(other: PlayerID): void;
canTrade(other: Player): boolean;
// Attacking.
canAttack(tile: TileRef): boolean;
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef,
): Attack;
outgoingAttacks(): Attack[];
incomingAttacks(): Attack[];
orderRetreat(attackID: string): void;
executeRetreat(attackID: string): void;
// Misc
toUpdate(): PlayerUpdate;
playerProfile(): PlayerProfile;
canBoat(tile: TileRef): boolean;
tradingPorts(port: Unit): Unit[];
}
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;
// 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, manpower: number): Player;
terraNullius(): TerraNullius;
owner(ref: TileRef): Player | TerraNullius;
teams(): Team[];
// Game State
ticks(): Tick;
inSpawnPhase(): boolean;
executeNextTick(): GameUpdates;
setWinner(winner: Player | TeamName, allPlayersStats: AllPlayersStats): void;
config(): Config;
// Units
units(...types: UnitType[]): Unit[];
unitInfo(type: UnitType): UnitInfo;
nearbyUnits(
tile: TileRef,
searchRange: number,
types: UnitType | UnitType[],
): Array<{ unit: Unit; distSquared: number }>;
addExecution(...exec: Execution[]): void;
displayMessage(
message: string,
type: MessageType,
playerID: PlayerID | null,
): void;
// Nations
nations(): Nation[];
numTilesWithFallout(): number;
// Optional as it's not initialized before the end of spawn phase
stats(): Stats;
}
export interface PlayerActions {
canBoat: boolean;
canAttack: boolean;
buildableUnits: BuildableUnit[];
canSendEmojiAllPlayers: boolean;
interaction?: PlayerInteraction;
}
export interface BuildableUnit {
canBuild: boolean;
type: UnitType;
cost: number;
}
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;
canDonate: boolean;
canEmbargo: boolean;
}
export interface EmojiMessage {
message: string;
senderID: number;
recipientID: number | typeof AllPlayers;
createdAt: Tick;
}
export enum MessageType {
SUCCESS,
INFO,
WARN,
ERROR,
}
export interface NameViewData {
x: number;
y: number;
size: number;
}