This commit is contained in:
Scott Anderson
2025-05-14 00:36:11 -04:00
parent 2b4d28bb34
commit 9a7d6283f8
17 changed files with 104 additions and 73 deletions
+3 -3
View File
@@ -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({
+1 -1
View File
@@ -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:
+1 -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;
+5 -3
View File
@@ -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<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();
@@ -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 {
+8 -5
View File
@@ -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);
}
+3 -2
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";
@@ -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(
+28 -16
View File
@@ -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;
+4 -1
View File
@@ -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(
+1 -1
View File
@@ -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,
+19 -11
View File
@@ -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");
}
}
}
+5 -5
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()) {
@@ -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 {
+1 -1
View File
@@ -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;
}
+4 -4
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,
) {}
@@ -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(
+1 -1
View File
@@ -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;
+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;
}
+1
View File
@@ -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);
+16 -15
View File
@@ -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;