From 30f72a3365dafe86f118903c8ac0231f6b49ecf7 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 30 Nov 2024 12:41:22 -0800 Subject: [PATCH] NPCs create battleships, destroyers. start work on miniastar --- src/core/configuration/DevConfig.ts | 2 +- src/core/execution/FakeHumanExecution.ts | 44 +++++++++++- src/core/game/Game.ts | 1 + src/core/game/GameImpl.ts | 17 ++++- src/core/game/TerrainMapLoader.ts | 68 +++++++++++------- src/core/pathfinding/AStar.ts | 5 ++ src/core/pathfinding/MiniAStar.ts | 88 ++++++++++++++++++++++++ src/core/pathfinding/PathFinding.ts | 23 ++++++- src/core/pathfinding/SerialAStar.ts | 5 +- src/core/worker/Worker.worker.ts | 1 - src/scripts/TerrainMapGenerator.ts | 22 +----- 11 files changed, 221 insertions(+), 55 deletions(-) create mode 100644 src/core/pathfinding/MiniAStar.ts diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 857e6a283..bb3edb960 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) / 10 + info.cost = (p: Player) => oldCost(p) / 1000 return info } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 949a8c61e..a22c3c653 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -7,6 +7,8 @@ import { SpawnExecution } from "./SpawnExecution"; import { PortExecution } from "./PortExecution"; import { ParallelAStar, WorkerClient } from "../worker/WorkerClient"; import { PathFinder } from "../pathfinding/PathFinding"; +import { DestroyerExecution } from "./DestroyerExecution"; +import { BattleshipExecution } from "./BattleshipExecution"; export class FakeHumanExecution implements Execution { @@ -149,13 +151,53 @@ export class FakeHumanExecution implements Execution { } private handleUnits() { - if (this.player.units(UnitType.Port).length == 0 && this.player.gold() > this.mg.unitInfo(UnitType.Port).cost(this.player)) { + 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 => t.isOceanShore()) if (oceanTiles.length > 0) { const buildTile = this.random.randElement(oceanTiles) this.mg.addExecution(new PortExecution(this.player.id(), buildTile.cell(), this.worker)) } + return } + if (this.maybeSpawnWarship(UnitType.Destroyer)) { + return + } + if (this.maybeSpawnWarship(UnitType.Battleship)) { + return + } + } + + private maybeSpawnWarship(shipType: UnitType.Destroyer | UnitType.Battleship): boolean { + const ports = this.player.units(UnitType.Port) + const ships = this.player.units(shipType) + if (ports.length > 0 && ships.length == 0 && this.player.gold() > this.cost(shipType)) { + const port = this.random.randElement(ports) + const spawns = Array.from(bfs(port.tile(), dist(port.tile(), this.mg.config().boatMaxDistance() / 2))).filter(t => t.isOcean()) + if (spawns.length == 0) { + return false + } + const targetTile = this.random.randElement(spawns) + const canBuild = this.player.canBuild(UnitType.Destroyer, targetTile) + if (canBuild == false) { + console.warn('cannot spawn destroyer') + return false + } + switch (shipType) { + case UnitType.Destroyer: + this.mg.addExecution(new DestroyerExecution(this.player.id(), targetTile.cell())) + break + case UnitType.Battleship: + this.mg.addExecution(new BattleshipExecution(this.player.id(), targetTile.cell())) + break + } + return true + } + return false + } + + private cost(type: UnitType): number { + return this.mg.unitInfo(type).cost(this.player) } handleAllianceRequests() { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a4de166a6..5ad144bee 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -290,6 +290,7 @@ export interface Game { units(...types: UnitType[]): Unit[] unitInfo(type: UnitType): UnitInfo terrainMap(): TerrainMap + terrainMiniMap(): TerrainMap } export interface MutableGame extends Game { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 4f2bec451..dfda7a9e9 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,8 +1,8 @@ import { info } from "console"; import { Config } from "../configuration/Config"; import { EventBus } from "../EventBus"; -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo } from "./Game"; -import { TerrainMapImpl } from "./TerrainMapLoader"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap } from "./Game"; +import { createMiniMap, TerrainMapImpl } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { TileImpl } from "./TileImpl"; @@ -38,6 +38,8 @@ export class GameImpl implements MutableGame { allianceRequests: AllianceRequestImpl[] = [] alliances_: AllianceImpl[] = [] + private _terrainMiniMap: TerrainMap = null + constructor(private _terrainMap: TerrainMapImpl, public eventBus: EventBus, private _config: Config) { this._terraNullius = new TerraNulliusImpl(this) this._width = _terrainMap.width(); @@ -57,6 +59,10 @@ export class GameImpl implements MutableGame { new Cell(n.coordinates[0], n.coordinates[1]), n.strength )) + createMiniMap(_terrainMap).then(m => { + console.log('mini map loaded!') + this._terrainMiniMap = m + }) } units(...types: UnitType[]): UnitImpl[] { return Array.from(this._players.values()).flatMap(p => p.units(...types)) @@ -398,6 +404,13 @@ export class GameImpl implements MutableGame { return this._terrainMap } + public terrainMiniMap(): TerrainMap { + if (this._terrainMiniMap == null) { + throw Error('mini map not loaded') + } + return this._terrainMiniMap + } + displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void { this.eventBus.emit(new DisplayMessageEvent(message, type, playerID)) } diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index decf30127..80179e392 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -37,7 +37,7 @@ export class TerrainTileImpl implements TerrainTile { public land = false private _neighbors: TerrainTile[] | null = null - constructor(public type: TerrainType, private _cell: Cell) { } + constructor(private map: TerrainMap, public type: TerrainType, private _cell: Cell) { } terrainType(): TerrainType { return this.type @@ -51,7 +51,7 @@ export class TerrainTileImpl implements TerrainTile { return this._cell } - initNeighbors(map: TerrainMapImpl): TerrainTile[] { + neighbors(): TerrainTile[] { if (this._neighbors === null) { const positions = [ { x: this._cell.x - 1, y: this._cell.y }, // Left @@ -61,23 +61,23 @@ export class TerrainTileImpl implements TerrainTile { ]; this._neighbors = positions - .filter(pos => pos.x >= 0 && pos.x < map.width() && - pos.y >= 0 && pos.y < map.height()) - .map(pos => map.terrain(new Cell(pos.x, pos.y))); + .filter(pos => pos.x >= 0 && pos.x < this.map.width() && + pos.y >= 0 && pos.y < this.map.height()) + .map(pos => this.map.terrain(new Cell(pos.x, pos.y))); } return this._neighbors; } } export class TerrainMapImpl implements TerrainMap { + public tiles: TerrainTileImpl[][] + public numLandTiles: number + public nationMap: NationMap constructor( - public readonly tiles: TerrainTileImpl[][], - public readonly numLandTiles: number, - public readonly nationMap: NationMap, ) { } neighbors(terrainTile: TerrainTile): TerrainTile[] { - return (terrainTile as TerrainTileImpl).initNeighbors(this); + return (terrainTile as TerrainTileImpl).neighbors(); } terrain(cell: Cell): TerrainTileImpl { @@ -121,6 +121,10 @@ export async function loadTerrainMap(map: GameMap): Promise { const terrain: TerrainTileImpl[][] = Array(width).fill(null).map(() => Array(height).fill(null)); let numLand = 0 + + + const m = new TerrainMapImpl(); + // Start from the 5th byte (index 4) when processing terrain data for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { @@ -150,47 +154,61 @@ export async function loadTerrainMap(map: GameMap): Promise { } } - terrain[x][y] = new TerrainTileImpl(type, new Cell(x, y)); + terrain[x][y] = new TerrainTileImpl(m, type, new Cell(x, y)); terrain[x][y].shoreline = shoreline; terrain[x][y].magnitude = magnitude; terrain[x][y].ocean = ocean terrain[x][y].land = land } } + m.tiles = terrain + m.numLandTiles = numLand + m.nationMap = mapData.info // const encoder = new TextEncoder(); // const encoded = encoder.encode(fileData); // const buffer = new SharedArrayBuffer(encoded.length); // const view = new Uint8Array(buffer); // view.set(encoded) - const m = new TerrainMapImpl(terrain, numLand, mapData.info); loadedMaps.set(map, m) return m } -export function createMiniMap(tm: TerrainMap): TerrainMap { +export async function createMiniMap(tm: TerrainMap): Promise { // Create 2D array properly with correct dimensions const miniMap: TerrainTileImpl[][] = Array(Math.floor(tm.width() / 2)) .fill(null) .map(() => Array(Math.floor(tm.height() / 2)).fill(null)); - for (let x = 0; x < tm.width(); x++) { - for (let y = 0; y < tm.height(); y++) { - const tile = tm.terrain(new Cell(x, y)) as TerrainTileImpl; - const miniX = Math.floor(x / 2); - const miniY = Math.floor(y / 2); + // Process rows in chunks to avoid blocking the main thread + const chunkSize = 10; // Process 10 rows at a time - if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].terrainType() != TerrainType.Ocean) { - miniMap[miniX][miniY] = new TerrainTileImpl(tile.terrainType(), new Cell(miniX, miniY)); - miniMap[miniX][miniY].shoreline = tile.shoreline; - miniMap[miniX][miniY].magnitude = tile.magnitude; - miniMap[miniX][miniY].ocean = tile.ocean; - miniMap[miniX][miniY].land = tile.land; + const m = new TerrainMapImpl + + for (let startX = 0; startX < tm.width(); startX += chunkSize) { + // Use setTimeout to yield to the main thread between chunks + await new Promise(resolve => setTimeout(resolve, 0)); + + const endX = Math.min(startX + chunkSize, tm.width()); + + for (let x = startX; x < endX; x++) { + for (let y = 0; y < tm.height(); y++) { + const tile = tm.terrain(new Cell(x, y)) as TerrainTileImpl; + const miniX = Math.floor(x / 2); + const miniY = Math.floor(y / 2); + + if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].terrainType() != TerrainType.Ocean) { + miniMap[miniX][miniY] = new TerrainTileImpl(m, tile.terrainType(), new Cell(miniX, miniY)); + miniMap[miniX][miniY].shoreline = tile.shoreline; + miniMap[miniX][miniY].magnitude = tile.magnitude; + miniMap[miniX][miniY].ocean = tile.ocean; + miniMap[miniX][miniY].land = tile.land; + } } } } - - return new TerrainMapImpl(miniMap, 0, null); + m.tiles = miniMap + return m } diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts index 5056af3c1..b00970239 100644 --- a/src/core/pathfinding/AStar.ts +++ b/src/core/pathfinding/AStar.ts @@ -25,5 +25,10 @@ export enum PathFindResultType { export interface SearchNode { cost(): number cell(): Cell + neighbors(): SearchNode[] +} +export interface Point { + x: number; + y: number; } diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts new file mode 100644 index 000000000..1722f5cef --- /dev/null +++ b/src/core/pathfinding/MiniAStar.ts @@ -0,0 +1,88 @@ +import { Cell, Game, TerrainMap, TerrainTile, TerrainType } from "../game/Game"; +import { AStar, PathFindResultType, Point, SearchNode } from "./AStar"; +import { SerialAStar } from "./SerialAStar"; + +// TODO: test this, get it work +export class MiniAStar implements AStar { + + private aStar: SerialAStar + + constructor( + private terrainMap: TerrainMap, + private miniMap: TerrainMap, + private src: SearchNode, + private dst: SearchNode, + private canMove: (t: SearchNode) => boolean, + private iterations: number, + private maxTries: number + ) { + const miniSrc = miniMap.terrain(new Cell(Math.floor(src.cell().x / 2), Math.floor(src.cell().y / 2))) + const miniDst = miniMap.terrain(new Cell(Math.floor(dst.cell().x / 2), Math.floor(dst.cell().y / 2))) + this.aStar = new SerialAStar( + miniSrc, + miniDst, + (t => (t as TerrainTile).terrainType() == TerrainType.Ocean), + iterations, + maxTries + ) + } + + compute(): PathFindResultType { + return this.aStar.compute() + } + + reconstructPath(): SearchNode[] { + 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 }) + return upscaled + } + +} + +function upscalePath(path: SearchNode[], scaleFactor: number = 2): Point[] { + // Scale up each point + const scaledPath = path.map(point => ({ + x: point.cell().x * scaleFactor, + y: point.cell().y * scaleFactor + })); + + const smoothPath: Point[] = []; + + 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({ + x: Math.round(current.x + (dx * step) / steps), + y: Math.round(current.y + (dy * step) / steps) + }); + } + } + + // Add the last point + if (scaledPath.length > 0) { + smoothPath.push(scaledPath[scaledPath.length - 1]); + } + + return smoothPath; +} \ No newline at end of file diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index f3d2974a1..968cbf9cc 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -3,6 +3,7 @@ import { manhattanDist } from "../Util"; import { AStar, PathFindResultType, TileResult } from "./AStar"; import { ParallelAStar, WorkerClient } from "../worker/WorkerClient"; import { SerialAStar } from "./SerialAStar"; +import { MiniAStar } from "./MiniAStar"; export class PathFinder { @@ -16,6 +17,23 @@ export class PathFinder { private newAStar: (curr: Tile, dst: Tile) => AStar ) { } + + public static Mini(game: Game, iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20) { + return new PathFinder( + (curr: Tile, dst: Tile) => { + return new MiniAStar( + game.terrainMap(), + game.terrainMiniMap(), + curr, + dst, + canMove, + iterations, + maxTries + ) + } + ) + } + public static Serial(iterations: number, canMove: (t: Tile) => boolean, maxTries: number = 20): PathFinder { return new PathFinder( (curr: Tile, dst: Tile) => { @@ -23,7 +41,8 @@ export class PathFinder { curr, dst, canMove, - sn => ((sn as Tile).neighbors()), iterations, maxTries + iterations, + maxTries ) } ) @@ -65,7 +84,7 @@ export class PathFinder { switch (this.aStar.compute()) { case PathFindResultType.Completed: this.computeFinished = true - this.path = this.aStar.reconstructPath().map(sn => sn as Tile) + this.path = this.aStar.reconstructPath() as Tile[] // 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 e2b1b58a6..266e6df53 100644 --- a/src/core/pathfinding/SerialAStar.ts +++ b/src/core/pathfinding/SerialAStar.ts @@ -3,7 +3,7 @@ import { AStar, SearchNode } from "./AStar"; import { PathFindResultType } from "./AStar"; -export class SerialAStar implements AStar{ +export class SerialAStar implements AStar { private fwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; private bwdOpenSet: PriorityQueue<{ tile: SearchNode; fScore: number; }>; private fwdCameFrom: Map; @@ -17,7 +17,6 @@ export class SerialAStar implements AStar{ private src: SearchNode, private dst: SearchNode, private canMove: (t: SearchNode) => boolean, - private neighbors: (sn: SearchNode) => SearchNode[], private iterations: number, private maxTries: number ) { @@ -85,7 +84,7 @@ export class SerialAStar implements AStar{ } private expandSearchNode(current: SearchNode, isForward: boolean) { - for (const neighbor of this.neighbors(current)) { + for (const neighbor of current.neighbors()) { if (neighbor !== (isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue; const gScore = isForward ? this.fwdGScore : this.bwdGScore; diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index c096c373c..ce87b0c8e 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -54,7 +54,6 @@ function findPath(terrainMap: TerrainMap, req: SearchRequest) { 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))), (sn: SearchNode) => (sn as TerrainTile).terrainType() == TerrainType.Ocean, - (sn: SearchNode): SearchNode[] => terrainMap.neighbors((sn as TerrainTile)), 10_000, req.duration, ); diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index 3e0e4eddb..decf5da08 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -15,30 +15,12 @@ interface Coord { y: number; } -export class TerrainMap { - constructor(public readonly tiles: Terrain[][]) { } - - terrain(coord: Coord): Terrain { - return this.tiles[coord.x][coord.y] - } - - - - width(): number { - return this.tiles.length - } - - height(): number { - return this.tiles[0].length - } -} - -export enum TerrainType { +enum TerrainType { Land, Water } -export class Terrain { +class Terrain { public shoreline: boolean = false public magnitude: number = 0 public ocean: boolean