Enable strictNullChecks, eqeqeq (#436)

## Description:

Improve type safety and runtime correctness by:
1. Enabling TypeScript's
[strictNullChecks](https://www.typescriptlang.org/tsconfig/#strictNullChecks)
compiler option.
2. Replacing all loose equality operators (`==` and `!=`) with strict
equality operators (`===` and `!==`).
3. Cleaning up of type declarations, null handling logic, and equality
expressions throughout the project.

Currently, the code allows implicit assumptions that `null` and
`undefined` are interchangeable, and relies on type-coercing equality
checks that can introduce subtle bugs. These practices make it difficult
to reason about when values may be absent and hinder the effectiveness
of static analysis.

Migrating to strict null checks and enforcing strict equality
comparisons will clarify intent, reduce bugs, and make the codebase
safer and easier to maintain.

Fixes #466 

## 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

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
Co-authored-by: evanpelle <openfrontio@gmail.com>
This commit is contained in:
Scott Anderson
2025-05-15 19:39:40 -04:00
committed by GitHub
parent 369483b4ac
commit 70745faac4
119 changed files with 1428 additions and 1123 deletions
+32 -21
View File
@@ -32,7 +32,7 @@ export class AttackExecution implements Execution {
private border = new Set<TileRef>();
private attack: Attack = null;
private attack: Attack | null = null;
constructor(
private startTroops: number | null = null,
@@ -42,7 +42,7 @@ export class AttackExecution implements Execution {
private removeTroops: boolean = true,
) {}
public targetID(): PlayerID {
public targetID(): PlayerID | null {
return this._targetID;
}
@@ -61,7 +61,7 @@ export class AttackExecution implements Execution {
this.active = false;
return;
}
if (this._targetID != null && !mg.hasPlayer(this._targetID)) {
if (this._targetID !== null && !mg.hasPlayer(this._targetID)) {
console.warn(`target ${this._targetID} not found`);
this.active = false;
return;
@@ -69,22 +69,22 @@ export class AttackExecution implements Execution {
this._owner = mg.player(this._ownerID);
this.target =
this._targetID == this.mg.terraNullius().id()
this._targetID === this.mg.terraNullius().id()
? mg.terraNullius()
: mg.player(this._targetID);
if (this.target && this.target.isPlayer()) {
const targetPlayer = this.target as Player;
if (
targetPlayer.type() != PlayerType.Bot &&
this._owner.type() != PlayerType.Bot
targetPlayer.type() !== PlayerType.Bot &&
this._owner.type() !== PlayerType.Bot
) {
// Don't let bots embargo since they can't trade anyway.
targetPlayer.addEmbargo(this._owner.id(), true);
}
}
if (this._owner == this.target) {
if (this._owner === this.target) {
console.error(`Player ${this._owner} cannot attack itself`);
this.active = false;
return;
@@ -101,7 +101,7 @@ export class AttackExecution implements Execution {
return;
}
if (this.startTroops == null) {
if (this.startTroops === null) {
this.startTroops = this.mg
.config()
.attackAmount(this._owner, this.target);
@@ -117,7 +117,7 @@ export class AttackExecution implements Execution {
);
for (const incoming of this._owner.incomingAttacks()) {
if (incoming.attacker() == this.target) {
if (incoming.attacker() === this.target) {
// Target has opposing attack, cancel them out
if (incoming.troops() > this.attack.troops()) {
incoming.setTroops(incoming.troops() - this.attack.troops());
@@ -132,9 +132,9 @@ export class AttackExecution implements Execution {
}
for (const outgoing of this._owner.outgoingAttacks()) {
if (
outgoing != this.attack &&
outgoing.target() == this.attack.target() &&
outgoing.sourceTile() == this.attack.sourceTile()
outgoing !== this.attack &&
outgoing.target() === this.attack.target() &&
outgoing.sourceTile() === this.attack.sourceTile()
) {
// Existing attack on same target, add troops
outgoing.setTroops(outgoing.troops() + this.attack.troops());
@@ -144,7 +144,7 @@ export class AttackExecution implements Execution {
}
}
if (this.sourceTile != null) {
if (this.sourceTile !== null) {
this.addNeighbors(this.sourceTile);
} else {
this.refreshToConquer();
@@ -168,6 +168,10 @@ export class AttackExecution implements Execution {
}
private retreat(malusPercent = 0) {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
const deaths = this.attack.troops() * (malusPercent / 100);
if (deaths) {
this.mg.displayMessage(
@@ -182,6 +186,10 @@ export class AttackExecution implements Execution {
}
tick(ticks: number) {
if (this.attack === null) {
throw new Error("Attack not initialized");
}
if (this.attack.retreated()) {
if (this.attack.target().isPlayer()) {
this.retreat(malusForRetreat);
@@ -202,7 +210,7 @@ export class AttackExecution implements Execution {
}
const alliance = this._owner.allianceWith(this.target as Player);
if (this.breakAlliance && alliance != null) {
if (this.breakAlliance && alliance !== null) {
this.breakAlliance = false;
this._owner.breakAlliance(alliance);
}
@@ -228,7 +236,7 @@ export class AttackExecution implements Execution {
return;
}
if (this.toConquer.size() == 0) {
if (this.toConquer.size() === 0) {
this.refreshToConquer();
this.retreat();
return;
@@ -240,8 +248,8 @@ export class AttackExecution implements Execution {
const onBorder =
this.mg
.neighbors(tileToConquer)
.filter((t) => this.mg.owner(t) == this._owner).length > 0;
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
.filter((t) => this.mg.owner(t) === this._owner).length > 0;
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
continue;
}
this.addNeighbors(tileToConquer);
@@ -266,13 +274,16 @@ export class AttackExecution implements Execution {
private addNeighbors(tile: TileRef) {
for (const neighbor of this.mg.neighbors(tile)) {
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
if (
this.mg.isWater(neighbor) ||
this.mg.owner(neighbor) !== this.target
) {
continue;
}
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:
@@ -314,13 +325,13 @@ 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 {
for (const neighbor of this.mg.neighbors(tile)) {
const no = this.mg.owner(neighbor);
if (no.isPlayer() && no != this.target) {
if (no.isPlayer() && no !== this.target) {
this.mg.player(no.id()).conquer(tile);
break;
}
+4 -1
View File
@@ -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;
@@ -55,6 +55,9 @@ export class BotExecution implements Execution {
}
private maybeAttack() {
if (this.behavior === null) {
throw new Error("not initialized");
}
const traitors = this.bot
.neighbors()
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
+1 -1
View File
@@ -27,7 +27,7 @@ export class BotSpawner {
}
const botName = this.randomBotName();
const spawn = this.spawnBot(botName);
if (spawn != null) {
if (spawn !== null) {
this.bots.push(spawn);
} else {
tries++;
+4 -4
View File
@@ -12,7 +12,7 @@ import { TileRef } from "../game/GameMap";
export class CityExecution implements Execution {
private player: Player;
private mg: Game;
private city: Unit;
private city: Unit | null = null;
private active: boolean = true;
constructor(
@@ -31,9 +31,9 @@ export class CityExecution implements Execution {
}
tick(ticks: number): void {
if (this.city == null) {
if (this.city === null) {
const spawnTile = this.player.canBuild(UnitType.City, this.tile);
if (spawnTile == false) {
if (spawnTile === false) {
consolex.warn("cannot build city");
this.active = false;
return;
@@ -45,7 +45,7 @@ export class CityExecution implements Execution {
return;
}
if (this.player != this.city.owner()) {
if (this.player !== this.city.owner()) {
this.player = this.city.owner();
}
}
+6 -6
View File
@@ -20,7 +20,7 @@ import { WarshipExecution } from "./WarshipExecution";
export class ConstructionExecution implements Execution {
private player: Player;
private construction: Unit;
private construction: Unit | null = null;
private active: boolean = true;
private mg: Game;
@@ -45,15 +45,15 @@ export class ConstructionExecution implements Execution {
}
tick(ticks: number): void {
if (this.construction == null) {
if (this.construction === null) {
const info = this.mg.unitInfo(this.constructionType);
if (info.constructionDuration == null) {
if (info.constructionDuration === undefined) {
this.completeConstruction();
this.active = false;
return;
}
const spawnTile = this.player.canBuild(this.constructionType, this.tile);
if (spawnTile == false) {
if (spawnTile === false) {
consolex.warn(`cannot build ${this.constructionType}`);
this.active = false;
return;
@@ -75,11 +75,11 @@ export class ConstructionExecution implements Execution {
return;
}
if (this.player != this.construction.owner()) {
if (this.player !== this.construction.owner()) {
this.player = this.construction.owner();
}
if (this.ticksUntilComplete == 0) {
if (this.ticksUntilComplete === 0) {
this.player = this.construction.owner();
this.construction.delete(false);
// refund the cost so player has the gold to build the unit
+50 -49
View File
@@ -13,10 +13,10 @@ import { ShellExecution } from "./ShellExecution";
export class DefensePostExecution implements Execution {
private player: Player;
private mg: Game;
private post: Unit;
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<Unit>();
@@ -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();
@@ -58,9 +60,9 @@ export class DefensePostExecution implements Execution {
}
tick(ticks: number): void {
if (this.post == null) {
if (this.post === null) {
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
if (spawnTile == false) {
if (spawnTile === false) {
consolex.warn("cannot build Defense Post");
this.active = false;
return;
@@ -72,58 +74,57 @@ export class DefensePostExecution implements Execution {
return;
}
if (this.player != this.post.owner()) {
if (this.player !== this.post.owner()) {
this.player = this.post.owner();
}
if (this.target != null && !this.target.isActive()) {
if (this.target !== null && !this.target.isActive()) {
this.target = null;
}
// TODO: Reconsider how/if defense posts target ships.
return;
const ships = this.mg
.nearbyUnits(
this.post.tile(),
this.mg.config().defensePostTargettingRange(),
[UnitType.TransportShip, UnitType.Warship],
)
.filter(
({ unit }) =>
unit.owner() !== this.post.owner() &&
!unit.owner().isFriendly(this.post.owner()) &&
!this.alreadySentShell.has(unit),
);
this.target =
ships.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
// Prioritize TransportShip
if (
unitA.type() === UnitType.TransportShip &&
unitB.type() !== UnitType.TransportShip
)
return -1;
if (
unitA.type() !== UnitType.TransportShip &&
unitB.type() === UnitType.TransportShip
)
return 1;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit ?? null;
if (this.target == null || !this.target.isActive()) {
this.target = null;
return;
} else {
this.shoot();
return;
}
// const ships = this.mg
// .nearbyUnits(
// this.post.tile(),
// this.mg.config().defensePostTargettingRange(),
// [UnitType.TransportShip, UnitType.Warship],
// )
// .filter(
// ({ unit }) =>
// this.post !== null &&
// unit.owner() !== this.post.owner() &&
// !unit.owner().isFriendly(this.post.owner()) &&
// !this.alreadySentShell.has(unit),
// );
//
// this.target =
// ships.sort((a, b) => {
// const { unit: unitA, distSquared: distA } = a;
// const { unit: unitB, distSquared: distB } = b;
//
// // Prioritize TransportShip
// if (
// unitA.type() === UnitType.TransportShip &&
// unitB.type() !== UnitType.TransportShip
// )
// return -1;
// if (
// unitA.type() !== UnitType.TransportShip &&
// unitB.type() === UnitType.TransportShip
// )
// return 1;
//
// // If both are the same type, sort by distance (lower `distSquared` means closer)
// return distA - distB;
// })[0]?.unit ?? null;
//
// if (this.target === null || !this.target.isActive()) {
// this.target = null;
// return;
// } else {
// this.shoot();
// return;
// }
}
isActive(): boolean {
+2 -5
View File
@@ -27,12 +27,13 @@ export class DonateGoldExecution implements Execution {
this.sender = mg.player(this.senderID);
this.recipient = mg.player(this.recipientID);
if (this.gold == null) {
if (this.gold === null) {
this.gold = Math.round(this.sender.gold() / 3);
}
}
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateGold(this.recipient, this.gold);
this.recipient.updateRelation(this.sender, 50);
@@ -44,10 +45,6 @@ export class DonateGoldExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+2 -5
View File
@@ -27,12 +27,13 @@ export class DonateTroopsExecution implements Execution {
this.sender = mg.player(this.senderID);
this.recipient = mg.player(this.recipientID);
if (this.troops == null) {
if (this.troops === null) {
this.troops = mg.config().defaultDonationAmount(this.sender);
}
}
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (this.sender.canDonate(this.recipient)) {
this.sender.donateTroops(this.recipient, this.troops);
this.recipient.updateRelation(this.sender, 50);
@@ -44,10 +45,6 @@ export class DonateTroopsExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+1 -5
View File
@@ -23,16 +23,12 @@ export class EmbargoExecution implements Execution {
}
tick(_: number): void {
if (this.action == "start") this.player.addEmbargo(this.targetID, false);
if (this.action === "start") this.player.addEmbargo(this.targetID, false);
else this.player.stopEmbargo(this.targetID);
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+12 -11
View File
@@ -27,7 +27,7 @@ export class EmojiExecution implements Execution {
this.active = false;
return;
}
if (this.recipientID != AllPlayers && !mg.hasPlayer(this.recipientID)) {
if (this.recipientID !== AllPlayers && !mg.hasPlayer(this.recipientID)) {
console.warn(`EmojiExecution: recipient ${this.recipientID} not found`);
this.active = false;
return;
@@ -35,18 +35,23 @@ export class EmojiExecution implements Execution {
this.requestor = mg.player(this.senderID);
this.recipient =
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
this.recipientID === AllPlayers
? AllPlayers
: mg.player(this.recipientID);
}
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);
}
@@ -58,10 +63,6 @@ export class EmojiExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+5 -4
View File
@@ -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";
@@ -24,7 +25,7 @@ import { TransportShipExecution } from "./TransportShipExecution";
export class Executor {
// private random = new PseudoRandom(999)
private random: PseudoRandom = null;
private random: PseudoRandom;
constructor(
private mg: Game,
@@ -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(
@@ -126,7 +127,7 @@ export class Executor {
}
fakeHumanExecutions(): Execution[] {
const execs = [];
const execs: Execution[] = [];
for (const nation of this.mg.nations()) {
execs.push(new FakeHumanExecution(this.gameID, nation));
}
+75 -51
View File
@@ -33,7 +33,7 @@ export class FakeHumanExecution implements Execution {
private random: PseudoRandom;
private behavior: BotBehavior | null = null;
private mg: Game;
private player: Player = null;
private player: Player | null = null;
private attackRate: number;
private attackTick: number;
@@ -67,51 +67,55 @@ export class FakeHumanExecution implements Execution {
}
private updateRelationsFromEmbargos() {
const others = this.mg.players().filter((p) => p.id() != this.player.id());
const player = this.player;
if (player === null) return;
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
const embargoMalus = -20;
if (
other.hasEmbargoAgainst(this.player) &&
other.hasEmbargoAgainst(player) &&
!this.embargoMalusApplied.has(other.id())
) {
this.player.updateRelation(other, embargoMalus);
player.updateRelation(other, embargoMalus);
this.embargoMalusApplied.add(other.id());
} else if (
!other.hasEmbargoAgainst(this.player) &&
!other.hasEmbargoAgainst(player) &&
this.embargoMalusApplied.has(other.id())
) {
this.player.updateRelation(other, -embargoMalus);
player.updateRelation(other, -embargoMalus);
this.embargoMalusApplied.delete(other.id());
}
});
}
private handleEmbargoesToHostileNations() {
const others = this.mg.players().filter((p) => p.id() != this.player.id());
const player = this.player;
if (player === null) return;
const others = this.mg.players().filter((p) => p.id() !== player.id());
others.forEach((other: Player) => {
/* When player is hostile starts embargo. Do not stop until neutral again */
if (
this.player.relation(other) <= Relation.Hostile &&
!this.player.hasEmbargoAgainst(other)
player.relation(other) <= Relation.Hostile &&
!player.hasEmbargoAgainst(other)
) {
this.player.addEmbargo(other.id(), false);
player.addEmbargo(other.id(), false);
} else if (
this.player.relation(other) >= Relation.Neutral &&
this.player.hasEmbargoAgainst(other)
player.relation(other) >= Relation.Neutral &&
player.hasEmbargoAgainst(other)
) {
this.player.stopEmbargo(other.id());
player.stopEmbargo(other.id());
}
});
}
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;
}
@@ -119,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;
}
}
@@ -166,13 +170,17 @@ 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(
(t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID(),
(t) =>
this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(),
);
if (enemyborder.length == 0) {
if (enemyborder.length === 0) {
if (this.random.chance(10)) {
this.sendBoatRandomly();
}
@@ -214,6 +222,7 @@ export class FakeHumanExecution implements Execution {
}
private shouldAttack(other: Player): boolean {
if (this.player === null) throw new Error("not initialized");
if (this.player.isOnSameTeam(other)) {
return false;
}
@@ -235,10 +244,13 @@ export class FakeHumanExecution implements Execution {
return false;
}
const difficulty = this.mg.config().gameConfig().difficulty;
if (difficulty == Difficulty.Hard || difficulty == Difficulty.Impossible) {
if (
difficulty === Difficulty.Hard ||
difficulty === Difficulty.Impossible
) {
return false;
}
if (other.type() != PlayerType.Human) {
if (other.type() !== PlayerType.Human) {
return false;
}
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
@@ -246,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();
@@ -261,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());
@@ -275,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;
@@ -293,20 +310,20 @@ 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) {
if (this.mg.owner(t) !== other) {
continue outer;
}
}
@@ -317,7 +334,7 @@ export class FakeHumanExecution implements Execution {
bestValue = value;
}
}
if (bestTile != null) {
if (bestTile !== null) {
this.sendNuke(bestTile);
}
}
@@ -334,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(
@@ -366,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;
@@ -381,6 +401,7 @@ export class FakeHumanExecution implements Execution {
}
private maybeSendBoatAttack(other: Player) {
if (this.player === null) throw new Error("not initialized");
if (this.player.isOnSameTeam(other)) return;
const closest = closestTwoTiles(
this.mg,
@@ -389,7 +410,7 @@ export class FakeHumanExecution implements Execution {
),
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)),
);
if (closest == null) {
if (closest === null) {
return;
}
this.mg.addExecution(
@@ -404,15 +425,17 @@ export class FakeHumanExecution implements Execution {
}
private handleUnits() {
const ports = this.player.units(UnitType.Port);
if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) {
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
const player = this.player;
if (player === null) return;
const ports = player.units(UnitType.Port);
if (ports.length === 0 && player.gold() > this.cost(UnitType.Port)) {
const oceanTiles = Array.from(player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
if (oceanTiles.length > 0) {
const buildTile = this.random.randElement(oceanTiles);
this.mg.addExecution(
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port),
new ConstructionExecution(player.id(), buildTile, UnitType.Port),
);
}
return;
@@ -425,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;
@@ -433,11 +457,11 @@ export class FakeHumanExecution implements Execution {
return;
}
const tile = this.randTerritoryTile(this.player);
if (tile == null) {
if (tile === null) {
return;
}
const canBuild = this.player.canBuild(type, tile);
if (canBuild == false) {
if (canBuild === false) {
return;
}
this.mg.addExecution(
@@ -446,6 +470,7 @@ export class FakeHumanExecution implements Execution {
}
private maybeSpawnWarship(): boolean {
if (this.player === null) throw new Error("not initialized");
if (!this.random.chance(50)) {
return false;
}
@@ -453,16 +478,16 @@ export class FakeHumanExecution implements Execution {
const ships = this.player.units(UnitType.Warship);
if (
ports.length > 0 &&
ships.length == 0 &&
ships.length === 0 &&
this.player.gold() > this.cost(UnitType.Warship)
) {
const port = this.random.randElement(ports);
const targetTile = this.warshipSpawnTile(port.tile());
if (targetTile == null) {
if (targetTile === null) {
return false;
}
const canBuild = this.player.canBuild(UnitType.Warship, targetTile);
if (canBuild == false) {
if (canBuild === false) {
consolex.warn("cannot spawn destroyer");
return false;
}
@@ -488,7 +513,7 @@ export class FakeHumanExecution implements Execution {
continue;
}
const randTile = this.mg.ref(randX, randY);
if (this.mg.owner(randTile) == p) {
if (this.mg.owner(randTile) === p) {
return randTile;
}
}
@@ -520,21 +545,23 @@ export class FakeHumanExecution implements Execution {
}
private cost(type: UnitType): number {
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");
const oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
this.mg.isOceanShore(t),
);
if (oceanShore.length == 0) {
if (oceanShore.length === 0) {
return;
}
const src = this.random.randElement(oceanShore);
const dst = this.randOceanShoreTile(src, 150);
if (dst == null) {
if (dst === null) {
return;
}
@@ -564,7 +591,7 @@ export class FakeHumanExecution implements Execution {
const tile = this.mg.ref(x, y);
if (this.mg.isLand(tile) && !this.mg.hasOwner(tile)) {
if (
this.mg.terrainType(tile) == TerrainType.Mountain &&
this.mg.terrainType(tile) === TerrainType.Mountain &&
this.random.chance(2)
) {
continue;
@@ -576,6 +603,7 @@ export class FakeHumanExecution implements Execution {
}
private randOceanShoreTile(tile: TileRef, dist: number): TileRef | null {
if (this.player === null) throw new Error("not initialized");
const x = this.mg.x(tile);
const y = this.mg.y(tile);
for (let i = 0; i < 500; i++) {
@@ -599,10 +627,6 @@ export class FakeHumanExecution implements Execution {
return null;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+8 -7
View File
@@ -22,7 +22,7 @@ export class MirvExecution implements Execution {
private mg: Game;
private nuke: Unit;
private nuke: Unit | null = null;
private mirvRange = 1500;
private warheadCount = 350;
@@ -66,9 +66,9 @@ export class MirvExecution implements Execution {
}
tick(ticks: number): void {
if (this.nuke == null) {
if (this.nuke === null) {
const spawn = this.player.canBuild(UnitType.MIRV, this.dst);
if (spawn == false) {
if (spawn === false) {
consolex.warn(`cannot build MIRV`);
this.active = false;
return;
@@ -100,12 +100,13 @@ export class MirvExecution implements Execution {
}
private separate() {
if (this.nuke === null) throw new Error("uninitialized");
const dsts: TileRef[] = [this.dst];
let attempts = 1000;
while (attempts > 0 && dsts.length < this.warheadCount) {
attempts--;
const potential = this.randomLand(this.dst, dsts);
if (potential == null) {
if (potential === null) {
continue;
}
dsts.push(potential);
@@ -132,10 +133,10 @@ export class MirvExecution implements Execution {
}
if (this.targetPlayer.isPlayer()) {
const alliance = this.player.allianceWith(this.targetPlayer);
if (alliance != null) {
if (alliance !== null) {
this.player.breakAlliance(alliance);
}
if (this.targetPlayer != this.player) {
if (this.targetPlayer !== this.player) {
this.targetPlayer.updateRelation(this.player, -100);
}
}
@@ -165,7 +166,7 @@ export class MirvExecution implements Execution {
if (this.mg.euclideanDistSquared(tile, ref) > mirvRange2) {
continue;
}
if (this.mg.owner(tile) != this.targetPlayer) {
if (this.mg.owner(tile) !== this.targetPlayer) {
continue;
}
for (const t of taken) {
+9 -6
View File
@@ -11,9 +11,9 @@ import { TileRef } from "../game/GameMap";
export class MissileSiloExecution implements Execution {
private active = true;
private mg: Game;
private player: Player;
private silo: Unit;
private mg: Game | null = null;
private player: Player | null = null;
private silo: Unit | null = null;
constructor(
private _owner: PlayerID,
@@ -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(
@@ -45,14 +48,14 @@ export class MissileSiloExecution implements Execution {
cooldownDuration: this.mg.config().SiloCooldown(),
});
if (this.player != this.silo.owner()) {
if (this.player !== this.silo.owner()) {
this.player = this.silo.owner();
}
}
if (
this.silo.isCooldown() &&
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) == 0
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) === 0
) {
this.silo.setCooldown(false);
}
+5 -2
View File
@@ -4,7 +4,7 @@ const cancelDelay = 2;
export class MoveWarshipExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | null = null;
constructor(
public readonly unitId: number,
@@ -16,7 +16,10 @@ export class MoveWarshipExecution implements Execution {
}
tick(ticks: number): void {
const warship = this.mg.units().find((u) => u.id() == this.unitId);
if (this.mg === null) {
throw new Error("Not initialized");
}
const warship = this.mg.units().find((u) => u.id() === this.unitId);
if (!warship) {
console.log("MoveWarshipExecution: warship is already dead");
return;
+1 -4
View File
@@ -1,4 +1,4 @@
import { Execution, Game, Player } from "../game/Game";
import { Execution, Game } from "../game/Game";
export class NoOpExecution implements Execution {
isActive(): boolean {
@@ -9,7 +9,4 @@ export class NoOpExecution implements Execution {
}
init(mg: Game, ticks: number): void {}
tick(ticks: number): void {}
owner(): Player {
return null;
}
}
+45 -24
View File
@@ -15,10 +15,10 @@ import { ParabolaPathFinder } from "../pathfinding/PathFinding";
import { PseudoRandom } from "../PseudoRandom";
export class NukeExecution implements Execution {
private player: Player;
private active = true;
private mg: Game;
private nuke: Unit;
private player: Player | null = null;
private mg: Game | null = null;
private nuke: Unit | null = null;
private random: PseudoRandom;
private pathFinder: ParabolaPathFinder;
@@ -27,7 +27,7 @@ export class NukeExecution implements Execution {
private type: NukeType,
private senderID: PlayerID,
private dst: TileRef,
private src?: TileRef,
private src?: TileRef | null,
private speed: number = -1,
private waitTicks = 0,
) {}
@@ -42,28 +42,37 @@ export class NukeExecution implements Execution {
this.mg = mg;
this.player = mg.player(this.senderID);
this.random = new PseudoRandom(ticks);
if (this.speed == -1) {
if (this.speed === -1) {
this.speed = this.mg.config().defaultNukeSpeed();
}
this.pathFinder = new ParabolaPathFinder(mg);
}
public target(): Player | TerraNullius {
if (this.mg === null) {
throw new Error("Not initialized");
}
return this.mg.owner(this.dst);
}
private tilesToDestroy(): Set<TileRef> {
if (this.mg === null || this.nuke === null) {
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;
const outer2 = magnitude.outer * magnitude.outer;
return this.mg.bfs(this.dst, (_, n: TileRef) => {
const d2 = this.mg.euclideanDistSquared(this.dst, n);
const d2 = this.mg?.euclideanDistSquared(this.dst, n) ?? 0;
return d2 <= outer2 && (d2 <= inner2 || rand.chance(2));
});
}
private breakAlliances(toDestroy: Set<TileRef>) {
if (this.mg === null || this.player === null || this.nuke === null) {
throw new Error("Not initialized");
}
const attacked = new Map<Player, number>();
for (const tile of toDestroy) {
const owner = this.mg.owner(tile);
@@ -74,13 +83,13 @@ export class NukeExecution implements Execution {
}
for (const [other, tilesDestroyed] of attacked) {
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
if (tilesDestroyed > 100 && this.nuke.type() !== UnitType.MIRVWarhead) {
// Mirv warheads shouldn't break alliances
const alliance = this.player.allianceWith(other);
if (alliance != null) {
if (alliance !== null) {
this.player.breakAlliance(alliance);
}
if (other != this.player) {
if (other !== this.player) {
other.updateRelation(this.player, -100);
}
}
@@ -88,9 +97,13 @@ export class NukeExecution implements Execution {
}
tick(ticks: number): void {
if (this.nuke == null) {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
if (this.nuke === null) {
const spawn = this.src ?? this.player.canBuild(this.type, this.dst);
if (spawn == false) {
if (spawn === false) {
consolex.warn(`cannot build Nuke`);
this.active = false;
return;
@@ -98,14 +111,14 @@ 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,
});
if (this.mg.hasOwner(this.dst)) {
const target = this.mg.owner(this.dst) as Player;
if (this.type == UnitType.AtomBomb) {
if (this.type === UnitType.AtomBomb) {
this.mg.displayIncomingUnit(
this.nuke.id(),
`${this.player.name()} - atom bomb inbound`,
@@ -113,7 +126,7 @@ export class NukeExecution implements Execution {
target.id(),
);
}
if (this.type == UnitType.HydrogenBomb) {
if (this.type === UnitType.HydrogenBomb) {
this.mg.displayIncomingUnit(
this.nuke.id(),
`${this.player.name()} - hydrogen bomb inbound`,
@@ -164,6 +177,9 @@ export class NukeExecution implements Execution {
}
private detonate() {
if (this.mg === null || this.nuke === null) {
throw new Error("Not initialized");
}
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
const toDestroy = this.tilesToDestroy();
this.breakAlliances(toDestroy);
@@ -183,15 +199,17 @@ export class NukeExecution implements Execution {
.nukeDeathFactor(owner.workers(), owner.numTilesOwned()),
);
owner.outgoingAttacks().forEach((attack) => {
const deaths = this.mg
.config()
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
const deaths =
this.mg
?.config()
.nukeDeathFactor(attack.troops(), owner.numTilesOwned()) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
owner.units(UnitType.TransportShip).forEach((attack) => {
const deaths = this.mg
.config()
.nukeDeathFactor(attack.troops(), owner.numTilesOwned());
const deaths =
this.mg
?.config()
.nukeDeathFactor(attack.troops(), owner.numTilesOwned()) ?? 0;
attack.setTroops(attack.troops() - deaths);
});
}
@@ -204,10 +222,10 @@ export class NukeExecution implements Execution {
const outer2 = magnitude.outer * magnitude.outer;
for (const unit of this.mg.units()) {
if (
unit.type() != UnitType.AtomBomb &&
unit.type() != UnitType.HydrogenBomb &&
unit.type() != UnitType.MIRVWarhead &&
unit.type() != UnitType.MIRV
unit.type() !== UnitType.AtomBomb &&
unit.type() !== UnitType.HydrogenBomb &&
unit.type() !== UnitType.MIRVWarhead &&
unit.type() !== UnitType.MIRV
) {
if (this.mg.euclideanDistSquared(this.dst, unit.tile()) < outer2) {
unit.delete();
@@ -219,6 +237,9 @@ export class NukeExecution implements Execution {
}
owner(): Player {
if (this.player === null) {
throw new Error("Not initialized");
}
return this.player;
}
+51 -24
View File
@@ -16,10 +16,10 @@ import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;
private player: Player;
private config: Config;
private player: Player | null = null;
private config: Config | null = null;
private lastCalc = 0;
private mg: Game;
private mg: Game | null = null;
private active = true;
constructor(private playerID: PlayerID) {}
@@ -42,6 +42,9 @@ export class PlayerExecution implements Execution {
}
tick(ticks: number) {
if (this.mg === null || this.config === null || this.player === null) {
throw new Error("Not initialized");
}
this.player.decayRelations();
const hasPort = this.player.units(UnitType.Port).length > 0;
this.player.units().forEach((u) => {
@@ -49,13 +52,14 @@ export class PlayerExecution implements Execution {
u.delete();
return;
}
if (hasPort && u.type() == UnitType.Warship) {
if (hasPort && u.type() === UnitType.Warship) {
u.modifyHealth(1);
}
if (this.mg === null) return;
const tileOwner = this.mg.owner(u.tile());
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner != this.player) {
if (tileOwner !== this.player) {
this.mg.player(tileOwner.id()).captureUnit(u);
}
} else {
@@ -67,10 +71,10 @@ export class PlayerExecution implements Execution {
if (!this.player.isAlive()) {
this.player.units().forEach((u) => {
if (
u.type() != UnitType.AtomBomb &&
u.type() != UnitType.HydrogenBomb &&
u.type() != UnitType.MIRVWarhead &&
u.type() != UnitType.MIRV
u.type() !== UnitType.AtomBomb &&
u.type() !== UnitType.HydrogenBomb &&
u.type() !== UnitType.MIRVWarhead &&
u.type() !== UnitType.MIRV
) {
u.delete();
}
@@ -122,10 +126,14 @@ export class PlayerExecution implements Execution {
}
private removeClusters() {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
const clusters = this.calculateClusters();
clusters.sort((a, b) => b.size - a.size);
const main = clusters.shift();
if (main === undefined) throw new Error("No clusters");
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
const surroundedBy = this.surroundedBySamePlayer(main);
if (surroundedBy && !this.player.isFriendly(surroundedBy)) {
@@ -140,6 +148,9 @@ export class PlayerExecution implements Execution {
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
const enemies = new Set<number>();
for (const tile of cluster) {
const isOceanShore = this.mg.isOceanShore(tile);
@@ -149,19 +160,19 @@ export class PlayerExecution implements Execution {
if (
isOceanShore ||
this.mg.isOnEdgeOfMap(tile) ||
this.mg.neighbors(tile).some((n) => !this.mg.hasOwner(n))
this.mg.neighbors(tile).some((n) => !this.mg?.hasOwner(n))
) {
return false;
}
this.mg
.neighbors(tile)
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
.forEach((p) => enemies.add(this.mg.ownerID(p)));
if (enemies.size != 1) {
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
.forEach((p) => this.mg && enemies.add(this.mg.ownerID(p)));
if (enemies.size !== 1) {
return false;
}
}
if (enemies.size != 1) {
if (enemies.size !== 1) {
return false;
}
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
@@ -174,6 +185,9 @@ export class PlayerExecution implements Execution {
}
private isSurrounded(cluster: Set<TileRef>): boolean {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
const enemyTiles = new Set<TileRef>();
for (const tr of cluster) {
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
@@ -181,10 +195,10 @@ export class PlayerExecution implements Execution {
}
this.mg
.neighbors(tr)
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
.filter((n) => this.mg?.ownerID(n) !== this.player?.smallID())
.forEach((n) => enemyTiles.add(n));
}
if (enemyTiles.size == 0) {
if (enemyTiles.size === 0) {
return false;
}
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
@@ -193,9 +207,12 @@ export class PlayerExecution implements Execution {
}
private removeCluster(cluster: Set<TileRef>) {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
if (
Array.from(cluster).some(
(t) => this.mg.ownerID(t) != this.player.smallID(),
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
)
) {
// Other removeCluster operations could change tile owners,
@@ -204,16 +221,16 @@ export class PlayerExecution implements Execution {
}
const capturing = this.getCapturingPlayer(cluster);
if (capturing == null) {
if (capturing === null) {
return;
}
const firstTile = cluster.values().next().value;
const filter = (_, t: TileRef): boolean =>
this.mg.ownerID(t) == this.player.smallID();
this.mg?.ownerID(t) === this.player?.smallID();
const tiles = this.mg.bfs(firstTile, filter);
if (this.player.numTilesOwned() == tiles.size) {
if (this.player.numTilesOwned() === tiles.size) {
const gold = this.player.gold();
this.mg.displayMessage(
`Conquered ${this.player.displayName()} received ${renderNumber(
@@ -232,10 +249,13 @@ export class PlayerExecution implements Execution {
}
private getCapturingPlayer(cluster: Set<TileRef>): Player | null {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
const neighborsIDs = new Set<number>();
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
if (this.mg.ownerID(neighbor) !== this.player.smallID()) {
neighborsIDs.add(this.mg.ownerID(neighbor));
}
}
@@ -249,7 +269,7 @@ export class PlayerExecution implements Execution {
continue;
}
for (const attack of neighbor.outgoingAttacks()) {
if (attack.target() == this.player) {
if (attack.target() === this.player) {
if (attack.troops() > largestTroopCount) {
largestTroopCount = attack.troops();
largestNeighborAttack = neighbor;
@@ -257,7 +277,7 @@ export class PlayerExecution implements Execution {
}
}
}
if (largestNeighborAttack != null) {
if (largestNeighborAttack !== null) {
return largestNeighborAttack;
}
@@ -270,10 +290,13 @@ export class PlayerExecution implements Execution {
if (!capturing.isPlayer()) {
return null;
}
return capturing as Player;
return capturing;
}
private calculateClusters(): Set<TileRef>[] {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
const seen = new Set<TileRef>();
const border = this.player.borderTiles();
const clusters: Set<TileRef>[] = [];
@@ -287,6 +310,7 @@ export class PlayerExecution implements Execution {
seen.add(tile);
while (queue.length > 0) {
const curr = queue.shift();
if (curr === undefined) throw new Error("curr is undefined");
cluster.add(curr);
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
@@ -303,6 +327,9 @@ export class PlayerExecution implements Execution {
}
owner(): Player {
if (this.player === null) {
throw new Error("Not initialized");
}
return this.player;
}
+14 -8
View File
@@ -14,10 +14,10 @@ import { TradeShipExecution } from "./TradeShipExecution";
export class PortExecution implements Execution {
private active = true;
private mg: Game;
private port: Unit;
private random: PseudoRandom;
private checkOffset: number;
private mg: Game | null = null;
private port: Unit | null = null;
private random: PseudoRandom | null = null;
private checkOffset: number | null = null;
constructor(
private _owner: PlayerID,
@@ -36,7 +36,10 @@ export class PortExecution implements Execution {
}
tick(ticks: number): void {
if (this.port == null) {
if (this.mg === null || this.random === null || this.checkOffset === null) {
throw new Error("Not initialized");
}
if (this.port === null) {
const tile = this.tile;
const player = this.mg.player(this._owner);
const spawn = player.canBuild(UnitType.Port, tile);
@@ -53,12 +56,12 @@ export class PortExecution implements Execution {
return;
}
if (this._owner != this.port.owner().id()) {
if (this._owner !== this.port.owner().id()) {
this._owner = this.port.owner().id();
}
// Only check every 10 ticks for performance.
if ((this.mg.ticks() + this.checkOffset) % 10 != 0) {
if ((this.mg.ticks() + this.checkOffset) % 10 !== 0) {
return;
}
@@ -71,7 +74,7 @@ export class PortExecution implements Execution {
const ports = this.player().tradingPorts(this.port);
if (ports.length == 0) {
if (ports.length === 0) {
return;
}
@@ -91,6 +94,9 @@ export class PortExecution implements Execution {
}
player(): Player {
if (this.port === null) {
throw new Error("Not initialized");
}
return this.port.owner();
}
}
+32 -18
View File
@@ -23,14 +23,14 @@ export class SAMLauncherExecution implements Execution {
private MIRVWarheadSearchRadius = 400;
private MIRVWarheadProtectionRadius = 50;
private pseudoRandom: PseudoRandom;
private pseudoRandom: PseudoRandom | undefined;
constructor(
private ownerId: PlayerID,
private tile: TileRef,
private tile: TileRef | null,
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();
}
@@ -92,9 +93,15 @@ export class SAMLauncherExecution implements Execution {
}
tick(ticks: number): void {
if (this.sam == null) {
if (this.mg === null || this.player === null) {
throw new Error("Not initialized");
}
if (this.sam === null) {
if (this.tile === null) {
throw new Error("tile is null");
}
const spawnTile = this.player.canBuild(UnitType.SAMLauncher, this.tile);
if (spawnTile == false) {
if (spawnTile === false) {
consolex.warn("cannot build SAM Launcher");
this.active = false;
return;
@@ -108,11 +115,11 @@ export class SAMLauncherExecution implements Execution {
return;
}
if (this.player != this.sam.owner()) {
if (this.player !== this.sam.owner()) {
this.player = this.sam.owner();
}
if (!this.pseudoRandom) {
if (this.pseudoRandom === undefined) {
this.pseudoRandom = new PseudoRandom(this.sam.id());
}
@@ -127,20 +134,24 @@ 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();
}
if (
this.sam.isCooldown() &&
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) == 0
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) === 0
) {
this.sam.setCooldown(false);
}
@@ -152,7 +163,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) {
@@ -171,7 +183,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(
@@ -181,6 +193,8 @@ export class SAMLauncherExecution implements Execution {
target,
),
);
} else {
throw new Error("target is null");
}
}
}
+3 -3
View File
@@ -13,7 +13,7 @@ import { PseudoRandom } from "../PseudoRandom";
export class SAMMissileExecution implements Execution {
private active = true;
private pathFinder: AirPathFinder;
private SAMMissile: Unit;
private SAMMissile: Unit | undefined;
private mg: Game;
constructor(
@@ -30,7 +30,7 @@ export class SAMMissileExecution implements Execution {
}
tick(ticks: number): void {
if (this.SAMMissile == null) {
if (this.SAMMissile === undefined) {
this.SAMMissile = this._owner.buildUnit(
UnitType.SAMMissile,
this.spawn,
@@ -46,7 +46,7 @@ export class SAMMissileExecution implements Execution {
if (
!this.target.isActive() ||
!this.ownerUnit.isActive() ||
this.target.owner() == this.SAMMissile.owner() ||
this.target.owner() === this.SAMMissile.owner() ||
!nukesWhitelist.includes(this.target.type())
) {
this.SAMMissile.delete(false);
@@ -31,10 +31,6 @@ export class SetTargetTroopRatioExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+7 -7
View File
@@ -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()) {
@@ -32,15 +32,15 @@ export class ShellExecution implements Execution {
}
if (
!this.target.isActive() ||
this.target.owner() == this.shell.owner() ||
(this.destroyAtTick != -1 && this.mg.ticks() >= this.destroyAtTick)
this.target.owner() === this.shell.owner() ||
(this.destroyAtTick !== -1 && this.mg.ticks() >= this.destroyAtTick)
) {
this.shell.delete(false);
this.active = false;
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 {
+2 -5
View File
@@ -25,7 +25,7 @@ export class SpawnExecution implements Execution {
return;
}
let player: Player = null;
let player: Player | null = null;
if (this.mg.hasPlayer(this.playerInfo.id)) {
player = this.mg.player(this.playerInfo.id);
} else {
@@ -39,16 +39,13 @@ export class SpawnExecution implements Execution {
if (!player.hasSpawned()) {
this.mg.addExecution(new PlayerExecution(player.id()));
if (player.type() == PlayerType.Bot) {
if (player.type() === PlayerType.Bot) {
this.mg.addExecution(new BotExecution(player));
}
}
player.setHasSpawned(true);
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -37,10 +37,6 @@ export class TargetPlayerExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+15 -8
View File
@@ -16,9 +16,9 @@ import { distSortUnit } from "../Util";
export class TradeShipExecution implements Execution {
private active = true;
private mg: Game;
private origOwner: Player;
private tradeShip: Unit;
private mg: Game | null = null;
private origOwner: Player | null = null;
private tradeShip: Unit | null = null;
private index = 0;
private wasCaptured = false;
private tilesTraveled = 0;
@@ -36,12 +36,15 @@ export class TradeShipExecution implements Execution {
}
tick(ticks: number): void {
if (this.tradeShip == null) {
if (this.mg === null || this.origOwner === null) {
throw new Error("Not initialized");
}
if (this.tradeShip === null) {
const spawn = this.origOwner.canBuild(
UnitType.TradeShip,
this.srcPort.tile(),
);
if (spawn == false) {
if (spawn === false) {
consolex.warn(`cannot build trade ship`);
this.active = false;
return;
@@ -57,14 +60,14 @@ 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;
}
// If a player captures another player's port while trading we should delete
// the ship.
if (this._dstPort.owner().id() == this.srcPort.owner().id()) {
if (this._dstPort.owner().id() === this.srcPort.owner().id()) {
this.tradeShip.delete(false);
this.active = false;
return;
@@ -85,7 +88,7 @@ export class TradeShipExecution implements Execution {
.owner()
.units(UnitType.Port)
.sort(distSortUnit(this.mg, this.tradeShip));
if (ports.length == 0) {
if (ports.length === 0) {
this.tradeShip.delete(false);
this.active = false;
return;
@@ -127,6 +130,10 @@ export class TradeShipExecution implements Execution {
}
private complete() {
if (this.mg === null || this.origOwner === null) {
throw new Error("Not initialized");
}
if (this.tradeShip === null) return;
this.active = false;
this.tradeShip.delete(false);
const gold = this.mg.config().tradeShipGold(this.tilesTraveled);
+16 -9
View File
@@ -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,
) {}
@@ -55,7 +55,7 @@ export class TransportShipExecution implements Execution {
this.active = false;
return;
}
if (this.targetID != null && !mg.hasPlayer(this.targetID)) {
if (this.targetID !== null && !mg.hasPlayer(this.targetID)) {
console.warn(`TransportShipExecution: target ${this.targetID} not found`);
this.active = false;
return;
@@ -81,13 +81,16 @@ export class TransportShipExecution implements Execution {
return;
}
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
if (
this.targetID === null ||
this.targetID === this.mg.terraNullius().id()
) {
this.target = mg.terraNullius();
} else {
this.target = mg.player(this.targetID);
}
if (this.troops == null) {
if (this.troops === null) {
this.troops = this.mg
.config()
.boatAttackAmount(this.attacker, this.target);
@@ -96,7 +99,7 @@ export class TransportShipExecution implements Execution {
this.troops = Math.min(this.troops, this.attacker.troops());
this.dst = targetTransportTile(this.mg, this.ref);
if (this.dst == null) {
if (this.dst === null) {
consolex.warn(
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
);
@@ -108,19 +111,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(
@@ -146,6 +149,10 @@ export class TransportShipExecution implements Execution {
}
tick(ticks: number) {
if (this.dst === null) {
this.active = false;
return;
}
if (!this.active) {
return;
}
@@ -161,7 +168,7 @@ export class TransportShipExecution implements Execution {
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
switch (result.type) {
case PathFindResultType.Completed:
if (this.mg.owner(this.dst) == this.attacker) {
if (this.mg.owner(this.dst) === this.attacker) {
this.attacker.addTroops(this.troops);
this.boat.delete(false);
this.active = false;
+2 -2
View File
@@ -10,11 +10,11 @@ export function closestTwoTiles(
gm: GameMap,
x: Iterable<TileRef>,
y: Iterable<TileRef>,
): { x: TileRef; y: TileRef } {
): { x: TileRef; y: TileRef } | null {
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
if (xSorted.length == 0 || ySorted.length == 0) {
if (xSorted.length === 0 || ySorted.length === 0) {
return null;
}
+42 -21
View File
@@ -18,13 +18,13 @@ export class WarshipExecution implements Execution {
private _owner: Player;
private active = true;
private warship: Unit = null;
private mg: Game = null;
private warship: Unit | null = null;
private mg: Game | null = null;
private target: Unit = null;
private pathfinder: PathFinder;
private target: Unit | null = null;
private pathfinder: PathFinder | null = null;
private patrolTile: TileRef;
private patrolTile: TileRef | null = null;
private lastShellAttack = 0;
private alreadySentShell = new Set<Unit>();
@@ -48,7 +48,10 @@ export class WarshipExecution implements Execution {
}
// Only for warships with "moveTarget" set
goToMoveTarget(target: TileRef): boolean {
goToMoveTarget(target: TileRef) {
if (this.warship === null || this.pathfinder === null) {
throw new Error("Warship not initialized");
}
// Patrol unless we are hunting down a tradeship
const result = this.pathfinder.nextTile(this.warship.tile(), target);
switch (result.type) {
@@ -69,6 +72,9 @@ export class WarshipExecution implements Execution {
}
private shoot() {
if (this.mg === null || this.warship === null || this.target === null) {
throw new Error("Warship not initialized");
}
const shellAttackRate = this.mg.config().warshipShellAttackRate();
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
this.lastShellAttack = this.mg.ticks();
@@ -90,8 +96,12 @@ export class WarshipExecution implements Execution {
}
private patrol() {
if (this.warship === null || this.pathfinder === null) {
throw new Error("Warship not initialized");
}
if (this.patrolTile === null) return;
this.warship.setWarshipTarget(this.target);
if (this.target == null || this.target.type() != UnitType.TradeShip) {
if (this.target === null || this.target.type() !== UnitType.TradeShip) {
// Patrol unless we are hunting down a tradeship
const result = this.pathfinder.nextTile(
this.warship.tile(),
@@ -117,9 +127,11 @@ export class WarshipExecution implements Execution {
}
tick(ticks: number): void {
if (this.warship == null) {
if (this.pathfinder === null) throw new Error("Warship not initialized");
if (this.warship === null) {
if (this.patrolTile === null) return;
const spawn = this._owner.canBuild(UnitType.Warship, this.patrolTile);
if (spawn == false) {
if (spawn === false) {
this.active = false;
return;
}
@@ -130,10 +142,13 @@ export class WarshipExecution implements Execution {
this.active = false;
return;
}
if (this.target != null && !this.target.isActive()) {
if (this.target !== null && !this.target.isActive()) {
this.target = null;
}
const hasPort = this._owner.units(UnitType.Port).length > 0;
if (this.mg === null) throw new Error("Game not initialized");
const warship = this.warship;
if (warship === null) throw new Error("Warship not initialized");
const ships = this.mg
.nearbyUnits(
this.warship.tile(),
@@ -142,12 +157,13 @@ export class WarshipExecution implements Execution {
)
.filter(
({ unit }) =>
unit.owner() !== this.warship.owner() &&
unit !== this.warship &&
!unit.owner().isFriendly(this.warship.owner()) &&
unit.owner() !== warship.owner() &&
unit !== warship &&
!unit.owner().isFriendly(warship.owner()) &&
!this.alreadySentShell.has(unit) &&
(unit.type() !== UnitType.TradeShip ||
(hasPort &&
this.warship !== null &&
unit.dstPort()?.owner() !== this.warship.owner() &&
!unit.dstPort()?.owner().isFriendly(this.warship.owner()) &&
unit.isSafeFromPirates() !== true)),
@@ -186,22 +202,23 @@ export class WarshipExecution implements Execution {
return distA - distB;
})[0]?.unit ?? null;
if (this.warship.moveTarget()) {
this.goToMoveTarget(this.warship.moveTarget());
const moveTarget = this.warship.moveTarget();
if (moveTarget) {
this.goToMoveTarget(moveTarget);
// If we have a "move target" then we cannot target trade ships as it
// requires moving.
if (this.target && this.target.type() == UnitType.TradeShip) {
if (this.target && this.target.type() === UnitType.TradeShip) {
this.target = null;
}
} else if (!this.target || this.target.type() != UnitType.TradeShip) {
} else if (!this.target || this.target.type() !== UnitType.TradeShip) {
this.patrol();
}
if (
this.target == null ||
this.target === null ||
!this.target.isActive() ||
this.target.owner() == this._owner ||
this.target.isSafeFromPirates() == true
this.target.owner() === this._owner ||
this.target.isSafeFromPirates() === true
) {
// In case another warship captured or destroyed target, or the target escaped into safe waters
this.target = null;
@@ -215,7 +232,7 @@ export class WarshipExecution implements Execution {
return;
}
if (this.target.type() != UnitType.TradeShip) {
if (this.target.type() !== UnitType.TradeShip) {
this.shoot();
return;
}
@@ -255,6 +272,9 @@ export class WarshipExecution implements Execution {
}
randomTile(): TileRef {
if (this.mg === null) {
throw new Error("Warship not initialized");
}
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
const maxAttemptBeforeExpand: number = warshipPatrolRange * 2;
let attemptCount: number = 0;
@@ -282,5 +302,6 @@ export class WarshipExecution implements Execution {
}
return tile;
}
throw new Error("unreachable");
}
}
+14 -11
View File
@@ -15,7 +15,7 @@ export class WinEvent implements GameEvent {
export class WinCheckExecution implements Execution {
private active = true;
private mg: Game;
private mg: Game | null = null;
constructor() {}
@@ -24,10 +24,11 @@ export class WinCheckExecution implements Execution {
}
tick(ticks: number) {
if (ticks % 10 != 0) {
if (ticks % 10 !== 0) {
return;
}
if (this.mg.config().gameConfig().gameMode == GameMode.FFA) {
if (this.mg === null) throw new Error("Not initialized");
if (this.mg.config().gameConfig().gameMode === GameMode.FFA) {
this.checkWinnerFFA();
} else {
this.checkWinnerTeam();
@@ -35,10 +36,11 @@ export class WinCheckExecution implements Execution {
}
checkWinnerFFA(): void {
if (this.mg === null) throw new Error("Not initialized");
const sorted = this.mg
.players()
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
if (sorted.length == 0) {
if (sorted.length === 0) {
return;
}
const max = sorted[0];
@@ -55,17 +57,21 @@ export class WinCheckExecution implements Execution {
}
checkWinnerTeam(): void {
if (this.mg === null) throw new Error("Not initialized");
const teamToTiles = new Map<Team, number>();
for (const player of this.mg.players()) {
const team = player.team();
// Sanity check, team should not be null here
if (team === null) continue;
teamToTiles.set(
player.team(),
(teamToTiles.get(player.team()) ?? 0) + player.numTilesOwned(),
team,
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
);
}
const sorted = Array.from(teamToTiles.entries()).sort(
(a, b) => b[1] - a[1],
);
if (sorted.length == 0) {
if (sorted.length === 0) {
return;
}
const max = sorted[0];
@@ -73,15 +79,12 @@ 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;
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
@@ -3,9 +3,8 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class AllianceRequestExecution implements Execution {
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
private requestor: Player | null = null;
private recipient: Player | null = null;
constructor(
private requestorID: PlayerID,
@@ -28,12 +27,14 @@ export class AllianceRequestExecution implements Execution {
return;
}
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
tick(ticks: number): void {
if (this.requestor === null || this.recipient === null) {
throw new Error("Not initialized");
}
if (this.requestor.isFriendly(this.recipient)) {
consolex.warn("already allied");
} else if (!this.requestor.canSendAllianceRequest(this.recipient)) {
@@ -44,10 +45,6 @@ export class AllianceRequestExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -3,9 +3,8 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class AllianceRequestReplyExecution implements Execution {
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
private requestor: Player | null = null;
private recipient: Player | null = null;
constructor(
private requestorID: PlayerID,
@@ -28,19 +27,21 @@ export class AllianceRequestReplyExecution implements Execution {
this.active = false;
return;
}
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
tick(ticks: number): void {
if (this.requestor === null || this.recipient === null) {
throw new Error("Not initialized");
}
if (this.requestor.isFriendly(this.recipient)) {
consolex.warn("already allied");
} else {
const request = this.requestor
.outgoingAllianceRequests()
.find((ar) => ar.recipient() == this.recipient);
if (request == null) {
.find((ar) => ar.recipient() === this.recipient);
if (request === undefined) {
consolex.warn("no alliance request found");
} else {
if (this.accept) {
@@ -55,10 +56,6 @@ export class AllianceRequestReplyExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
@@ -3,9 +3,9 @@ import { Execution, Game, Player, PlayerID } from "../../game/Game";
export class BreakAllianceExecution implements Execution {
private active = true;
private requestor: Player;
private recipient: Player;
private mg: Game;
private requestor: Player | null = null;
private recipient: Player | null = null;
private mg: Game | null = null;
constructor(
private requestorID: PlayerID,
@@ -33,14 +33,21 @@ export class BreakAllianceExecution implements Execution {
}
tick(ticks: number): void {
if (
this.mg === null ||
this.requestor === null ||
this.recipient === null
) {
throw new Error("Not initialized");
}
const alliance = this.requestor.allianceWith(this.recipient);
if (alliance == null) {
if (alliance === null) {
consolex.warn("cant break alliance, not allied");
} else {
this.requestor.breakAlliance(alliance);
this.recipient.updateRelation(this.requestor, -200);
for (const player of this.mg.players()) {
if (player != this.requestor) {
if (player !== this.requestor) {
player.updateRelation(this.requestor, -40);
}
}
@@ -48,10 +55,6 @@ export class BreakAllianceExecution implements Execution {
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
+3 -3
View File
@@ -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;
}