diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index f0d8e8900..ca9904a48 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -226,11 +226,11 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ type: z.literal("boat"), targetID: ID.nullable(), - troops: z.number().nullable(), + troops: z.number(), dstX: z.number(), dstY: z.number(), - srcX: z.number().nullable().optional(), - srcY: z.number().nullable().optional(), + srcX: z.number().nullable(), + srcY: z.number().nullable(), }); export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 571532118..3b48b4049 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -283,7 +283,7 @@ export class AttackExecution implements Execution { this.border.add(neighbor); const numOwnedByMe = this.mg .neighbors(neighbor) - .filter((t) => this.mg.owner(t) == this._owner).length; + .filter((t) => this.mg.owner(t) === this._owner).length; let mag = 0; switch (this.mg.terrainType(tile)) { case TerrainType.Plains: diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index b25cd0a67..3b0354810 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -33,7 +33,7 @@ export class BotExecution implements Execution { } tick(ticks: number) { - if (ticks % this.attackRate != this.attackTick) return; + if (ticks % this.attackRate !== this.attackTick) return; if (!this.bot.isAlive()) { this.active = false; diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index ae4a0e74e..4580ba77f 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -16,7 +16,7 @@ export class DefensePostExecution implements Execution { private post: Unit | null = null; private active: boolean = true; - private target: Unit = null; + private target: Unit | null = null; private lastShellAttack = 0; private alreadySentShell = new Set(); @@ -37,6 +37,8 @@ export class DefensePostExecution implements Execution { } private shoot() { + if (this.post === null) return; + if (this.target === null) return; const shellAttackRate = this.mg.config().defensePostShellAttackRate(); if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { this.lastShellAttack = this.mg.ticks(); @@ -76,7 +78,7 @@ export class DefensePostExecution implements Execution { this.player = this.post.owner(); } - if (this.target != null && !this.target.isActive()) { + if (this.target !== null && !this.target.isActive()) { this.target = null; } @@ -117,7 +119,7 @@ export class DefensePostExecution implements Execution { return distA - distB; })[0]?.unit ?? null; - if (this.target == null || !this.target.isActive()) { + if (this.target === null || !this.target.isActive()) { this.target = null; return; } else { diff --git a/src/core/execution/EmojiExecution.ts b/src/core/execution/EmojiExecution.ts index 8d48c8b6c..d5cc56e24 100644 --- a/src/core/execution/EmojiExecution.ts +++ b/src/core/execution/EmojiExecution.ts @@ -42,13 +42,16 @@ export class EmojiExecution implements Execution { tick(ticks: number): void { const emojiString = flattenedEmojiTable.at(this.emoji); - - if (this.requestor.canSendEmoji(this.recipient)) { + if (emojiString === undefined) { + consolex.warn( + `cannot send emoji ${this.emoji} from ${this.requestor} to ${this.recipient}`, + ); + } else if (this.requestor.canSendEmoji(this.recipient)) { this.requestor.sendEmoji(this.recipient, emojiString); if ( - emojiString == "🖕" && - this.recipient != AllPlayers && - this.recipient.type() == PlayerType.FakeHuman + emojiString === "🖕" && + this.recipient !== AllPlayers && + this.recipient.type() === PlayerType.FakeHuman ) { this.recipient.updateRelation(this.requestor, -100); } diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 685383060..ab66789c1 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,4 +1,5 @@ import { Execution, Game } from "../game/Game"; +import { TileRef } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; import { ClientID, GameID, Intent, Turn } from "../Schemas"; import { simpleHash } from "../Util"; @@ -66,8 +67,8 @@ export class Executor { this.mg.ref(intent.x, intent.y), ); case "boat": - let src = null; - if (intent.srcX != null || intent.srcY != null) { + let src: TileRef | null = null; + if (intent.srcX !== null && intent.srcY !== null) { src = this.mg.ref(intent.srcX, intent.srcY); } return new TransportShipExecution( diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index d84f7c268..981b74098 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -111,11 +111,11 @@ export class FakeHumanExecution implements Execution { } tick(ticks: number) { - if (ticks % this.attackRate != this.attackTick) return; + if (ticks % this.attackRate !== this.attackTick) return; if (this.mg.inSpawnPhase()) { const rl = this.randomLand(); - if (rl == null) { + if (rl === null) { consolex.warn(`cannot spawn ${this.nation.playerInfo.name}`); return; } @@ -123,11 +123,11 @@ export class FakeHumanExecution implements Execution { return; } - if (this.player == null) { - this.player = this.mg - .players() - .find((p) => p.id() == this.nation.playerInfo.id); - if (this.player == null) { + if (this.player === null) { + this.player = + this.mg.players().find((p) => p.id() === this.nation.playerInfo.id) ?? + null; + if (this.player === null) { return; } } @@ -170,6 +170,9 @@ export class FakeHumanExecution implements Execution { } private maybeAttack() { + if (this.player === null || this.behavior === null) { + throw new Error("not initialized"); + } const enemyborder = Array.from(this.player.borderTiles()) .flatMap((t) => this.mg.neighbors(t)) .filter( @@ -255,6 +258,9 @@ export class FakeHumanExecution implements Execution { } handleEnemies() { + if (this.player === null || this.behavior === null) { + throw new Error("not initialized"); + } this.behavior.forgetOldEnemies(); this.behavior.checkIncomingAttacks(); this.behavior.assistAllies(); @@ -270,7 +276,8 @@ export class FakeHumanExecution implements Execution { } private maybeSendEmoji(enemy: Player) { - if (enemy.type() != PlayerType.Human) return; + 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.ticks() - lastSent <= 300) return; this.lastEmojiSent.set(enemy, this.mg.ticks()); @@ -284,11 +291,12 @@ export class FakeHumanExecution implements Execution { } private maybeSendNuke(other: Player) { + if (this.player === null) throw new Error("not initialized"); const silos = this.player.units(UnitType.MissileSilo); if ( - silos.length == 0 || + silos.length === 0 || this.player.gold() < this.cost(UnitType.AtomBomb) || - other.type() == PlayerType.Bot || + other.type() === PlayerType.Bot || this.player.isOnSameTeam(other) ) { return; @@ -302,17 +310,17 @@ export class FakeHumanExecution implements Execution { UnitType.SAMLauncher, ); const structureTiles = structures.map((u) => u.tile()); - const randomTiles: TileRef[] = new Array(10); + const randomTiles: (TileRef | null)[] = new Array(10); for (let i = 0; i < randomTiles.length; i++) { randomTiles[i] = this.randTerritoryTile(other); } const allTiles = randomTiles.concat(structureTiles); - let bestTile = null; + let bestTile: TileRef | null = null; let bestValue = 0; this.removeOldNukeEvents(); outer: for (const tile of new Set(allTiles)) { - if (tile == null) continue; + if (tile === null) continue; for (const t of this.mg.bfs(tile, manhattanDistFN(tile, 15))) { // Make sure we nuke at least 15 tiles in border if (this.mg.owner(t) !== other) { @@ -321,12 +329,12 @@ export class FakeHumanExecution implements Execution { } if (!this.player.canBuild(UnitType.AtomBomb, tile)) continue; const value = this.nukeTileScore(tile, silos, structures); - if (value > bestTile) { + if (value > bestValue) { bestTile = tile; bestValue = value; } } - if (bestTile != null) { + if (bestTile !== null) { this.sendNuke(bestTile); } } @@ -343,6 +351,7 @@ export class FakeHumanExecution implements Execution { } private sendNuke(tile: TileRef) { + if (this.player === null) throw new Error("not initialized"); const tick = this.mg.ticks(); this.lastNukeSent.push([tick, tile]); this.mg.addExecution( @@ -375,7 +384,9 @@ export class FakeHumanExecution implements Execution { // Prefer tiles that are closer to a silo const siloTiles = silos.map((u) => u.tile()); - const { x: closestSilo } = closestTwoTiles(this.mg, siloTiles, [tile]); + const result = closestTwoTiles(this.mg, siloTiles, [tile]); + if (result === null) throw new Error("Missing result"); + const { x: closestSilo } = result; const distanceSquared = this.mg.euclideanDistSquared(tile, closestSilo); const distanceToClosestSilo = Math.sqrt(distanceSquared); tileValue -= distanceToClosestSilo * 30; @@ -437,6 +448,7 @@ export class FakeHumanExecution implements Execution { } private maybeSpawnStructure(type: UnitType, maxNum: number) { + if (this.player === null) throw new Error("not initialized"); const units = this.player.units(type); if (units.length >= maxNum) { return; diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index fa0378bb1..2b40b39ad 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -32,7 +32,10 @@ export class MissileSiloExecution implements Execution { } tick(ticks: number): void { - if (this.silo == null) { + if (this.player === null || this.mg === null) { + throw new Error("Not initialized"); + } + if (this.silo === null) { const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile); if (spawn === false) { consolex.warn( diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index b6c0bb5bf..26d046e6f 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -111,7 +111,7 @@ export class NukeExecution implements Execution { this.pathFinder.computeControlPoints( spawn, this.dst, - this.type != UnitType.MIRVWarhead, + this.type !== UnitType.MIRVWarhead, ); this.nuke = this.player.buildUnit(this.type, spawn, { detonationDst: this.dst, diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 702244d30..92ced26d9 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -30,7 +30,7 @@ export class SAMLauncherExecution implements Execution { private tile: TileRef, private sam: Unit | null = null, ) { - if (sam != null) { + if (sam !== null) { this.tile = sam.tile(); } } @@ -46,6 +46,7 @@ export class SAMLauncherExecution implements Execution { } private getSingleTarget(): Unit | null { + if (this.sam === null) return null; const nukes = this.mg .nearbyUnits(this.sam.tile(), this.searchRangeRadius, [ UnitType.AtomBomb, @@ -80,11 +81,11 @@ export class SAMLauncherExecution implements Execution { } private isHit(type: UnitType, random: number): boolean { - if (type == UnitType.AtomBomb) { + if (type === UnitType.AtomBomb) { return true; } - if (type == UnitType.MIRVWarhead) { + if (type === UnitType.MIRVWarhead) { return random < this.mg.config().samWarheadHittingChance(); } @@ -130,14 +131,18 @@ export class SAMLauncherExecution implements Execution { (unit) => unit.owner() !== this.player && !this.player.isFriendly(unit.owner()), ) - .filter( - (unit) => - this.mg.manhattanDist(unit.detonationDst(), this.sam.tile()) < - this.MIRVWarheadProtectionRadius, - ); + .filter((unit) => { + const dst = unit.detonationDst(); + return ( + this.sam !== null && + dst !== null && + this.mg.manhattanDist(dst, this.sam.tile()) < + this.MIRVWarheadProtectionRadius + ); + }); let target: Unit | null = null; - if (mirvWarheadTargets.length == 0) { + if (mirvWarheadTargets.length === 0) { target = this.getSingleTarget(); } @@ -155,7 +160,8 @@ export class SAMLauncherExecution implements Execution { ) { this.sam.setCooldown(true); const type = - mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target.type(); + mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target?.type(); + if (type === undefined) throw new Error("Unknown unit type"); const random = this.pseudoRandom.next(); const hit = this.isHit(type, random); if (!hit) { @@ -174,7 +180,7 @@ export class SAMLauncherExecution implements Execution { ); // Delete warheads mirvWarheadTargets.forEach((u) => u.delete()); - } else { + } else if (target !== null) { target.setTargetedBySAM(true); this.mg.addExecution( new SAMMissileExecution( @@ -184,6 +190,8 @@ export class SAMLauncherExecution implements Execution { target, ), ); + } else { + throw new Error("target is null"); } } } diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 12c6e6d77..6d4eab4a2 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -6,7 +6,7 @@ import { PseudoRandom } from "../PseudoRandom"; export class ShellExecution implements Execution { private active = true; private pathFinder: AirPathFinder; - private shell: Unit; + private shell: Unit | undefined; private mg: Game; private destroyAtTick: number = -1; @@ -23,7 +23,7 @@ export class ShellExecution implements Execution { } tick(ticks: number): void { - if (this.shell == null) { + if (this.shell === undefined) { this.shell = this._owner.buildUnit(UnitType.Shell, this.spawn, {}); } if (!this.shell.isActive()) { @@ -40,7 +40,7 @@ export class ShellExecution implements Execution { return; } - if (this.destroyAtTick == -1 && !this.ownerUnit.isActive()) { + if (this.destroyAtTick === -1 && !this.ownerUnit.isActive()) { this.destroyAtTick = this.mg.ticks() + this.mg.config().shellLifetime(); } @@ -61,8 +61,8 @@ export class ShellExecution implements Execution { } private effectOnTarget(): number { - const baseDamage: number = this.mg.config().unitInfo(UnitType.Shell).damage; - return baseDamage; + const { damage } = this.mg.config().unitInfo(UnitType.Shell); + return damage ?? 0; } isActive(): boolean { diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 84dee6a73..6c8d52005 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -59,7 +59,7 @@ export class TradeShipExecution implements Execution { return; } - if (this.origOwner != this.tradeShip.owner()) { + if (this.origOwner !== this.tradeShip.owner()) { // Store as variable in case ship is recaptured by previous owner this.wasCaptured = true; } diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 830c93a33..b73ea6bd7 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -39,7 +39,7 @@ export class TransportShipExecution implements Execution { private attackerID: PlayerID, private targetID: PlayerID | null, private ref: TileRef, - private troops: number | null, + private troops: number, private src: TileRef | null, ) {} @@ -120,19 +120,19 @@ export class TransportShipExecution implements Execution { UnitType.TransportShip, this.dst, ); - if (closestTileSrc == false) { + if (closestTileSrc === false) { consolex.warn(`can't build transport ship`); this.active = false; return; } - if (this.src == null) { + if (this.src === null) { // Only update the src if it's not already set // because we assume that the src is set to the best spawn tile this.src = closestTileSrc; } else { if ( - this.mg.owner(this.src) != this.attacker || + this.mg.owner(this.src) !== this.attacker || !this.mg.isShore(this.src) ) { console.warn( diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index 189bd12e1..8c2159a71 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -79,7 +79,7 @@ export class WinCheckExecution implements Execution { this.mg.numLandTiles() - this.mg.numTilesWithFallout(); const percentage = (max[1] / numTilesWithoutFallout) * 100; if (percentage > this.mg.config().percentageTilesOwnedToWin()) { - if (max[0] == ColoredTeams.Bot) return; + if (max[0] === ColoredTeams.Bot) return; this.mg.setWinner(max[0], this.mg.stats().stats()); console.log(`${max[0]} has won the game`); this.active = false; diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index c4f765c79..afbb53331 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -111,8 +111,8 @@ export class BotBehavior { // Select the most hated player if (this.enemy === null) { - const mostHated = this.player.allRelationsSorted()[0] ?? null; - if (mostHated != null && mostHated.relation === Relation.Hostile) { + const mostHated = this.player.allRelationsSorted()[0]; + if (mostHated !== undefined && mostHated.relation === Relation.Hostile) { this.enemy = mostHated.player; this.enemyUpdated = this.game.ticks(); } @@ -137,7 +137,7 @@ export class BotBehavior { for (const neighbor of this.random.shuffleArray(neighbors)) { if (!neighbor.isPlayer()) continue; if (this.player.isFriendly(neighbor)) continue; - if (neighbor.type() == PlayerType.FakeHuman) { + if (neighbor.type() === PlayerType.FakeHuman) { if (this.random.chance(2)) { continue; } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index af134975c..f3fec6c39 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -762,6 +762,7 @@ export class PlayerImpl implements Player { case UnitType.MIRVWarhead: return targetTile; case UnitType.Port: + if (validTiles === null) throw new Error("validTiles is required"); return this.portSpawn(targetTile, validTiles); case UnitType.Warship: return this.warshipSpawn(targetTile); diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index ada40769f..993671ca7 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -16,13 +16,12 @@ import { PlayerImpl } from "./PlayerImpl"; export class UnitImpl implements Unit { private _active = true; private _health: bigint; - private _lastTile: TileRef = null; - private _target: Unit = null; - private _moveTarget: TileRef = null; + private _lastTile: TileRef; + private _moveTarget: TileRef | null = null; private _targetedBySAM = false; private _safeFromPiratesCooldown: number; // Only for trade ships private _lastSetSafeFromPirates: number; // Only for trade ships - private _constructionType: UnitType = undefined; + private _constructionType: UnitType | undefined; private _troops: number; private _cooldownTick: Tick | null = null; @@ -45,12 +44,14 @@ export class UnitImpl implements Unit { .config() .safeFromPiratesCooldownMax(); - this._troops = "troops" in params ? params.troops : 0; - this._dstPort = "dstPort" in params ? params.dstPort : null; + this._troops = "troops" in params ? (params.troops ?? 0) : 0; + this._dstPort = "dstPort" in params ? params.dstPort : undefined; this._cooldownDuration = - "cooldownDuration" in params ? params.cooldownDuration : null; + "cooldownDuration" in params ? params.cooldownDuration : undefined; this._lastSetSafeFromPirates = - "lastSetSafeFromPirates" in params ? params.lastSetSafeFromPirates : 0; + "lastSetSafeFromPirates" in params + ? (params.lastSetSafeFromPirates ?? 0) + : 0; } id() { @@ -93,7 +94,7 @@ export class UnitImpl implements Unit { } move(tile: TileRef): void { - if (tile == null) { + if (tile === null) { throw new Error("tile cannot be null"); } this.mg.removeUnit(this); @@ -112,7 +113,7 @@ export class UnitImpl implements Unit { return Number(this._health); } hasHealth(): boolean { - return this.info().maxHealth != undefined; + return this.info().maxHealth !== undefined; } tile(): TileRef { return this._tile; @@ -127,7 +128,7 @@ export class UnitImpl implements Unit { setOwner(newOwner: Player): void { const oldOwner = this._owner; - oldOwner._units = oldOwner._units.filter((u) => u != this); + oldOwner._units = oldOwner._units.filter((u) => u !== this); this._owner = newOwner as PlayerImpl; this.mg.addUpdate(this.toUpdate()); this.mg.displayMessage( @@ -149,11 +150,11 @@ export class UnitImpl implements Unit { if (!this.isActive()) { throw new Error(`cannot delete ${this} not active`); } - this._owner._units = this._owner._units.filter((b) => b != this); + this._owner._units = this._owner._units.filter((b) => b !== this); this._active = false; this.mg.addUpdate(this.toUpdate()); this.mg.removeUnit(this); - if (displayMessage && this.type() != UnitType.MIRVWarhead) { + if (displayMessage && this.type() !== UnitType.MIRVWarhead) { this.mg.displayMessage( `Your ${this.type()} was destroyed`, MessageType.ERROR, @@ -166,14 +167,14 @@ export class UnitImpl implements Unit { } constructionType(): UnitType | null { - if (this.type() != UnitType.Construction) { + if (this.type() !== UnitType.Construction) { throw new Error(`Cannot get construction type on ${this.type()}`); } return this._constructionType ?? null; } setConstructionType(type: UnitType): void { - if (this.type() != UnitType.Construction) { + if (this.type() !== UnitType.Construction) { throw new Error(`Cannot set construction type on ${this.type()}`); } this._constructionType = type;