NPCs create battleships, destroyers. start work on miniastar

This commit is contained in:
Evan
2024-11-30 12:41:22 -08:00
parent 5d4befb117
commit 30f72a3365
11 changed files with 221 additions and 55 deletions
+1 -1
View File
@@ -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
}
+43 -1
View File
@@ -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() {
+1
View File
@@ -290,6 +290,7 @@ export interface Game {
units(...types: UnitType[]): Unit[]
unitInfo(type: UnitType): UnitInfo
terrainMap(): TerrainMap
terrainMiniMap(): TerrainMap
}
export interface MutableGame extends Game {
+15 -2
View File
@@ -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))
}
+43 -25
View File
@@ -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<TerrainMapImpl> {
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<TerrainMapImpl> {
}
}
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<TerrainMap> {
// 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
}
+5
View File
@@ -25,5 +25,10 @@ export enum PathFindResultType {
export interface SearchNode {
cost(): number
cell(): Cell
neighbors(): SearchNode[]
}
export interface Point {
x: number;
y: number;
}
+88
View File
@@ -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;
}
+21 -2
View File
@@ -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)
+2 -3
View File
@@ -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<SearchNode, SearchNode>;
@@ -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;
-1
View File
@@ -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,
);
+2 -20
View File
@@ -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