From d173e22b6b12dad3f0ff02b55cc309c8b1c472a3 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 1 Dec 2024 21:31:01 -0800 Subject: [PATCH 01/10] defense post --- TODO.txt | 2 ++ src/client/graphics/layers/TerritoryLayer.ts | 9 ++++++++- src/core/game/GameImpl.ts | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index a459bba7a..7b411b5a7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -202,6 +202,8 @@ * add info view on top right DONE 11/30/2024 * add info view for units DONE 11/30/2024 * add defense post +* bugfix: destroyers can't find path to dest and freeze +* bugfix: when trade ships captured don't render * use mini A* for all pathfinding * record single player game stats * add radiation from nuke diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 6dbe4128c..1b5a7f229 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -120,7 +120,14 @@ export class TerritoryLayer implements Layer { unitEvent(event: UnitEvent) { if (event.unit.type() == UnitType.DefensePost) { - bfs(event.unit.tile(), dist(event.unit.tile(), this.game.config().defensePostRange())).forEach(t => this.enqueue(t)) + bfs( + event.unit.tile(), + dist(event.unit.tile(), this.game.config().defensePostRange()) + ).forEach(t => { + if (t.isBorder()) { + this.enqueue(t) + } + }) } } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index fd80ca762..19ac3e822 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -68,14 +68,14 @@ export class GameImpl implements MutableGame { addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { const df = { unit: unit, tile: tile, amount: amount }; (tile as TileImpl)._defenseBonuses.push(df) - this.eventBus.emit(new TileEvent(tile)) + // this.eventBus.emit(new TileEvent(tile)) return df } removeTileDefenseBonus(bonus: DefenseBonus): void { const t = bonus.tile as TileImpl t._defenseBonuses = t._defenseBonuses.filter(db => db != bonus) - this.eventBus.emit(new TileEvent(bonus.tile)) + // this.eventBus.emit(new TileEvent(bonus.tile)) } units(...types: UnitType[]): UnitImpl[] { From 330b6b93cb199022f43ebb826aadd3dcd2547acb Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 2 Dec 2024 07:51:01 -0800 Subject: [PATCH 02/10] finish defense post --- TODO.txt | 8 +++++++- src/client/graphics/layers/TerritoryLayer.ts | 2 +- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 2 +- src/core/configuration/DevConfig.ts | 6 +++--- src/core/configuration/PastelTheme.ts | 9 +++++++++ src/core/game/GameImpl.ts | 4 ++-- src/core/game/TileImpl.ts | 4 ++++ src/core/pathfinding/AStar.ts | 3 ++- src/core/pathfinding/MiniAStar.ts | 2 +- 10 files changed, 31 insertions(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index 7b411b5a7..bf2a93a70 100644 --- a/TODO.txt +++ b/TODO.txt @@ -202,12 +202,18 @@ * add info view on top right DONE 11/30/2024 * add info view for units DONE 11/30/2024 * add defense post -* bugfix: destroyers can't find path to dest and freeze +* bugfix: when thread doesn't complete computation, do it in main +* bugfix: destroyers can't find path to dst and freeze * bugfix: when trade ships captured don't render +* bugfix: gameStop not found error * use mini A* for all pathfinding +* log stack traces & display them on screen +* record and replay games for debugging purposes * record single player game stats * add radiation from nuke * add cities +* create behavior tests +* create perf test * create alternate view to show friendly & enemy units * spread out calculate clusters * NPC has relations diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 1b5a7f229..7a2edac37 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -84,7 +84,7 @@ export class TerritoryLayer implements Layer { if (tile.defenseBonuses().filter(db => db.unit.owner() == owner).length > 0) { this.paintCell( tile.cell(), - colord({ r: 0, g: 0, b: 0 }), + this.theme.defendedBorderColor(owner.info()), 255 ) } else { diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 99a39cfe2..88cabcffd 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -68,6 +68,7 @@ export interface Theme { playerInfoColor(id: PlayerID): Colord; territoryColor(playerInfo: PlayerInfo): Colord; borderColor(playerInfo: PlayerInfo): Colord; + defendedBorderColor(playerInfo: PlayerInfo): Colord; terrainColor(tile: Tile): Colord; backgroundColor(): Colord; font(): string; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 085329574..24d81ac72 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -8,7 +8,7 @@ import { pastelTheme } from "./PastelTheme"; export class DefaultConfig implements Config { defensePostRange(): number { - return 20 + return 30 } defensePostDefenseBonus(): number { return 3 diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 77e7d0344..d72b57aa3 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -5,7 +5,7 @@ export const devConfig = new class extends DefaultConfig { unitInfo(type: UnitType): UnitInfo { const info = super.unitInfo(type) const oldCost = info.cost - info.cost = (p: Player) => oldCost(p) / 1000 + info.cost = (p: Player) => oldCost(p) / 100000 return info } @@ -13,8 +13,8 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - // return 40 - return 100 + return 40 + // return 100 } gameCreationRate(): number { return 10 * 1000 diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 17382567b..37420f38e 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -6,6 +6,7 @@ import {PseudoRandom} from "../PseudoRandom"; import {simpleHash} from "../Util"; export const pastelTheme = new class implements Theme { + private rand = new PseudoRandom(123) private background = colord({r: 60, g: 60, b: 60}); @@ -131,6 +132,14 @@ export const pastelTheme = new class implements Theme { b: Math.max(tc.b - 40, 0) }) } + defendedBorderColor(playerInfo: PlayerInfo): Colord { + const bc = this.borderColor(playerInfo).rgba; + return colord({ + r: Math.max(bc.r - 40, 0), + g: Math.max(bc.g - 40, 0), + b: Math.max(bc.b - 40, 0) + }) + } terrainColor(tile: Tile): Colord { let mag = tile.magnitude() diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 19ac3e822..fd80ca762 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -68,14 +68,14 @@ export class GameImpl implements MutableGame { addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { const df = { unit: unit, tile: tile, amount: amount }; (tile as TileImpl)._defenseBonuses.push(df) - // this.eventBus.emit(new TileEvent(tile)) + this.eventBus.emit(new TileEvent(tile)) return df } removeTileDefenseBonus(bonus: DefenseBonus): void { const t = bonus.tile as TileImpl t._defenseBonuses = t._defenseBonuses.filter(db => db != bonus) - // this.eventBus.emit(new TileEvent(bonus.tile)) + this.eventBus.emit(new TileEvent(bonus.tile)) } units(...types: UnitType[]): UnitImpl[] { diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index 17e5c4142..b88923b22 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -20,6 +20,10 @@ export class TileImpl implements Tile { private readonly _terrain: TerrainTileImpl ) { } + terrainType(): TerrainType { + return this._terrain.type + } + defenseBonus(player: Player): number { if (this.owner() == player) { throw Error(`cannot get defense bonus of tile already owned by player`) diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts index b00970239..8863be5bd 100644 --- a/src/core/pathfinding/AStar.ts +++ b/src/core/pathfinding/AStar.ts @@ -1,4 +1,4 @@ -import { Cell, Tile } from "../game/Game"; +import { Cell, TerrainType, Tile } from "../game/Game"; export interface AStar { compute(): PathFindResultType @@ -26,6 +26,7 @@ export interface SearchNode { cost(): number cell(): Cell neighbors(): SearchNode[] + terrainType(): TerrainType } export interface Point { x: number; diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts index 1722f5cef..4cda90a52 100644 --- a/src/core/pathfinding/MiniAStar.ts +++ b/src/core/pathfinding/MiniAStar.ts @@ -21,7 +21,7 @@ export class MiniAStar implements AStar { this.aStar = new SerialAStar( miniSrc, miniDst, - (t => (t as TerrainTile).terrainType() == TerrainType.Ocean), + canMove, iterations, maxTries ) From 4edd66dc1fdcfc135983945fbefedff9c137ef46 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Dec 2024 14:35:09 -0800 Subject: [PATCH 03/10] AStar returns cell instead of path --- src/core/execution/BattleshipExecution.ts | 3 +- src/core/execution/DestroyerExecution.ts | 5 +-- src/core/execution/NukeExecution.ts | 3 +- src/core/execution/PortExecution.ts | 4 +-- src/core/execution/ShellExecution.ts | 3 +- src/core/execution/TransportShipExecution.ts | 3 +- src/core/pathfinding/AStar.ts | 2 +- src/core/pathfinding/MiniAStar.ts | 31 +++++++----------- src/core/pathfinding/PathFinding.ts | 16 ++++++---- src/core/pathfinding/SerialAStar.ts | 5 +-- src/core/worker/Worker.worker.ts | 33 +++++++++----------- src/core/worker/WorkerClient.ts | 8 ++--- 12 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index 7699d94b8..fcd0d7443 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -14,7 +14,7 @@ export class BattleshipExecution implements Execution { private battleship: MutableUnit = null private mg: MutableGame = null - private pathfinder = PathFinder.Serial(5000, t => t.isWater()) + private pathfinder: PathFinder private patrolTile: Tile; private patrolCenterTile: Tile @@ -31,6 +31,7 @@ export class BattleshipExecution implements Execution { init(mg: MutableGame, ticks: number): void { + this.pathfinder = PathFinder.Serial(mg, 5000, t => t.isWater()) this._owner = mg.player(this.playerID) this.mg = mg this.patrolCenterTile = mg.tile(this.cell) diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 373189bfe..cd2605e13 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { SerialAStar } from "../pathfinding/SerialAStar"; @@ -14,7 +14,7 @@ export class DestroyerExecution implements Execution { private mg: MutableGame = null private target: MutableUnit = null - private pathfinder = PathFinder.Serial(5000, t => t.isWater()) + private pathfinder: PathFinder private patrolTile: Tile; private patrolCenterTile: Tile @@ -29,6 +29,7 @@ export class DestroyerExecution implements Execution { init(mg: MutableGame, ticks: number): void { + this.pathfinder = PathFinder.Serial(mg, 5000, t => t.terrainType() == TerrainType.Ocean) this._owner = mg.player(this.playerID) this.mg = mg this.patrolCenterTile = mg.tile(this.cell) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index df4570c78..8e819350b 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -16,7 +16,7 @@ export class NukeExecution implements Execution { private nuke: MutableUnit private dst: Tile - private pathFinder: PathFinder = PathFinder.Serial(10_000, () => true) + private pathFinder: PathFinder constructor( private type: UnitType.AtomBomb | UnitType.HydrogenBomb, private senderID: PlayerID, @@ -26,6 +26,7 @@ export class NukeExecution implements Execution { init(mg: MutableGame, ticks: number): void { this.mg = mg + this.pathFinder = PathFinder.Serial(mg, 10_000, () => true) this.player = mg.player(this.senderID) this.dst = this.mg.tile(this.cell) } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 64d1589d2..df1d7ab67 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -72,7 +72,7 @@ export class PortExecution implements Execution { const aStar = this.computingPaths.get(port) switch (aStar.compute()) { case PathFindResultType.Completed: - this.portPaths.set(port, aStar.reconstructPath().map(sn => sn as Tile)) + this.portPaths.set(port, aStar.reconstructPath().map(cell => this.mg.tile(cell))) this.computingPaths.delete(port) break case PathFindResultType.Pending: @@ -101,7 +101,7 @@ export class PortExecution implements Execution { const port = this.random.randElement(portConnections) const path = this.portPaths.get(port) if (path != null) { - const pf = PathFinder.Parallel(this.worker, 30) + const pf = PathFinder.Parallel(this.mg, this.worker, 30) this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path)) } } diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 7d92e5169..0be6cf368 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -5,7 +5,7 @@ import { PathFindResultType } from "../pathfinding/AStar"; export class ShellExecution implements Execution { private active = true - private pathFinder = PathFinder.Serial(2000, () => true, 10) + private pathFinder: PathFinder private shell: MutableUnit constructor(private spawn: Tile, private _owner: MutablePlayer, private target: MutableUnit) { @@ -13,6 +13,7 @@ export class ShellExecution implements Execution { } init(mg: MutableGame, ticks: number): void { + this.pathFinder = PathFinder.Serial(mg, 2000, () => true, 10) } tick(ticks: number): void { diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 3126b8180..6c7c0ff40 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -27,7 +27,7 @@ export class TransportShipExecution implements Execution { private boat: MutableUnit - private pathFinder: PathFinder = PathFinder.Serial(10_000, t => t.isWater(), 2) + private pathFinder: PathFinder constructor( private attackerID: PlayerID, @@ -43,6 +43,7 @@ export class TransportShipExecution implements Execution { init(mg: MutableGame, ticks: number) { this.lastMove = ticks this.mg = mg + this.pathFinder = PathFinder.Serial(mg, 10_000, t => t.isWater(), 2) this.attacker = mg.player(this.attackerID) diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts index 8863be5bd..e7a1188e4 100644 --- a/src/core/pathfinding/AStar.ts +++ b/src/core/pathfinding/AStar.ts @@ -2,7 +2,7 @@ import { Cell, TerrainType, Tile } from "../game/Game"; export interface AStar { compute(): PathFindResultType - reconstructPath(): SearchNode[] + reconstructPath(): Cell[] } export enum PathFindResultType { diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts index 4cda90a52..e71795869 100644 --- a/src/core/pathfinding/MiniAStar.ts +++ b/src/core/pathfinding/MiniAStar.ts @@ -31,29 +31,22 @@ export class MiniAStar implements AStar { return this.aStar.compute() } - reconstructPath(): SearchNode[] { + reconstructPath(): Cell[] { const upscaled = upscalePath(this.aStar.reconstructPath()) - .map(p => this.terrainMap.terrain(new Cell(p.x, p.y))) as SearchNode[] - upscaled.push(this.dst) - return upscaled - } - - reconstructPathAsPoints(): Point[] { - const upscaled = upscalePath(this.aStar.reconstructPath()) - upscaled.push({ x: this.dst.cell().x, y: this.dst.cell().y }) + upscaled.push(this.dst.cell()) return upscaled } } -function upscalePath(path: SearchNode[], scaleFactor: number = 2): Point[] { +function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] { // Scale up each point - const scaledPath = path.map(point => ({ - x: point.cell().x * scaleFactor, - y: point.cell().y * scaleFactor - })); + const scaledPath = path.map(point => (new Cell( + point.x * scaleFactor, + point.y * scaleFactor + ))); - const smoothPath: Point[] = []; + const smoothPath: Cell[] = []; for (let i = 0; i < scaledPath.length - 1; i++) { const current = scaledPath[i]; @@ -72,10 +65,10 @@ function upscalePath(path: SearchNode[], scaleFactor: number = 2): Point[] { // Add intermediate points for (let step = 1; step < steps; step++) { - smoothPath.push({ - x: Math.round(current.x + (dx * step) / steps), - y: Math.round(current.y + (dy * step) / steps) - }); + smoothPath.push(new Cell( + Math.round(current.x + (dx * step) / steps), + Math.round(current.y + (dy * step) / steps) + )); } } diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 968cbf9cc..4da1cb623 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -1,4 +1,4 @@ -import { Game, Tile } from "../game/Game"; +import { Cell, Game, Tile } from "../game/Game"; import { manhattanDist } from "../Util"; import { AStar, PathFindResultType, TileResult } from "./AStar"; import { ParallelAStar, WorkerClient } from "../worker/WorkerClient"; @@ -9,17 +9,19 @@ export class PathFinder { private curr: Tile = null private dst: Tile = null - private path: Tile[] + private path: Cell[] private aStar: AStar private computeFinished = true private constructor( + private game: Game, private newAStar: (curr: Tile, dst: Tile) => AStar ) { } public static Mini(game: Game, iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20) { return new PathFinder( + game, (curr: Tile, dst: Tile) => { return new MiniAStar( game.terrainMap(), @@ -34,8 +36,9 @@ export class PathFinder { ) } - public static Serial(iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20): PathFinder { + public static Serial(game: Game, iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20): PathFinder { return new PathFinder( + game, (curr: Tile, dst: Tile) => { return new SerialAStar( curr, @@ -48,8 +51,9 @@ export class PathFinder { ) } - public static Parallel(worker: WorkerClient, numTicks: number): PathFinder { + public static Parallel(game: Game, worker: WorkerClient, numTicks: number): PathFinder { return new PathFinder( + game, (curr: Tile, dst: Tile) => { return worker.createParallelAStar(curr, dst, numTicks) } @@ -77,14 +81,14 @@ export class PathFinder { this.computeFinished = false return this.nextTile(curr, dst) } else { - return { type: PathFindResultType.NextTile, tile: this.path.shift() } + return { type: PathFindResultType.NextTile, tile: this.game.tile(this.path.shift()) } } } switch (this.aStar.compute()) { case PathFindResultType.Completed: this.computeFinished = true - this.path = this.aStar.reconstructPath() as Tile[] + this.path = this.aStar.reconstructPath() // Remove the start tile this.path.shift() return this.nextTile(curr, dst) diff --git a/src/core/pathfinding/SerialAStar.ts b/src/core/pathfinding/SerialAStar.ts index 266e6df53..c35ad8423 100644 --- a/src/core/pathfinding/SerialAStar.ts +++ b/src/core/pathfinding/SerialAStar.ts @@ -1,6 +1,7 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { AStar, SearchNode } from "./AStar"; import { PathFindResultType } from "./AStar"; +import { Cell } from "../game/Game"; export class SerialAStar implements AStar { @@ -114,7 +115,7 @@ export class SerialAStar implements AStar { } } - public reconstructPath(): SearchNode[] { + public reconstructPath(): Cell[] { if (!this.meetingPoint) return []; // Reconstruct path from start to meeting point @@ -132,6 +133,6 @@ export class SerialAStar implements AStar { fwdPath.push(current); } - return fwdPath; + return fwdPath.map(sn => sn.cell()); } } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index ce87b0c8e..cb8504cbe 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -10,16 +10,11 @@ let searches = new PriorityQueue((a: Search, b: Search) => (a.deadline - let processingInterval: number | null = null; let isProcessingSearch = false -interface Point { - x: number - y: number -} - interface Search { aStar: SerialAStar, deadline: number requestId: string, - end: Point + end: Cell } interface SearchRequest { @@ -27,8 +22,8 @@ interface SearchRequest { currentTick: number // duration in ticks duration: number - start: Point - end: Point + start: Cell + end: Cell } self.onmessage = (e) => { @@ -81,7 +76,7 @@ function computeSearches() { const search = searches.dequeue() switch (search.aStar.compute()) { case PathFindResultType.Completed: - const path = upscalePath(search.aStar.reconstructPath().map(sn => ({ x: sn.cell().x, y: sn.cell().y }))) + const path = upscalePath(search.aStar.reconstructPath()) path.push(search.end) self.postMessage({ type: 'pathFound', @@ -107,14 +102,14 @@ function computeSearches() { } } -function upscalePath(path: Point[], scaleFactor: number = 2): Point[] { +function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] { // Scale up each point - const scaledPath = path.map(point => ({ - x: point.x * scaleFactor, - y: point.y * scaleFactor - })); + const scaledPath = path.map(point => (new Cell( + point.x * scaleFactor, + point.y * scaleFactor + ))); - const smoothPath: Point[] = []; + const smoothPath: Cell[] = []; for (let i = 0; i < scaledPath.length - 1; i++) { const current = scaledPath[i]; @@ -133,10 +128,10 @@ function upscalePath(path: Point[], scaleFactor: number = 2): Point[] { // Add intermediate points for (let step = 1; step < steps; step++) { - smoothPath.push({ - x: Math.round(current.x + (dx * step) / steps), - y: Math.round(current.y + (dy * step) / steps) - }); + smoothPath.push(new Cell( + Math.round(current.x + (dx * step) / steps), + Math.round(current.y + (dy * step) / steps) + )); } } diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 9f4766a39..5f28ea4b2 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -46,7 +46,7 @@ export class WorkerClient { } } export class ParallelAStar implements AStar { - private path: Tile[] | 'NOT_FOUND' | null = null; + private path: Cell[] | 'NOT_FOUND' | null = null; private promise: Promise; constructor( @@ -72,7 +72,7 @@ export class ParallelAStar implements AStar { this.worker.removeEventListener('message', handler); if (e.data.type === 'pathFound') { - this.path = e.data.path.map(pos => this.game.tile(new Cell(pos.x, pos.y))); + this.path = e.data.path resolve(); } else if (e.data.type === 'pathNotFound') { this.path = 'NOT_FOUND'; @@ -113,11 +113,11 @@ export class ParallelAStar implements AStar { return PathFindResultType.Pending; } - reconstructPath(): Tile[] { + reconstructPath(): Cell[] { if (this.path == "NOT_FOUND" || this.path == null) { throw Error(`cannot reconstruct path: ${this.path}`); } - return this.path as Tile[]; + return this.path } } From e6fa108eaa5f591a93f94606a8e74bbeb85f1e19 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Dec 2024 15:06:55 -0800 Subject: [PATCH 04/10] use mini a star for all pathfinding --- src/core/execution/BattleshipExecution.ts | 4 ++-- src/core/execution/DestroyerExecution.ts | 2 +- src/core/execution/NukeExecution.ts | 2 +- src/core/execution/ShellExecution.ts | 2 +- src/core/execution/TransportShipExecution.ts | 4 ++-- src/core/pathfinding/MiniAStar.ts | 4 ++-- src/core/pathfinding/PathFinding.ts | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index fcd0d7443..a2fa7793a 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { SerialAStar } from "../pathfinding/SerialAStar"; @@ -31,7 +31,7 @@ export class BattleshipExecution implements Execution { init(mg: MutableGame, ticks: number): void { - this.pathfinder = PathFinder.Serial(mg, 5000, t => t.isWater()) + this.pathfinder = PathFinder.Mini(mg, 5000, t => t.terrainType() == TerrainType.Ocean) this._owner = mg.player(this.playerID) this.mg = mg this.patrolCenterTile = mg.tile(this.cell) diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index cd2605e13..371bbc00d 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -29,7 +29,7 @@ export class DestroyerExecution implements Execution { init(mg: MutableGame, ticks: number): void { - this.pathfinder = PathFinder.Serial(mg, 5000, t => t.terrainType() == TerrainType.Ocean) + this.pathfinder = PathFinder.Mini(mg, 5000, t => t.terrainType() == TerrainType.Ocean) this._owner = mg.player(this.playerID) this.mg = mg this.patrolCenterTile = mg.tile(this.cell) diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 8e819350b..9e095ffaa 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -26,7 +26,7 @@ export class NukeExecution implements Execution { init(mg: MutableGame, ticks: number): void { this.mg = mg - this.pathFinder = PathFinder.Serial(mg, 10_000, () => true) + this.pathFinder = PathFinder.Mini(mg, 10_000, () => true) this.player = mg.player(this.senderID) this.dst = this.mg.tile(this.cell) } diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 0be6cf368..a22923f9a 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -13,7 +13,7 @@ export class ShellExecution implements Execution { } init(mg: MutableGame, ticks: number): void { - this.pathFinder = PathFinder.Serial(mg, 2000, () => true, 10) + this.pathFinder = PathFinder.Mini(mg, 2000, () => true, 10) } tick(ticks: number): void { diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 6c7c0ff40..bc0569d75 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -1,4 +1,4 @@ -import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent, UnitType } from "../game/Game"; +import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent, UnitType, TerrainType } from "../game/Game"; import { and, bfs, manhattanDistWrapped, sourceDstOceanShore, targetTransportTile } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; @@ -43,7 +43,7 @@ export class TransportShipExecution implements Execution { init(mg: MutableGame, ticks: number) { this.lastMove = ticks this.mg = mg - this.pathFinder = PathFinder.Serial(mg, 10_000, t => t.isWater(), 2) + this.pathFinder = PathFinder.Mini(mg, 10_000, t => t.terrainType() == TerrainType.Ocean, 2) this.attacker = mg.player(this.attackerID) diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts index e71795869..a3695b219 100644 --- a/src/core/pathfinding/MiniAStar.ts +++ b/src/core/pathfinding/MiniAStar.ts @@ -42,8 +42,8 @@ export class MiniAStar implements AStar { function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] { // Scale up each point const scaledPath = path.map(point => (new Cell( - point.x * scaleFactor, - point.y * scaleFactor + point.x * scaleFactor, + point.y * scaleFactor ))); const smoothPath: Cell[] = []; diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 4da1cb623..40ec4fda1 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -1,6 +1,6 @@ import { Cell, Game, Tile } from "../game/Game"; import { manhattanDist } from "../Util"; -import { AStar, PathFindResultType, TileResult } from "./AStar"; +import { AStar, PathFindResultType, SearchNode, TileResult } from "./AStar"; import { ParallelAStar, WorkerClient } from "../worker/WorkerClient"; import { SerialAStar } from "./SerialAStar"; import { MiniAStar } from "./MiniAStar"; @@ -19,7 +19,7 @@ export class PathFinder { ) { } - public static Mini(game: Game, iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20) { + public static Mini(game: Game, iterations: number, canMove: (s: SearchNode) => boolean, maxTries: number = 20) { return new PathFinder( game, (curr: Tile, dst: Tile) => { From 5a4a62bb86eb17feb6a84275a68bbe626e7a6bcf Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Dec 2024 16:44:28 -0800 Subject: [PATCH 05/10] in progress --- src/core/execution/PortExecution.ts | 4 +- src/core/pathfinding/PathFinding.ts | 9 ++-- src/core/worker/Worker.worker.ts | 78 +++++++++-------------------- src/core/worker/WorkerClient.ts | 44 +++++++++++++--- 4 files changed, 70 insertions(+), 65 deletions(-) diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index df1d7ab67..4b71b2191 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,4 +1,4 @@ -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, TerrainType, Tile, Unit, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { SerialAStar } from "../pathfinding/SerialAStar"; @@ -83,7 +83,7 @@ export class PortExecution implements Execution { } continue } - const asyncPF = this.worker.createParallelAStar(this.port.tile(), port.tile(), 100) + const asyncPF = this.worker.createParallelAStar(this.port.tile(), port.tile(), 100, [TerrainType.Ocean]) // console.log(`adding new port path from ${this.player().name()}:${this.port.tile().cell()} to ${port.owner().name()}:${port.tile().cell()}`) this.computingPaths.set(port, asyncPF) } diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 40ec4fda1..5a2ce35c0 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -1,4 +1,4 @@ -import { Cell, Game, Tile } from "../game/Game"; +import { Cell, Game, TerrainTile, TerrainType, Tile } from "../game/Game"; import { manhattanDist } from "../Util"; import { AStar, PathFindResultType, SearchNode, TileResult } from "./AStar"; import { ParallelAStar, WorkerClient } from "../worker/WorkerClient"; @@ -51,11 +51,14 @@ export class PathFinder { ) } - public static Parallel(game: Game, worker: WorkerClient, numTicks: number): PathFinder { + public static Parallel(game: Game, worker: WorkerClient, numTicks: number, ...types: TerrainType[]): PathFinder { + if (types.length == 0) { + types = [TerrainType.Ocean] + } return new PathFinder( game, (curr: Tile, dst: Tile) => { - return worker.createParallelAStar(curr, dst, numTicks) + return worker.createParallelAStar(curr, dst, numTicks, types) } ) } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index cb8504cbe..abfc37aed 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -3,15 +3,19 @@ import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from "../game/Gam import { createMiniMap, loadTerrainMap } from "../game/TerrainMapLoader"; import { PriorityQueue } from "@datastructures-js/priority-queue"; import { SerialAStar } from "../pathfinding/SerialAStar"; -import { PathFindResultType, SearchNode } from "../pathfinding/AStar"; +import { AStar, PathFindResultType, SearchNode } from "../pathfinding/AStar"; +import { MiniAStar } from "../pathfinding/MiniAStar"; -let terrainMapPromise: Promise; +let terrainMapPromise: Promise<{ + terrainMap: TerrainMap, + miniMap: TerrainMap +}> | null = null; let searches = new PriorityQueue((a: Search, b: Search) => (a.deadline - b.deadline)) let processingInterval: number | null = null; let isProcessingSearch = false interface Search { - aStar: SerialAStar, + aStar: AStar, deadline: number requestId: string, end: Cell @@ -32,22 +36,31 @@ self.onmessage = (e) => { initializeMap(e.data); break; case 'findPath': - terrainMapPromise.then(tm => findPath(tm, e.data)) + terrainMapPromise.then(tm => findPath(tm.terrainMap, tm.miniMap, e.data)) break; } }; function initializeMap(data: { gameMap: GameMap }) { - terrainMapPromise = loadTerrainMap(data.gameMap).then(tm => createMiniMap(tm)) + terrainMapPromise = loadTerrainMap(data.gameMap) + .then(async terrainMap => { + const miniMap = await createMiniMap(terrainMap); + return { + terrainMap: terrainMap, + miniMap: miniMap + }; + }); self.postMessage({ type: 'initialized' }); - processingInterval = setInterval(computeSearches, .1) as unknown as number; + processingInterval = setInterval(computeSearches, 100000) as unknown as number; } -function findPath(terrainMap: TerrainMap, req: SearchRequest) { +function findPath(terrainMap: TerrainMap, miniTerrainMap: TerrainMap, req: SearchRequest) { console.log(`terrain map height: ${terrainMap.height()}`) - const aStar = new SerialAStar( - terrainMap.terrain(new Cell(Math.floor(req.start.x / 2), Math.floor(req.start.y / 2))), - terrainMap.terrain(new Cell(Math.floor(req.end.x / 2), Math.floor(req.end.y / 2))), + const aStar = new MiniAStar( + terrainMap, + miniTerrainMap, + terrainMap.terrain(req.start), + terrainMap.terrain(req.end), (sn: SearchNode) => (sn as TerrainTile).terrainType() == TerrainType.Ocean, 10_000, req.duration, @@ -76,12 +89,10 @@ function computeSearches() { const search = searches.dequeue() switch (search.aStar.compute()) { case PathFindResultType.Completed: - const path = upscalePath(search.aStar.reconstructPath()) - path.push(search.end) self.postMessage({ type: 'pathFound', requestId: search.requestId, - path: path + path: search.aStar.reconstructPath() }); break; @@ -101,44 +112,3 @@ function computeSearches() { isProcessingSearch = false } } - -function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] { - // Scale up each point - const scaledPath = path.map(point => (new Cell( - point.x * scaleFactor, - point.y * scaleFactor - ))); - - const smoothPath: Cell[] = []; - - for (let i = 0; i < scaledPath.length - 1; i++) { - const current = scaledPath[i]; - const next = scaledPath[i + 1]; - - // Add the current point - smoothPath.push(current); - - // Always interpolate between scaled points - const dx = next.x - current.x; - const dy = next.y - current.y; - - // Calculate number of steps needed - const distance = Math.max(Math.abs(dx), Math.abs(dy)); - const steps = distance; - - // Add intermediate points - for (let step = 1; step < steps; step++) { - smoothPath.push(new Cell( - Math.round(current.x + (dx * step) / steps), - Math.round(current.y + (dy * step) / steps) - )); - } - } - - // Add the last point - if (scaledPath.length > 0) { - smoothPath.push(scaledPath[scaledPath.length - 1]); - } - - return smoothPath; -} diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 5f28ea4b2..e5cffc9b4 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -1,5 +1,6 @@ -import { Cell, Game, GameMap, Tile } from "../game/Game"; +import { Cell, Game, GameMap, TerrainTile, TerrainType, Tile } from "../game/Game"; import { AStar, PathFindResultType } from "../pathfinding/AStar"; +import { MiniAStar } from "../pathfinding/MiniAStar"; export class WorkerClient { @@ -34,17 +35,18 @@ export class WorkerClient { }); } - createParallelAStar(src: Tile, dst: Tile, numTicks: number): ParallelAStar { + createParallelAStar(src: Tile, dst: Tile, numTicks: number, types: TerrainType[]): ParallelAStar { if (!this.isInitialized) { throw new Error('PathFinder not initialized'); } - return new ParallelAStar(this.game, this.worker, src, dst, numTicks); + return new ParallelAStar(this.game, this.worker, src, dst, numTicks, types); } cleanup() { this.worker.terminate(); } } + export class ParallelAStar implements AStar { private path: Cell[] | 'NOT_FOUND' | null = null; private promise: Promise; @@ -54,7 +56,8 @@ export class ParallelAStar implements AStar { private worker: Worker, private src: Tile, private dst: Tile, - private numTicks: number + private numTicks: number, + private terrainTypes: TerrainType[] ) { } findPath(): Promise { @@ -85,6 +88,7 @@ export class ParallelAStar implements AStar { this.worker.postMessage({ type: 'findPath', requestId: requestId, + terrainTypes: this.terrainTypes, currentTick: this.game.ticks(), duration: this.numTicks, start: { x: this.src.cell().x, y: this.src.cell().y }, @@ -108,7 +112,36 @@ export class ParallelAStar implements AStar { if (this.path != null) { return PathFindResultType.Completed; } - throw new Error(`path not completed in time`); + // Path was not found in worker thread in time, so now we need + // to recompute it in main thread. This will lock up game. + console.warn(`path not completed in worker thread, recomputing`) + const local = new MiniAStar( + this.game.terrainMap(), + this.game.terrainMiniMap(), + this.src, this.dst, + (t: TerrainTile) => t.terrainType() == TerrainType.Ocean, + 100_000_000, + 20 + ) + const result = local.compute() + switch (result) { + case PathFindResultType.Completed: + console.log('recomputed path in worker client') + this.path = local.reconstructPath() + break + case PathFindResultType.PathNotFound: + this.path = "NOT_FOUND" + break + case PathFindResultType.Pending: + // TODO: make sure same number of tries as worker thread. + console.warn("path not found after many tries") + this.path = "NOT_FOUND" + break + } + if (result == PathFindResultType.Completed) { + this.path = local.reconstructPath() + } + return result } return PathFindResultType.Pending; } @@ -121,4 +154,3 @@ export class ParallelAStar implements AStar { } } - From 9391dad131d9d27d7f2095b7e0d01ade1275efb0 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Dec 2024 19:20:07 -0800 Subject: [PATCH 06/10] remove worker timeout --- src/core/configuration/DevConfig.ts | 3 +++ src/core/worker/Worker.worker.ts | 2 +- src/core/worker/WorkerClient.ts | 4 ---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index d72b57aa3..e5dc3bdda 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -25,6 +25,9 @@ export const devConfig = new class extends DefaultConfig { turnIntervalMs(): number { return 100 } + tradeShipSpawnRate(): number { + return 10 + } // boatMaxDistance(): number { // return 5000 // } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index abfc37aed..adbc3edc1 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -51,7 +51,7 @@ function initializeMap(data: { gameMap: GameMap }) { }; }); self.postMessage({ type: 'initialized' }); - processingInterval = setInterval(computeSearches, 100000) as unknown as number; + processingInterval = setInterval(computeSearches, .1) as unknown as number; } function findPath(terrainMap: TerrainMap, miniTerrainMap: TerrainMap, req: SearchRequest) { diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index e5cffc9b4..99dfbb858 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -63,15 +63,11 @@ export class ParallelAStar implements AStar { findPath(): Promise { const requestId = crypto.randomUUID(); this.promise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject("Path timeout"); - }, 100000); const handler = (e: MessageEvent) => { if (e.data.requestId != requestId) { return; } - clearTimeout(timeout); this.worker.removeEventListener('message', handler); if (e.data.type === 'pathFound') { From a0a40262a62c84206a73487edc67eee966f529a6 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Dec 2024 19:47:27 -0800 Subject: [PATCH 07/10] render trade ship when stopped, hash units --- TODO.txt | 12 ++++++------ src/core/execution/PortExecution.ts | 4 ++-- src/core/execution/TradeShipExecution.ts | 2 ++ src/core/game/PlayerImpl.ts | 2 +- src/core/game/UnitImpl.ts | 7 ++++++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index bf2a93a70..d2f65cefb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -201,14 +201,14 @@ * have NPCs build destroyers and battleships DONE 11/30/2024 * add info view on top right DONE 11/30/2024 * add info view for units DONE 11/30/2024 -* add defense post -* bugfix: when thread doesn't complete computation, do it in main -* bugfix: destroyers can't find path to dst and freeze -* bugfix: when trade ships captured don't render +* add defense post DONE 12/2/2024 +* bugfix: when thread doesn't complete computation, do it in main DONE 12/3/2024 +* bugfix: when trade ships captured don't render DONE 12/3/2024 +* use mini A* for all pathfinding DONE 12/3/2024 * bugfix: gameStop not found error -* use mini A* for all pathfinding -* log stack traces & display them on screen * record and replay games for debugging purposes +* bugfix: destroyers can't find path to dst and freeze +* log stack traces & display them on screen * record single player game stats * add radiation from nuke * add cities diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 4b71b2191..aeb6d81a2 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -83,7 +83,7 @@ export class PortExecution implements Execution { } continue } - const asyncPF = this.worker.createParallelAStar(this.port.tile(), port.tile(), 100, [TerrainType.Ocean]) + const asyncPF = this.worker.createParallelAStar(this.port.tile(), port.tile(), 25, [TerrainType.Ocean]) // console.log(`adding new port path from ${this.player().name()}:${this.port.tile().cell()} to ${port.owner().name()}:${port.tile().cell()}`) this.computingPaths.set(port, asyncPF) } @@ -101,7 +101,7 @@ export class PortExecution implements Execution { const port = this.random.randElement(portConnections) const path = this.portPaths.get(port) if (path != null) { - const pf = PathFinder.Parallel(this.mg, this.worker, 30) + const pf = PathFinder.Parallel(this.mg, this.worker, 10) this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path)) } } diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index ef8d817ba..018327b8c 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -79,6 +79,8 @@ export class TradeShipExecution implements Execution { this.tradeShip.delete() break case PathFindResultType.Pending: + // Fire unit event to rerender. + this.tradeShip.move(this.tradeShip.tile()) break case PathFindResultType.NextTile: this.tradeShip.move(result.tile) diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 82b3e1387..8c2113a0a 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -435,7 +435,7 @@ export class PlayerImpl implements MutablePlayer { } hash(): number { - return simpleHash(this.id()) * (this.population() + this.numTilesOwned()); + return simpleHash(this.id()) * (this.population() + this.numTilesOwned()) + this._units.reduce((acc, unit) => acc + unit.hash(), 0) } toString(): string { return `Player:{name:${this.info().name},clientID:${this.info().clientID},isAlive:${this.isAlive()},troops:${this._troops},numTileOwned:${this.numTilesOwned()}}]`; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 9e374227a..804685957 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,3 +1,4 @@ +import { simpleHash } from "../Util"; import { MutableUnit, Tile, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; @@ -20,7 +21,7 @@ export class UnitImpl implements MutableUnit { } move(tile: Tile): void { - if(tile == null) { + if (tile == null) { throw new Error("tile cannot be null") } const oldTile = this._tile; @@ -57,4 +58,8 @@ export class UnitImpl implements MutableUnit { isActive(): boolean { return this._active; } + + hash(): number { + return this.tile().cell().x + this.tile().cell().y + simpleHash(this.type()) + } } From ec155dcb84ec403d83d306f55ece8b704d648f09 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Dec 2024 20:04:00 -0800 Subject: [PATCH 08/10] fix gameStop undefined error when refreshing page --- TODO.txt | 2 +- src/client/Main.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO.txt b/TODO.txt index d2f65cefb..3a473dceb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -205,7 +205,7 @@ * bugfix: when thread doesn't complete computation, do it in main DONE 12/3/2024 * bugfix: when trade ships captured don't render DONE 12/3/2024 * use mini A* for all pathfinding DONE 12/3/2024 -* bugfix: gameStop not found error +* bugfix: gameStop not found error DONE 12/3/2024 * record and replay games for debugging purposes * bugfix: destroyers can't find path to dst and freeze * log stack traces & display them on screen diff --git a/src/client/Main.ts b/src/client/Main.ts index 6dbe23ccb..064154065 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -32,7 +32,9 @@ class Client { const s = this.stopGame window.addEventListener('beforeunload', function (event) { console.log('Browser is closing'); - s() + if (s != null) { + s() + } }); setFavicon() @@ -84,9 +86,7 @@ class Client { } private stopGame() { - if (this.gameStop != null) { - this.gameStop() - } + this?.gameStop?.() } private async handleLeaveLobby(event: CustomEvent) { From 019635e5238e1db84fa7d75eec2c52d3e85d1c19 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Dec 2024 20:15:30 -0800 Subject: [PATCH 09/10] alert on stack trace --- TODO.txt | 2 +- src/client/GameRunner.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index 3a473dceb..b4467f2d8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -206,9 +206,9 @@ * bugfix: when trade ships captured don't render DONE 12/3/2024 * use mini A* for all pathfinding DONE 12/3/2024 * bugfix: gameStop not found error DONE 12/3/2024 +* log stack traces & display them on screen DONE 12/3/2024 * record and replay games for debugging purposes * bugfix: destroyers can't find path to dst and freeze -* log stack traces & display them on screen * record single player game stats * add radiation from nuke * add cities diff --git a/src/client/GameRunner.ts b/src/client/GameRunner.ts index fc6e82d19..d489ebab9 100644 --- a/src/client/GameRunner.ts +++ b/src/client/GameRunner.ts @@ -173,7 +173,14 @@ export class GameRunner { } this.isProcessingTurn = true this.gs.addExecution(...this.executor.createExecs(this.turns[this.currTurn])) - this.gs.executeNextTick() + try { + this.gs.executeNextTick() + throw Error("test") + } catch (error) { + const errorText = `Error: ${error.message}\nStack: ${error.stack}`; + alert("Game crashed! Error info copied to clipboard. Please paste this in your bug report in Discord."); + navigator.clipboard.writeText(errorText); + } this.renderer.tick() this.currTurn++ this.isProcessingTurn = false From 5abe60070b1e8674da0fdb8e6083c393ba135ec9 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Dec 2024 20:17:58 -0800 Subject: [PATCH 10/10] remove references to warfront --- .gitmodules | 3 --- LICENSE | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/client/GameRunner.ts | 1 - 5 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 40c352aa9..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/map/codec"] - path = src/map/codec - url = https://github.com/WarFrontIO/MapCodec diff --git a/LICENSE b/LICENSE index 66550bd84..0eeabe67b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 WarFront.io Team +Copyright (c) 2024 OpenFront.io Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package-lock.json b/package-lock.json index eb28cb574..e510a1acd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,10 @@ { - "name": "warfront-client", + "name": "openfront-client", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "warfront-client", + "name": "openfront-client", "dependencies": { "@datastructures-js/priority-queue": "^6.3.1", "@types/dompurify": "^3.0.5", diff --git a/package.json b/package.json index 6edabc285..cdafbfd87 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "warfront-client", + "name": "openfront-client", "scripts": { "build-map": "node --loader ts-node/esm --experimental-specifier-resolution=node src/scripts/TerrainMapGenerator.ts", "build-dev": "webpack --config webpack.config.js --mode development", diff --git a/src/client/GameRunner.ts b/src/client/GameRunner.ts index d489ebab9..f0419c66d 100644 --- a/src/client/GameRunner.ts +++ b/src/client/GameRunner.ts @@ -175,7 +175,6 @@ export class GameRunner { this.gs.addExecution(...this.executor.createExecs(this.turns[this.currTurn])) try { this.gs.executeNextTick() - throw Error("test") } catch (error) { const errorText = `Error: ${error.message}\nStack: ${error.stack}`; alert("Game crashed! Error info copied to clipboard. Please paste this in your bug report in Discord.");