mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:50:42 +00:00
NPCs create battleships, destroyers. start work on miniastar
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -290,6 +290,7 @@ export interface Game {
|
||||
units(...types: UnitType[]): Unit[]
|
||||
unitInfo(type: UnitType): UnitInfo
|
||||
terrainMap(): TerrainMap
|
||||
terrainMiniMap(): TerrainMap
|
||||
}
|
||||
|
||||
export interface MutableGame extends Game {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,5 +25,10 @@ export enum PathFindResultType {
|
||||
export interface SearchNode {
|
||||
cost(): number
|
||||
cell(): Cell
|
||||
neighbors(): SearchNode[]
|
||||
}
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user