Enable strictPropertyInitialization (#1909)

## Description:

Enable the tsconfig option `strictPropertyInitialization`.

Fixes #1907

## 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
This commit is contained in:
Scott Anderson
2025-08-23 19:21:40 -04:00
committed by GitHub
parent f5316cc378
commit 51519b0b9d
76 changed files with 599 additions and 367 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
return process.env.CF_CREDS_PATH ?? "";
}
private publicKey: JWK;
private publicKey: JWK | undefined;
abstract jwtAudience(): string;
jwtIssuer(): string {
const audience = this.jwtAudience();
+22 -9
View File
@@ -23,9 +23,9 @@ export class AttackExecution implements Execution {
private readonly random = new PseudoRandom(123);
private target: Player | TerraNullius;
private target: Player | TerraNullius | undefined;
private mg: Game;
private mg: Game | undefined;
private attack: Attack | null = null;
@@ -171,6 +171,9 @@ export class AttackExecution implements Execution {
}
private retreat(malusPercent = 0) {
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
if (this.attack === null) {
throw new Error("Attack not initialized");
}
@@ -189,22 +192,27 @@ export class AttackExecution implements Execution {
this.active = false;
// Not all retreats are canceled attacks
if (this.attack.retreated()) {
if (this.attack.retreated() && this.target && this.target.isPlayer()) {
// Record stats
this.mg.stats().attackCancel(this._owner, this.target, survivors);
}
}
tick(ticks: number) {
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
if (this.target === undefined) {
throw new Error("Attack not initialized");
}
if (this.attack === null) {
throw new Error("Attack not initialized");
}
let troopCount = this.attack.troops(); // cache troop count
const targetIsPlayer = this.target.isPlayer(); // cache target type
const targetPlayer = targetIsPlayer ? (this.target as Player) : null; // cache target player
const targetPlayer: Player | null = this.target.isPlayer() ? this.target : null; // cache target player
if (this.attack.retreated()) {
if (targetIsPlayer) {
if (targetPlayer !== null) {
this.retreat(malusForRetreat);
} else {
this.retreat();
@@ -222,8 +230,8 @@ export class AttackExecution implements Execution {
return;
}
const alliance = targetPlayer
? this._owner.allianceWith(targetPlayer)
const alliance = this.target && this.target.isPlayer()
? this._owner.allianceWith(this.target)
: null;
if (this.breakAlliance && alliance !== null) {
this.breakAlliance = false;
@@ -309,6 +317,9 @@ export class AttackExecution implements Execution {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
if (this.mg === undefined) {
throw new Error("Attack not initialized");
}
const tickNow = this.mg.ticks(); // cache tick
@@ -349,6 +360,8 @@ export class AttackExecution implements Execution {
}
private handleDeadDefender() {
if (!this.mg) return;
if (!this.target) return;
if (!(this.target.isPlayer() && this.target.numTilesOwned() < 100)) return;
this.mg.conquerPlayer(this._owner, this.target);
@@ -357,7 +370,7 @@ export class AttackExecution implements Execution {
for (const tile of this.target.tiles()) {
const borders = this.mg
.neighbors(tile)
.some((t) => this.mg.owner(t) === this._owner);
.some((t) => this.mg?.owner(t) === this._owner);
if (borders) {
this._owner.conquer(tile);
} else {
+4 -2
View File
@@ -6,7 +6,7 @@ import { simpleHash } from "../Util";
export class BotExecution implements Execution {
private active = true;
private readonly random: PseudoRandom;
private mg: Game;
private mg: Game | undefined;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
@@ -42,6 +42,7 @@ export class BotExecution implements Execution {
}
if (this.behavior === null) {
if (this.mg === undefined) throw new Error("Not initialized");
this.behavior = new BotBehavior(
this.random,
this.mg,
@@ -63,7 +64,7 @@ export class BotExecution implements Execution {
private maybeAttack() {
if (this.behavior === null) {
throw new Error("not initialized");
throw new Error("Not initialized");
}
const toAttack = this.behavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
@@ -75,6 +76,7 @@ export class BotExecution implements Execution {
}
if (this.neighborsTerraNullius) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
return;
+2 -1
View File
@@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap";
import { TrainStationExecution } from "./TrainStationExecution";
export class CityExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private city: Unit | null = null;
private active = true;
@@ -47,6 +47,7 @@ export class CityExecution implements Execution {
createStation(): void {
if (this.city !== null) {
if (this.mg === undefined) throw new Error("Not initialized");
const nearbyFactory = this.mg.hasUnitNearby(
this.city.tile(),
this.mg.config().trainStationMaxRange(),
+7 -3
View File
@@ -21,11 +21,11 @@ import { WarshipExecution } from "./WarshipExecution";
export class ConstructionExecution implements Execution {
private construction: Unit | null = null;
private active = true;
private mg: Game;
private mg: Game | undefined;
private ticksUntilComplete: Tick;
private ticksUntilComplete: Tick | undefined;
private cost: Gold;
private cost: Gold | undefined;
constructor(
private player: Player,
@@ -53,6 +53,7 @@ export class ConstructionExecution implements Execution {
tick(ticks: number): void {
if (this.construction === null) {
if (this.mg === undefined) throw new Error("Not initialized");
const info = this.mg.unitInfo(this.constructionType);
if (info.constructionDuration === undefined) {
this.completeConstruction();
@@ -90,15 +91,18 @@ export class ConstructionExecution implements Execution {
this.player = this.construction.owner();
this.construction.delete(false);
// refund the cost so player has the gold to build the unit
if (this.cost === undefined) throw new Error("Not initialized");
this.player.addGold(this.cost);
this.completeConstruction();
this.active = false;
return;
}
if (this.ticksUntilComplete === undefined) throw new Error("Not initialized");
this.ticksUntilComplete--;
}
private completeConstruction() {
if (this.mg === undefined) throw new Error("Not initialized");
const { player } = this;
switch (this.constructionType) {
case UnitType.AtomBomb:
+2 -1
View File
@@ -3,7 +3,7 @@ import { ShellExecution } from "./ShellExecution";
import { TileRef } from "../game/GameMap";
export class DefensePostExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private post: Unit | null = null;
private active = true;
@@ -24,6 +24,7 @@ export class DefensePostExecution implements Execution {
private shoot() {
if (this.post === null) return;
if (this.target === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const shellAttackRate = this.mg.config().defensePostShellAttackRate();
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
this.lastShellAttack = this.mg.ticks();
+1 -1
View File
@@ -2,7 +2,7 @@ import { Execution, Game, MessageType, Player } from "../game/Game";
export class DeleteUnitExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
constructor(
private readonly player: Player,
+3 -2
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Gold, Player, PlayerID } from "../game/Game";
export class DonateGoldExecution implements Execution {
private recipient: Player;
private recipient: Player | undefined;
private active = true;
@@ -23,7 +23,8 @@ export class DonateGoldExecution implements Execution {
}
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (this.gold === null) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
if (
this.sender.canDonateGold(this.recipient) &&
this.sender.donateGold(this.recipient, this.gold)
+3 -2
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class DonateTroopsExecution implements Execution {
private recipient: Player;
private recipient: Player | undefined;
private active = true;
@@ -26,7 +26,8 @@ export class DonateTroopsExecution implements Execution {
}
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (this.troops === null) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
if (
this.sender.canDonateTroops(this.recipient) &&
this.sender.donateTroops(this.recipient, this.troops)
+2 -1
View File
@@ -3,7 +3,7 @@ import { Execution, Game, Player, PlayerID } from "../game/Game";
export class EmbargoExecution implements Execution {
private active = true;
private target: Player;
private target: Player | undefined;
constructor(
private readonly player: Player,
@@ -21,6 +21,7 @@ export class EmbargoExecution implements Execution {
}
tick(_: number): void {
if (this.target === undefined) throw new Error("Not initialized");
if (this.action === "start") this.player.addEmbargo(this.target, false);
else this.player.stopEmbargo(this.target);
+2 -1
View File
@@ -9,7 +9,7 @@ import {
import { flattenedEmojiTable } from "../Util";
export class EmojiExecution implements Execution {
private recipient: Player | typeof AllPlayers;
private recipient: Player | typeof AllPlayers | undefined;
private active = true;
@@ -33,6 +33,7 @@ export class EmojiExecution implements Execution {
}
tick(ticks: number): void {
if (this.recipient === undefined) throw new Error("Not initialized");
const emojiString = flattenedEmojiTable[this.emoji];
if (emojiString === undefined) {
console.warn(
+3 -1
View File
@@ -5,7 +5,8 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class FactoryExecution implements Execution {
private factory: Unit | null = null;
private active = true;
private game: Game;
private game: Game | undefined;
constructor(
private player: Player,
private readonly tile: TileRef,
@@ -46,6 +47,7 @@ export class FactoryExecution implements Execution {
createStation(): void {
if (this.factory !== null) {
if (this.game === undefined) throw new Error("Not initialized");
const structures = this.game.nearbyUnits(
this.factory.tile(),
this.game.config().trainStationMaxRange(),
+47 -27
View File
@@ -30,7 +30,7 @@ export class FakeHumanExecution implements Execution {
private active = true;
private readonly random: PseudoRandom;
private behavior: BotBehavior | null = null;
private mg: Game;
private mg: Game | undefined;
private player: Player | null = null;
private readonly attackRate: number;
@@ -69,6 +69,7 @@ export class FakeHumanExecution implements Execution {
private updateRelationsFromEmbargos() {
const { player } = this;
if (player === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
@@ -92,6 +93,7 @@ export class FakeHumanExecution implements Execution {
private handleEmbargoesToHostileNations() {
const { player } = this;
if (player === null) return;
if (this.mg === undefined) throw new Error("Not initialized");
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
@@ -114,6 +116,7 @@ export class FakeHumanExecution implements Execution {
tick(ticks: number) {
if (ticks % this.attackRate !== this.attackTick) return;
if (this.mg === undefined) throw new Error("Not initialized");
if (this.mg.inSpawnPhase()) {
const rl = this.randomLand();
if (rl === null) {
@@ -164,13 +167,15 @@ export class FakeHumanExecution implements Execution {
private maybeAttack() {
if (this.player === null || this.behavior === null) {
throw new Error("not initialized");
throw new Error("Not initialized");
}
const game = this.mg;
if (game === undefined) throw new Error("Not initialized");
const enemyborder = Array.from(this.player.borderTiles())
.flatMap((t) => this.mg.neighbors(t))
.flatMap((t) => game.neighbors(t))
.filter(
(t) =>
this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(),
game.isLand(t) && game.ownerID(t) !== this.player?.smallID(),
);
if (enemyborder.length === 0) {
@@ -185,10 +190,10 @@ export class FakeHumanExecution implements Execution {
}
const borderPlayers = enemyborder.map((t) =>
this.mg.playerBySmallID(this.mg.ownerID(t)),
game.playerBySmallID(game.ownerID(t)),
);
if (borderPlayers.some((o) => !o.isPlayer())) {
this.behavior.sendAttack(this.mg.terraNullius());
this.behavior.sendAttack(game.terraNullius());
return;
}
@@ -228,7 +233,7 @@ export class FakeHumanExecution implements Execution {
}
private shouldAttack(other: Player): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.player.isOnSameTeam(other)) {
return false;
}
@@ -249,6 +254,7 @@ export class FakeHumanExecution implements Execution {
if (other.isTraitor()) {
return false;
}
if (this.mg === undefined) throw new Error("Not initialized");
const { difficulty } = this.mg.config().gameConfig();
if (
difficulty === Difficulty.Hard ||
@@ -264,9 +270,10 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendEmoji(enemy: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (enemy.type() !== PlayerType.Human) return;
const lastSent = this.lastEmojiSent.get(enemy) ?? -300;
if (this.mg === undefined) throw new Error("Not initialized");
if (this.mg.ticks() - lastSent <= 300) return;
this.lastEmojiSent.set(enemy, this.mg.ticks());
this.mg.addExecution(
@@ -279,7 +286,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendNuke(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const silos = this.player.units(UnitType.MissileSilo);
if (
silos.length === 0 ||
@@ -328,6 +336,7 @@ export class FakeHumanExecution implements Execution {
}
private removeOldNukeEvents() {
if (this.mg === undefined) throw new Error("Not initialized");
const maxAge = 500;
const tick = this.mg.ticks();
while (
@@ -339,7 +348,8 @@ export class FakeHumanExecution implements Execution {
}
private sendNuke(tile: TileRef) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const tick = this.mg.ticks();
this.lastNukeSent.push([tick, tile]);
this.mg.addExecution(
@@ -348,10 +358,12 @@ export class FakeHumanExecution implements Execution {
}
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
if (this.mg === undefined) throw new Error("Not initialized");
const game = this.mg;
// Potential damage in a 25-tile radius
const dist = euclDistFN(tile, 25, false);
let tileValue = targets
.filter((unit) => dist(this.mg, unit.tile()))
.filter((unit) => dist(game, unit.tile()))
.map((unit): number => {
switch (unit.type()) {
case UnitType.City:
@@ -374,7 +386,7 @@ export class FakeHumanExecution implements Execution {
50_000 *
targets.filter(
(unit) =>
unit.type() === UnitType.SAMLauncher && dist50(this.mg, unit.tile()),
unit.type() === UnitType.SAMLauncher && dist50(game, unit.tile()),
).length;
// Prefer tiles that are closer to a silo
@@ -388,7 +400,7 @@ export class FakeHumanExecution implements Execution {
// Don't target near recent targets
tileValue -= this.lastNukeSent
.filter(([_tick, tile]) => dist(this.mg, tile))
.filter(([_tick, tile]) => dist(game, tile))
.map((_) => 1_000_000)
.reduce((prev, cur) => prev + cur, 0);
@@ -396,14 +408,13 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendBoatAttack(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.player.isOnSameTeam(other)) return;
const closest = closestTwoTiles(
this.mg,
Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
),
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)),
Array.from(this.player.borderTiles()).filter((t) => this.mg?.isOceanShore(t)),
Array.from(other.borderTiles()).filter((t) => this.mg?.isOceanShore(t)),
);
if (closest === null) {
return;
@@ -430,7 +441,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnStructure(type: UnitType): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const owned = this.player.unitsOwned(type);
const perceivedCostMultiplier = Math.min(owned + 1, 5);
const realCost = this.cost(type);
@@ -451,11 +463,11 @@ export class FakeHumanExecution implements Execution {
}
private structureSpawnTile(type: UnitType): TileRef | null {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
const tiles =
type === UnitType.Port
? Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
this.mg?.isOceanShore(t),
)
: Array.from(this.player.tiles());
if (tiles.length === 0) return null;
@@ -490,7 +502,8 @@ export class FakeHumanExecution implements Execution {
}
private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const borderTiles = this.player.borderTiles();
const { mg } = this;
const otherUnits = this.player.units(type);
@@ -547,7 +560,8 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnWarship(): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
if (!this.random.chance(50)) {
return false;
}
@@ -577,6 +591,7 @@ export class FakeHumanExecution implements Execution {
}
private randTerritoryTile(p: Player): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const boundingBox = calculateBoundingBox(this.mg, p.borderTiles());
for (let i = 0; i < 100; i++) {
const randX = this.random.nextInt(boundingBox.min.x, boundingBox.max.x);
@@ -594,6 +609,7 @@ export class FakeHumanExecution implements Execution {
}
private warshipSpawnTile(portTile: TileRef): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const radius = 250;
for (let attempts = 0; attempts < 50; attempts++) {
const randX = this.random.nextInt(
@@ -618,14 +634,16 @@ export class FakeHumanExecution implements Execution {
}
private cost(type: UnitType): Gold {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
return this.mg.unitInfo(type).cost(this.player);
}
sendBoatRandomly() {
if (this.player === null) throw new Error("not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.player === null) throw new Error("Not initialized");
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
this.mg?.isOceanShore(t),
);
if (oceanShore.length === 0) {
return;
@@ -651,6 +669,7 @@ export class FakeHumanExecution implements Execution {
}
randomLand(): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
const delta = 25;
let tries = 0;
while (tries < 50) {
@@ -676,7 +695,8 @@ export class FakeHumanExecution implements Execution {
}
private randomBoatTarget(tile: TileRef, dist: number): TileRef | null {
if (this.player === null) throw new Error("not initialized");
if (this.player === null) throw new Error("Not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
for (let i = 0; i < 500; i++) {
+16 -7
View File
@@ -16,20 +16,20 @@ import { simpleHash } from "../Util";
export class MirvExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private nuke: Unit | null = null;
private readonly mirvRange = 1500;
private readonly warheadCount = 350;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
private pathFinder: ParabolaPathFinder;
private pathFinder: ParabolaPathFinder | undefined;
private targetPlayer: Player | TerraNullius;
private targetPlayer: Player | TerraNullius | undefined;
private separateDst: TileRef;
private separateDst: TileRef | undefined;
private speed = -1;
@@ -61,6 +61,9 @@ export class MirvExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.targetPlayer === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
const spawn = this.player.canBuild(UnitType.MIRV, this.dst);
if (spawn === false) {
@@ -98,7 +101,9 @@ export class MirvExecution implements Execution {
}
private separate() {
if (this.nuke === null) throw new Error("uninitialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.nuke === null) throw new Error("Not initialized");
const dsts: TileRef[] = [this.dst];
let attempts = 1000;
while (attempts > 0 && dsts.length < this.warheadCount) {
@@ -110,9 +115,10 @@ export class MirvExecution implements Execution {
dsts.push(potential);
}
console.log(`dsts: ${dsts.length}`);
const game = this.mg;
dsts.sort(
(a, b) =>
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst),
game.manhattanDist(b, this.dst) - game.manhattanDist(a, this.dst),
);
console.log(`got ${dsts.length} dsts!!`);
@@ -133,6 +139,8 @@ export class MirvExecution implements Execution {
}
randomLand(ref: TileRef, taken: TileRef[]): TileRef | null {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
let tries = 0;
const mirvRange2 = this.mirvRange * this.mirvRange;
while (tries < 100) {
@@ -168,6 +176,7 @@ export class MirvExecution implements Execution {
}
private proximityCheck(tile: TileRef, taken: TileRef[]): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
for (const t of taken) {
if (this.mg.manhattanDist(tile, t) < 55) {
return true;
+2 -1
View File
@@ -3,7 +3,7 @@ import { TileRef } from "../game/GameMap";
export class MissileSiloExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private silo: Unit | null = null;
constructor(
@@ -38,6 +38,7 @@ export class MissileSiloExecution implements Execution {
return;
}
if (this.mg === undefined) throw new Error("Not initialized");
const cooldown =
this.mg.config().SiloCooldown() - (this.mg.ticks() - frontTime);
+13 -2
View File
@@ -18,10 +18,10 @@ const SPRITE_RADIUS = 16;
export class NukeExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private nuke: Unit | null = null;
private tilesToDestroyCache: Set<TileRef> | undefined;
private pathFinder: ParabolaPathFinder;
private pathFinder: ParabolaPathFinder | undefined;
constructor(
private readonly nukeType: NukeType,
@@ -41,6 +41,7 @@ export class NukeExecution implements Execution {
}
public target(): Player | TerraNullius {
if (this.mg === undefined) throw new Error("Not initialized");
return this.mg.owner(this.dst);
}
@@ -51,6 +52,7 @@ export class NukeExecution implements Execution {
if (this.nuke === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
const rand = new PseudoRandom(this.mg.ticks());
const inner2 = magnitude.inner * magnitude.inner;
@@ -63,6 +65,7 @@ export class NukeExecution implements Execution {
}
private maybeBreakAlliances(toDestroy: Set<TileRef>) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
throw new Error("Not initialized");
}
@@ -94,6 +97,8 @@ export class NukeExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
const spawn = this.src ?? this.player.canBuild(this.nukeType, this.dst);
if (spawn === false) {
@@ -179,6 +184,8 @@ export class NukeExecution implements Execution {
}
private getTrajectory(target: TileRef): TrajectoryTile[] {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
const trajectoryTiles: TrajectoryTile[] = [];
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
@@ -199,6 +206,7 @@ export class NukeExecution implements Execution {
nukeTile: TileRef,
targetRangeSquared: number,
): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
return (
this.mg.euclideanDistSquared(nukeTile, targetTile) < targetRangeSquared ||
(this.src !== undefined &&
@@ -211,6 +219,7 @@ export class NukeExecution implements Execution {
if (this.nuke === null || this.nuke.targetTile() === undefined) {
return;
}
if (this.mg === undefined) throw new Error("Not initialized");
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
const targetTile = this.nuke.targetTile();
@@ -221,6 +230,7 @@ export class NukeExecution implements Execution {
}
private detonate() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.nuke === null) {
throw new Error("Not initialized");
}
@@ -304,6 +314,7 @@ export class NukeExecution implements Execution {
}
private redrawBuildings(range: number) {
if (this.mg === undefined) throw new Error("Not initialized");
const rangeSquared = range * range;
for (const unit of this.mg.units()) {
if (isStructureType(unit.type())) {
+12 -5
View File
@@ -7,9 +7,9 @@ import { GameImpl } from "../game/GameImpl";
export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;
private config: Config;
private config: Config | undefined;
private lastCalc = 0;
private mg: Game;
private mg: Game | undefined;
private active = true;
constructor(private readonly player: Player) {}
@@ -26,13 +26,15 @@ export class PlayerExecution implements Execution {
}
tick(ticks: number) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.config === undefined) throw new Error("Not initialized");
this.player.decayRelations();
this.player.units().forEach((u) => {
const tileOwner = this.mg.owner(u.tile());
const tileOwner = this.mg?.owner(u.tile());
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner?.isPlayer()) {
if (tileOwner !== this.player) {
this.mg.player(tileOwner.id()).captureUnit(u);
this.mg?.player(tileOwner.id()).captureUnit(u);
}
} else {
u.delete();
@@ -98,6 +100,7 @@ export class PlayerExecution implements Execution {
}
private removeClusters() {
if (this.mg === undefined) throw new Error("Not initialized");
const clusters = this.calculateClusters();
clusters.sort((a, b) => b.size - a.size);
@@ -117,6 +120,7 @@ export class PlayerExecution implements Execution {
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
if (this.mg === undefined) throw new Error("Not initialized");
const enemies = new Set<number>();
for (const tile of cluster) {
const isOceanShore = this.mg.isOceanShore(tile);
@@ -151,6 +155,7 @@ export class PlayerExecution implements Execution {
}
private isSurrounded(cluster: Set<TileRef>): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
const enemyTiles = new Set<TileRef>();
for (const tr of cluster) {
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
@@ -174,6 +179,7 @@ export class PlayerExecution implements Execution {
}
private removeCluster(cluster: Set<TileRef>) {
if (this.mg === undefined) throw new Error("Not initialized");
if (
Array.from(cluster).some(
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
@@ -208,6 +214,7 @@ export class PlayerExecution implements Execution {
}
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
if (this.mg === undefined) throw new Error("Not initialized");
const neighborsIDs = new Set<number>();
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
+9 -6
View File
@@ -6,10 +6,10 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class PortExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private port: Unit | null = null;
private random: PseudoRandom;
private checkOffset: number;
private random: PseudoRandom | undefined;
private checkOffset: number | undefined;
constructor(
private player: Player,
@@ -23,9 +23,9 @@ export class PortExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null || this.random === null || this.checkOffset === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.checkOffset === undefined) throw new Error("Not initialized");
if (this.port === null) {
const { tile } = this;
const spawn = this.player.canBuild(UnitType.Port, tile);
@@ -77,6 +77,8 @@ export class PortExecution implements Execution {
}
shouldSpawnTradeShip(): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips);
const level = this.port?.level() ?? 0;
@@ -89,6 +91,7 @@ export class PortExecution implements Execution {
}
createStation(): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.port !== null) {
const nearbyFactory = this.mg.hasUnitNearby(
this.port.tile(),
+4 -2
View File
@@ -1,8 +1,8 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class QuickChatExecution implements Execution {
private recipient: Player;
private mg: Game;
private recipient: Player | undefined;
private mg: Game | undefined;
private active = true;
@@ -27,6 +27,8 @@ export class QuickChatExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.recipient === undefined) throw new Error("Not initialized");
const message = this.getMessageFromKey(this.quickChatKey);
this.mg.displayChat(
+4 -7
View File
@@ -4,7 +4,7 @@ import { Railroad } from "../game/Railroad";
import { TileRef } from "../game/GameMap";
export class RailroadExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
private headIndex = 0;
private tailIndex = 0;
@@ -52,6 +52,7 @@ export class RailroadExecution implements Execution {
/* eslint-enable sort-keys */
private computeExtremityDirection(tile: TileRef, next: TileRef): RailType {
if (this.mg === undefined) throw new Error("Not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
const nextX = this.mg.x(next);
@@ -75,9 +76,7 @@ export class RailroadExecution implements Execution {
current: TileRef,
next: TileRef,
): RailType {
if (this.mg === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
const x1 = this.mg.x(prev);
const y1 = this.mg.y(prev);
const x2 = this.mg.x(current);
@@ -114,9 +113,7 @@ export class RailroadExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (!this.activeSourceOrDestination()) {
this.active = false;
return;
+5 -2
View File
@@ -5,8 +5,8 @@ const cancelDelay = 20;
export class RetreatExecution implements Execution {
private active = true;
private retreatOrdered = false;
private startTick: number;
private mg: Game;
private startTick: number | undefined;
private mg: Game | undefined;
constructor(
private readonly player: Player,
private readonly attackID: string,
@@ -18,6 +18,9 @@ export class RetreatExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.startTick === undefined) throw new Error("Not initialized");
if (!this.retreatOrdered) {
this.player.orderRetreat(this.attackID);
this.retreatOrdered = true;
+5 -5
View File
@@ -126,14 +126,14 @@ class SAMTargetingSystem {
}
export class SAMLauncherExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
// As MIRV go very fast we have to detect them very early but we only
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
private readonly MIRVWarheadSearchRadius = 400;
private readonly MIRVWarheadProtectionRadius = 50;
private targetingSystem: SAMTargetingSystem;
private targetingSystem: SAMTargetingSystem | undefined;
private pseudoRandom: PseudoRandom | undefined;
@@ -156,6 +156,7 @@ export class SAMLauncherExecution implements Execution {
return true;
}
if (this.mg === undefined) throw new Error("Not initialized");
if (type === UnitType.MIRVWarhead) {
return random < this.mg.config().samWarheadHittingChance();
}
@@ -164,9 +165,7 @@ export class SAMLauncherExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
if (this.mg === undefined) throw new Error("Not initialized");
if (this.sam === null) {
if (this.tile === null) {
throw new Error("tile is null");
@@ -211,6 +210,7 @@ export class SAMLauncherExecution implements Execution {
this.MIRVWarheadSearchRadius,
UnitType.MIRVWarhead,
({ unit }) => {
if (this.mg === undefined) return false;
if (!isUnit(unit)) return false;
if (unit.owner() === this.player) return false;
if (this.player.isFriendly(unit.owner())) return false;
+4 -2
View File
@@ -13,9 +13,9 @@ import { TileRef } from "../game/GameMap";
export class SAMMissileExecution implements Execution {
private active = true;
private pathFinder: AirPathFinder;
private pathFinder: AirPathFinder | undefined;
private SAMMissile: Unit | undefined;
private mg: Game;
private mg: Game | undefined;
private speed = 0;
constructor(
@@ -33,6 +33,8 @@ export class SAMMissileExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
this.SAMMissile ??= this._owner.buildUnit(
UnitType.SAMMissile,
this.spawn,
+7 -3
View File
@@ -5,11 +5,11 @@ import { TileRef } from "../game/GameMap";
export class ShellExecution implements Execution {
private active = true;
private pathFinder: AirPathFinder;
private pathFinder: AirPathFinder | undefined;
private shell: Unit | undefined;
private mg: Game;
private mg: Game | undefined;
private destroyAtTick = -1;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
constructor(
private readonly spawn: TileRef,
@@ -25,6 +25,8 @@ export class ShellExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
this.shell ??= this._owner.buildUnit(UnitType.Shell, this.spawn, {});
if (!this.shell.isActive()) {
this.active = false;
@@ -62,6 +64,8 @@ export class ShellExecution implements Execution {
}
private effectOnTarget(): number {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const { damage } = this.mg.config().unitInfo(UnitType.Shell);
const baseDamage = damage ?? 250;
+2 -1
View File
@@ -6,7 +6,7 @@ import { getSpawnTiles } from "./Util";
export class SpawnExecution implements Execution {
active = true;
private mg: Game;
private mg: Game | undefined;
constructor(
private readonly playerInfo: PlayerInfo,
@@ -20,6 +20,7 @@ export class SpawnExecution implements Execution {
tick(ticks: number) {
this.active = false;
if (this.mg === undefined) throw new Error("Not initialized");
if (!this.mg.isValidRef(this.tile)) {
console.warn(`SpawnExecution: tile ${this.tile} not valid`);
return;
+2 -1
View File
@@ -1,7 +1,7 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class TargetPlayerExecution implements Execution {
private target: Player;
private target: Player | undefined;
private active = true;
@@ -21,6 +21,7 @@ export class TargetPlayerExecution implements Execution {
}
tick(ticks: number): void {
if (this.target === undefined) throw new Error("Not initialized");
if (this.requestor.canTarget(this.target)) {
this.requestor.target(this.target);
this.target.updateRelation(this.requestor, -40);
+5 -2
View File
@@ -14,10 +14,10 @@ import { renderNumber } from "../../client/Utils";
export class TradeShipExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | undefined;
private tradeShip: Unit | undefined;
private wasCaptured = false;
private pathFinder: PathFinder;
private pathFinder: PathFinder | undefined;
private tilesTraveled = 0;
constructor(
@@ -32,6 +32,8 @@ export class TradeShipExecution implements Execution {
}
tick(ticks: number): void {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (this.tradeShip === undefined) {
const spawn = this.origOwner.canBuild(
UnitType.TradeShip,
@@ -131,6 +133,7 @@ export class TradeShipExecution implements Execution {
}
private complete() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.tradeShip === undefined) throw new Error("Not initialized");
this.active = false;
this.tradeShip.delete(false);
+6 -2
View File
@@ -4,9 +4,9 @@ import { TrainExecution } from "./TrainExecution";
import { TrainStation } from "../game/TrainStation";
export class TrainStationExecution implements Execution {
private mg: Game;
private mg: Game | undefined;
private active = true;
private random: PseudoRandom;
private random: PseudoRandom | undefined;
private station: TrainStation | null = null;
private readonly numCars = 5;
private lastSpawnTick = 0;
@@ -49,6 +49,8 @@ export class TrainStationExecution implements Execution {
}
private shouldSpawnTrain(clusterSize: number): boolean {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
const spawnRate = this.mg.config().trainSpawnRate(clusterSize);
for (let i = 0; i < this.unit.level(); i++) {
if (this.random.chance(spawnRate)) {
@@ -59,6 +61,8 @@ export class TrainStationExecution implements Execution {
}
private spawnTrain(station: TrainStation, currentTick: number) {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (
!this.spawnTrains ||
currentTick - this.lastSpawnTick < this.ticksCooldown
+12 -7
View File
@@ -15,23 +15,23 @@ import { TileRef } from "../game/GameMap";
import { targetTransportTile } from "../game/TransportShipUtils";
export class TransportShipExecution implements Execution {
private lastMove: number;
private lastMove: number | undefined;
// TODO: make this configurable
private readonly ticksPerMove = 1;
private active = true;
private mg: Game;
private target: Player | TerraNullius;
private mg: Game | undefined;
private target: Player | TerraNullius | undefined;
// TODO make private
public path: TileRef[];
private dst: TileRef | null;
public path: TileRef[] | undefined;
private dst: TileRef | null = null;
private boat: Unit;
private boat: Unit | undefined;
private pathFinder: PathFinder;
private pathFinder: PathFinder | undefined;
constructor(
private readonly attacker: Player,
@@ -158,6 +158,11 @@ export class TransportShipExecution implements Execution {
if (!this.active) {
return;
}
if (this.boat === undefined) throw new Error("Not initialized");
if (this.lastMove === undefined) throw new Error("Not initialized");
if (this.mg === undefined) throw new Error("Not initialized");
if (this.target === undefined) throw new Error("Not initialized");
if (this.pathFinder === undefined) throw new Error("Not initialized");
if (!this.boat.isActive()) {
this.active = false;
return;
@@ -2,7 +2,7 @@ import { Execution, Game, Player, Unit } from "../game/Game";
export class UpgradeStructureExecution implements Execution {
private structure: Unit | undefined;
private readonly cost: bigint;
private readonly cost: bigint | undefined;
constructor(
private readonly player: Player,
+17 -5
View File
@@ -14,10 +14,10 @@ import { ShellExecution } from "./ShellExecution";
import { TileRef } from "../game/GameMap";
export class WarshipExecution implements Execution {
private random: PseudoRandom;
private warship: Unit;
private mg: Game;
private pathfinder: PathFinder;
private random: PseudoRandom | undefined;
private warship: Unit | undefined;
private mg: Game | undefined;
private pathfinder: PathFinder | undefined;
private lastShellAttack = 0;
private readonly alreadySentShell = new Set<Unit>();
@@ -51,6 +51,7 @@ export class WarshipExecution implements Execution {
}
tick(ticks: number): void {
if (this.warship === undefined) throw new Error("Not initialized");
if (this.warship.health() <= 0) {
this.warship.delete();
return;
@@ -75,6 +76,8 @@ export class WarshipExecution implements Execution {
}
private findTargetUnit(): Unit | undefined {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0;
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
@@ -152,6 +155,8 @@ export class WarshipExecution implements Execution {
}
private shootTarget() {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
const shellAttackRate = this.mg.config().warshipShellAttackRate();
@@ -178,6 +183,8 @@ export class WarshipExecution implements Execution {
}
private huntDownTradeShip() {
if (this.pathfinder === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
const targetUnit = this.warship.targetUnit();
if (targetUnit === undefined) return;
for (let i = 0; i < 2; i++) {
@@ -207,6 +214,8 @@ export class WarshipExecution implements Execution {
}
private patrol() {
if (this.pathfinder === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
let targetTile = this.warship.targetTile();
if (targetTile === undefined) {
targetTile = this.randomTile();
@@ -238,7 +247,7 @@ export class WarshipExecution implements Execution {
}
isActive(): boolean {
return this.warship?.isActive();
return this.warship?.isActive() ?? false;
}
activeDuringSpawnPhase(): boolean {
@@ -246,6 +255,9 @@ export class WarshipExecution implements Execution {
}
randomTile(allowShoreline = false): TileRef | undefined {
if (this.mg === undefined) throw new Error("Not initialized");
if (this.random === undefined) throw new Error("Not initialized");
if (this.warship === undefined) throw new Error("Not initialized");
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
const maxAttemptBeforeExpand = 500;
let attempts = 0;
+2 -1
View File
@@ -15,7 +15,7 @@ import { flattenedEmojiTable } from "../../Util";
export class BotBehavior {
private enemy: Player | null = null;
private enemyUpdated: Tick;
private enemyUpdated: Tick | undefined;
private readonly assistAcceptEmoji = flattenedEmojiTable.indexOf("👍");
@@ -75,6 +75,7 @@ export class BotBehavior {
}
forgetOldEnemies() {
if (this.enemyUpdated === undefined) return;
// Forget old enemies
if (this.game.ticks() - this.enemyUpdated > 100) {
this.clearEnemy();
+1 -1
View File
@@ -283,7 +283,7 @@ export class Nation {
}
export class Cell {
public index: number;
public index: number | undefined;
private readonly strRepr: string;
+3 -2
View File
@@ -78,7 +78,7 @@ export class GameImpl implements Game {
private updates: GameUpdates = createGameUpdatesMap();
private readonly unitGrid: UnitGrid;
private playerTeams: Team[];
private playerTeams: Team[] = [];
private readonly botTeam: Team = ColoredTeams.Bot;
private readonly _railNetwork: RailNetwork = createRailNetwork(this);
@@ -125,7 +125,8 @@ export class GameImpl implements Game {
if (numPlayerTeams < 2) {
throw new Error(`Too few teams: ${numPlayerTeams}`);
} else if (numPlayerTeams < 8) {
this.playerTeams = [ColoredTeams.Red, ColoredTeams.Blue];
this.playerTeams.push(ColoredTeams.Red);
this.playerTeams.push(ColoredTeams.Blue);
if (numPlayerTeams >= 3) this.playerTeams.push(ColoredTeams.Yellow);
if (numPlayerTeams >= 4) this.playerTeams.push(ColoredTeams.Green);
if (numPlayerTeams >= 5) this.playerTeams.push(ColoredTeams.Purple);
+1 -1
View File
@@ -119,7 +119,7 @@ export class PlayerImpl implements Player {
this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id));
}
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
largestClusterBoundingBox: { min: Cell; max: Cell } | null = null;
toUpdate(): PlayerUpdate {
const outgoingAllianceRequests = this.outgoingAllianceRequests().map((ar) =>
+1 -1
View File
@@ -87,7 +87,7 @@ export function createTrainStopHandlers(
export class TrainStation {
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
{};
private cluster: Cluster | null;
private cluster: Cluster | null = null;
private readonly railroads: Set<Railroad> = new Set();
constructor(
+2 -2
View File
@@ -106,7 +106,7 @@ export class PathFinder {
private curr: TileRef | null = null;
private dst: TileRef | null = null;
private path: TileRef[] | null = null;
private aStar: AStar<TileRef>;
private aStar: AStar<TileRef> | undefined;
private computeFinished = true;
private constructor(
@@ -170,7 +170,7 @@ export class PathFinder {
}
}
switch (this.aStar.compute()) {
switch (this.aStar?.compute()) {
case PathFindResultType.Completed:
this.computeFinished = true;
this.path = this.aStar.reconstructPath();