use TileRef instead of TerrainTile for astar

This commit is contained in:
evanpelle
2025-01-14 10:52:55 -08:00
committed by Evan
parent 2a2f62436c
commit b22532d41f
16 changed files with 149 additions and 110 deletions
-1
View File
@@ -1,6 +1,5 @@
import { Executor } from "../core/execution/ExecutionManager";
import { Cell, MutableGame, PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game";
import { createGame } from "../core/game/GameImpl";
import { EventBus } from "../core/EventBus";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler"
+1 -1
View File
@@ -14,7 +14,7 @@ import { GameUpdateViewData, packTileData } from "./GameView";
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
const config = getConfig(gameConfig)
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
const game = createGame(terrainMap.map, terrainMap.miniMap, config)
const game = createGame(terrainMap.gameMap, terrainMap.miniGameMap, terrainMap.map, terrainMap.miniMap, config)
const gr = new GameRunner(game as MutableGame, new Executor(game, gameID), callBack)
gr.init()
return gr
+7
View File
@@ -4,6 +4,7 @@ import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage
import { ClientID } from "./Schemas";
import { TerraNulliusImpl } from './game/TerraNulliusImpl';
import { WorkerClient } from './worker/WorkerClient';
import { TileRef } from './game/GameMap';
export class TileView {
@@ -12,6 +13,9 @@ export class TileView {
constructor(private game: GameView, public data: TileUpdate, private _terrain: TerrainTile) { }
ref(): TileRef {
throw new Error('uh oh')
}
type(): TerrainType {
return this._terrain.type()
}
@@ -32,6 +36,9 @@ export class TileView {
}
return false
}
isBorderUpdated(): boolean {
return this.data.isBorder
}
cell(): Cell {
return this._terrain.cell()
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class BattleshipExecution implements Execution {
init(mg: MutableGame, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, t => t.type() == TerrainType.Ocean)
this.pathfinder = PathFinder.Mini(mg, 5000, false)
this._owner = mg.player(this.playerID)
this.mg = mg
this.patrolCenterTile = mg.tile(this.cell)
+1 -1
View File
@@ -30,7 +30,7 @@ export class DestroyerExecution implements Execution {
init(mg: MutableGame, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, t => t.type() == TerrainType.Ocean)
this.pathfinder = PathFinder.Mini(mg, 5000, false)
this._owner = mg.player(this.playerID)
this.mg = mg
this.patrolCenterTile = mg.tile(this.cell)
+1 -1
View File
@@ -27,7 +27,7 @@ export class NukeExecution implements Execution {
init(mg: MutableGame, ticks: number): void {
this.mg = mg
this.pathFinder = PathFinder.Mini(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)
}
+7 -5
View File
@@ -7,6 +7,7 @@ import { bfs, dist, manhattanDist } from "../Util";
import { TradeShipExecution } from "./TradeShipExecution";
import { consolex } from "../Consolex";
import { MiniAStar } from "../pathfinding/MiniAStar";
import { TileRef } from "../game/GameMap";
export class PortExecution implements Execution {
@@ -85,10 +86,11 @@ export class PortExecution implements Execution {
}
const pf = new MiniAStar(
this.mg.terrainMap(),
this.mg.terrainMiniMap(),
this.port.tile().terrain(), port.tile().terrain(),
sn => sn.type() == TerrainType.Ocean,
this.mg.map(),
this.mg.miniMap(),
this.port.tile().ref(),
port.tile().ref(),
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
10_000,
25
)
@@ -108,7 +110,7 @@ export class PortExecution implements Execution {
const port = this.random.randElement(portConnections)
const path = this.portPaths.get(port)
if (path != null) {
const pf = PathFinder.Mini(this.mg, 10, (sn) => sn.type() == TerrainType.Ocean)
const pf = PathFinder.Mini(this.mg, 10, false)
this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path))
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ export class ShellExecution implements Execution {
}
init(mg: MutableGame, ticks: number): void {
this.pathFinder = PathFinder.Mini(mg, 2000, () => true, 10)
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10)
}
tick(ticks: number): void {
+1 -1
View File
@@ -45,7 +45,7 @@ export class TransportShipExecution implements Execution {
init(mg: MutableGame, ticks: number) {
this.lastMove = ticks
this.mg = mg
this.pathFinder = PathFinder.Mini(mg, 10_000, t => t.type() == TerrainType.Ocean, 2)
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2)
this.attacker = mg.player(this.attackerID)
+5
View File
@@ -1,6 +1,7 @@
import { Config } from "../configuration/Config"
import { GameEvent } from "../EventBus"
import { ClientID, GameConfig, GameID } from "../Schemas"
import { GameMap, TileRef } from "./GameMap"
export type PlayerID = string
export type Tick = number
@@ -221,6 +222,7 @@ export interface Tile {
terrain(): TerrainTile
neighbors(): Tile[]
hasDefenseBonus(): boolean
ref(): TileRef
}
export interface MutableTile extends Tile {
@@ -376,6 +378,9 @@ export interface Game {
unitInfo(type: UnitType): UnitInfo
terrainMap(): TerrainMap
terrainMiniMap(): TerrainMap
map(): GameMap
miniMap(): GameMap
}
export interface MutableGame extends Game {
+21 -12
View File
@@ -11,9 +11,10 @@ import { MessageType } from './Game';
import { UnitImpl } from "./UnitImpl";
import { consolex } from "../Consolex";
import { string } from "zod";
import { GameMap } from "./GameMap";
export function createGame(terrainMap: TerrainMapImpl, miniMap: TerrainMap, config: Config): Game {
return new GameImpl(terrainMap, miniMap, config)
export function createGame(gameMap: GameMap, miniGameMap: GameMap, terrainMap: TerrainMapImpl, miniMap: TerrainMap, config: Config): Game {
return new GameImpl(terrainMap, miniMap, gameMap, miniGameMap, config)
}
export type CellString = string
@@ -23,7 +24,7 @@ export class GameImpl implements MutableGame {
private unInitExecs: Execution[] = []
map: TileImpl[][]
_map: TileImpl[][]
private nations_: Nation[] = []
@@ -45,17 +46,19 @@ export class GameImpl implements MutableGame {
constructor(
private _terrainMap: TerrainMapImpl,
private _miniMap: TerrainMap,
private gameMap: GameMap,
private miniGameMap: GameMap,
private _config: Config,
) {
this._terraNullius = new TerraNulliusImpl()
this._width = _terrainMap.width();
this._height = _terrainMap.height();
this.map = new Array(this._width);
this._map = new Array(this._width);
for (let x = 0; x < this._width; x++) {
this.map[x] = new Array(this._height);
this._map[x] = new Array(this._height);
for (let y = 0; y < this._height; y++) {
let cell = new Cell(x, y);
this.map[x][y] = new TileImpl(this, this._terraNullius, cell, _terrainMap);
this._map[x][y] = new TileImpl(this, this._terraNullius, cell, _terrainMap);
}
}
this.nations_ = _terrainMap.nationMap.nations
@@ -65,6 +68,12 @@ export class GameImpl implements MutableGame {
n.strength
))
}
map(): GameMap {
return this.gameMap
}
miniMap(): GameMap {
return this.miniGameMap
}
addUpdate(update: GameUpdate) {
(this.updates[update.type] as any[]).push(update);
@@ -296,7 +305,7 @@ export class GameImpl implements MutableGame {
tile(cell: Cell): MutableTile {
this.assertIsOnMap(cell)
return this.map[cell.x][cell.y] as MutableTile
return this._map[cell.x][cell.y] as MutableTile
}
isOnMap(cell: Cell): boolean {
@@ -311,16 +320,16 @@ export class GameImpl implements MutableGame {
const y = tile.cell().y
const ns: TileImpl[] = []
if (y > 0) {
ns.push(this.map[x][y - 1])
ns.push(this._map[x][y - 1])
}
if (y < this._height - 1) {
ns.push(this.map[x][y + 1])
ns.push(this._map[x][y + 1])
}
if (x > 0) {
ns.push(this.map[x - 1][y])
ns.push(this._map[x - 1][y])
}
if (x < this._width - 1) {
ns.push(this.map[x + 1][y])
ns.push(this._map[x + 1][y])
}
return ns
}
@@ -335,7 +344,7 @@ export class GameImpl implements MutableGame {
const newX = x + dx
const newY = y + dy
if (newX >= 0 && newX < this._width && newY >= 0 && newY < this._height) {
ns.push(this.map[newX][newY])
ns.push(this._map[newX][newY])
}
}
}
+14 -6
View File
@@ -1,4 +1,4 @@
import { TerrainType } from "./Game";
import { Cell, TerrainType } from "./Game";
export type TileRef = number;
@@ -9,10 +9,10 @@ export class GameMap {
private readonly height_: number;
// Terrain bits (Uint8Array)
private static readonly IS_LAND_BIT = 0;
private static readonly SHORELINE_BIT = 1;
private static readonly OCEAN_BIT = 2;
private static readonly MAGNITUDE_OFFSET = 3; // Uses bits 3-7 (5 bits)
private static readonly IS_LAND_BIT = 7;
private static readonly SHORELINE_BIT = 6;
private static readonly OCEAN_BIT = 5;
private static readonly MAGNITUDE_OFFSET = 4; // Uses bits 3-7 (5 bits)
private static readonly MAGNITUDE_MASK = 0x1F; // 11111 in binary
// State bits (Uint16Array)
@@ -48,6 +48,10 @@ export class GameMap {
return Math.floor(ref / this.width_);
}
cell(ref: TileRef): Cell {
return new Cell(this.x(ref), this.y(ref))
}
width(): number { return this.width_; }
height(): number { return this.height_; }
numLandTiles(): number { return this.numLandTiles_; }
@@ -70,7 +74,7 @@ export class GameMap {
}
magnitude(ref: TileRef): number {
return (this.terrain[ref] >> GameMap.MAGNITUDE_OFFSET) & GameMap.MAGNITUDE_MASK;
return this.terrain[ref] & GameMap.MAGNITUDE_MASK;
}
// State getters and setters (mutable)
@@ -157,6 +161,10 @@ export class GameMap {
if (ref % w !== 0) neighbors.push(ref - 1);
if (ref % w !== w - 1) neighbors.push(ref + 1);
for (const n of neighbors) {
(this.ref(this.x(n), this.y(n)))
}
return neighbors;
}
}
+11 -6
View File
@@ -3,6 +3,7 @@ import { TerrainMapImpl, TerrainTileImpl } from "./TerrainMapLoader";
import { GameImpl } from "./GameImpl";
import { PlayerImpl } from "./PlayerImpl";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { TileRef } from "./GameMap";
export class TileImpl implements MutableTile {
@@ -21,6 +22,10 @@ export class TileImpl implements MutableTile {
private terrainMap: TerrainMapImpl
) { }
ref(): TileRef {
return this.gs.map().ref(this._cell.x, this._cell.y)
}
toUpdate(): TileUpdate {
return {
type: GameUpdateType.Tile,
@@ -71,26 +76,26 @@ export class TileImpl implements MutableTile {
// Check top neighbor
if (y > 0) {
ns.push(this.gs.map[x][y - 1]);
ns.push(this.gs._map[x][y - 1]);
}
// Check bottom neighbor
if (y < this.gs.height() - 1) {
ns.push(this.gs.map[x][y + 1]);
ns.push(this.gs._map[x][y + 1]);
}
// Check left neighbor (wrap around)
if (x > 0) {
ns.push(this.gs.map[x - 1][y]);
ns.push(this.gs._map[x - 1][y]);
} else {
ns.push(this.gs.map[this.gs.width() - 1][y]);
ns.push(this.gs._map[this.gs.width() - 1][y]);
}
// Check right neighbor (wrap around)
if (x < this.gs.width() - 1) {
ns.push(this.gs.map[x + 1][y]);
ns.push(this.gs._map[x + 1][y]);
} else {
ns.push(this.gs.map[0][y]);
ns.push(this.gs._map[0][y]);
}
return ns;
}
+22 -13
View File
@@ -1,5 +1,7 @@
import { Cell, Game, TerrainMap, TerrainTile, TerrainType } from "../game/Game";
import { AStar, PathFindResultType, } from "./AStar";
import { GameManager } from "../../server/GameManager";
import { Cell, Game, TerrainMap, TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { AStar, PathFindResultType, } from "./AStar";
import { SerialAStar } from "./SerialAStar";
// TODO: test this, get it work
@@ -8,22 +10,29 @@ export class MiniAStar implements AStar {
private aStar: SerialAStar
constructor(
private terrainMap: TerrainMap,
private miniMap: TerrainMap,
private src: TerrainTile,
private dst: TerrainTile,
private canMove: (t: TerrainTile) => boolean,
private gameMap: GameMap,
private miniMap: GameMap,
private src: TileRef,
private dst: TileRef,
private canMove: (t: TileRef) => 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)))
const miniSrc = this.miniMap.ref(
Math.floor(gameMap.x(src) / 2),
Math.floor(gameMap.y(src) / 2)
)
const miniDst = this.miniMap.ref(
Math.floor(gameMap.x(dst) / 2),
Math.floor(gameMap.y(dst) / 2)
)
this.aStar = new SerialAStar(
miniSrc,
miniDst,
canMove,
iterations,
maxTries
maxTries,
this.miniMap
)
}
@@ -33,7 +42,7 @@ export class MiniAStar implements AStar {
reconstructPath(): Cell[] {
const upscaled = upscalePath(this.aStar.reconstructPath())
upscaled.push(this.dst.cell())
upscaled.push(new Cell(this.gameMap.x(this.dst), this.gameMap.y(this.dst)))
return upscaled
}
@@ -42,8 +51,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[] = [];
+14 -21
View File
@@ -4,6 +4,7 @@ import { AStar, PathFindResultType, TileResult } from "./AStar";
import { SerialAStar } from "./SerialAStar";
import { MiniAStar } from "./MiniAStar";
import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class PathFinder {
@@ -19,31 +20,23 @@ export class PathFinder {
) { }
public static Mini(game: Game, iterations: number, canMove: (s: TerrainTile) => boolean, maxTries: number = 20) {
public static Mini(game: Game, iterations: number, canMoveOnLand: boolean, maxTries: number = 20) {
return new PathFinder(
game,
(curr: Tile, dst: Tile) => {
const currRef = game.map().ref(curr.cell().x, curr.cell().y)
const dstRef = game.map().ref(dst.cell().x, dst.cell().y)
return new MiniAStar(
game.terrainMap(),
game.terrainMiniMap(),
curr.terrain(),
dst.terrain(),
canMove,
iterations,
maxTries
)
}
)
}
public static Serial(game: Game, iterations: number, canMove: (t: TerrainTile) => boolean, maxTries: number = 20): PathFinder {
return new PathFinder(
game,
(curr: Tile, dst: Tile) => {
return new SerialAStar(
curr.terrain(),
dst.terrain(),
canMove,
game.map(),
game.miniMap(),
currRef,
dstRef,
(tr: TileRef): boolean => {
if (canMoveOnLand) {
return true
}
return game.miniMap().isOcean(tr)
},
iterations,
maxTries
)
+42 -40
View File
@@ -1,46 +1,48 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { AStar} from "./AStar";
import { AStar } from "./AStar";
import { PathFindResultType } from "./AStar";
import { Cell, TerrainTile, TerrainTileKey } from "../game/Game";
import { Cell } from "../game/Game";
import { consolex } from "../Consolex";
import { GameMap, TileRef } from "../game/GameMap";
export class SerialAStar implements AStar {
private fwdOpenSet: PriorityQueue<{ tile: TerrainTile; fScore: number; }>;
private bwdOpenSet: PriorityQueue<{ tile: TerrainTile; fScore: number; }>;
private fwdCameFrom: Map<TerrainTileKey, TerrainTile>;
private bwdCameFrom: Map<TerrainTileKey, TerrainTile>;
private fwdGScore: Map<TerrainTileKey, number>;
private bwdGScore: Map<TerrainTileKey, number>;
private meetingPoint: TerrainTile | null;
private fwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number; }>;
private bwdOpenSet: PriorityQueue<{ tile: TileRef; fScore: number; }>;
private fwdCameFrom: Map<TileRef, TileRef>;
private bwdCameFrom: Map<TileRef, TileRef>;
private fwdGScore: Map<TileRef, number>;
private bwdGScore: Map<TileRef, number>;
private meetingPoint: TileRef | null;
public completed: boolean;
constructor(
private src: TerrainTile,
private dst: TerrainTile,
private canMove: (t: TerrainTile) => boolean,
private src: TileRef,
private dst: TileRef,
private canMove: (t: TileRef) => boolean,
private iterations: number,
private maxTries: number
private maxTries: number,
private gameMap: GameMap
) {
this.fwdOpenSet = new PriorityQueue<{ tile: TerrainTile; fScore: number; }>(
this.fwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>(
(a, b) => a.fScore - b.fScore
);
this.bwdOpenSet = new PriorityQueue<{ tile: TerrainTile; fScore: number; }>(
this.bwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>(
(a, b) => a.fScore - b.fScore
);
this.fwdCameFrom = new Map<TerrainTileKey, TerrainTile>();
this.bwdCameFrom = new Map<TerrainTileKey, TerrainTile>();
this.fwdGScore = new Map<TerrainTileKey, number>();
this.bwdGScore = new Map<TerrainTileKey, number>();
this.fwdCameFrom = new Map<TileRef, TileRef>();
this.bwdCameFrom = new Map<TileRef, TileRef>();
this.fwdGScore = new Map<TileRef, number>();
this.bwdGScore = new Map<TileRef, number>();
this.meetingPoint = null;
this.completed = false;
// Initialize forward search
this.fwdGScore.set(src.key(), 0);
this.fwdGScore.set(src, 0);
this.fwdOpenSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) });
// Initialize backward search
this.bwdGScore.set(dst.key(), 0);
this.bwdGScore.set(dst, 0);
this.bwdOpenSet.enqueue({ tile: dst, fScore: this.heuristic(dst, src) });
}
@@ -61,43 +63,43 @@ export class SerialAStar implements AStar {
// Process forward search
const fwdCurrent = this.fwdOpenSet.dequeue()!.tile;
if (this.bwdGScore.has(fwdCurrent.key())) {
if (this.bwdGScore.has(fwdCurrent)) {
// We found a meeting point!
this.meetingPoint = fwdCurrent;
this.completed = true;
return PathFindResultType.Completed;
}
this.expandTerrainTile(fwdCurrent, true);
this.expandTileRef(fwdCurrent, true);
// Process backward search
const bwdCurrent = this.bwdOpenSet.dequeue()!.tile;
if (this.fwdGScore.has(bwdCurrent.key())) {
if (this.fwdGScore.has(bwdCurrent)) {
// We found a meeting point!
this.meetingPoint = bwdCurrent;
this.completed = true;
return PathFindResultType.Completed;
}
this.expandTerrainTile(bwdCurrent, false);
this.expandTileRef(bwdCurrent, false);
}
return this.completed ? PathFindResultType.Completed : PathFindResultType.PathNotFound;
}
private expandTerrainTile(current: TerrainTile, isForward: boolean) {
for (const neighbor of current.neighbors()) {
if (!neighbor.equals(isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue;
private expandTileRef(current: TileRef, isForward: boolean) {
for (const neighbor of this.gameMap.neighbors(current)) {
if (neighbor != (isForward ? this.dst : this.src) && !this.canMove(neighbor)) continue;
const gScore = isForward ? this.fwdGScore : this.bwdGScore;
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
let tentativeGScore = gScore.get(current.key())! + neighbor.cost();
let tentativeGScore = gScore.get(current)! + this.gameMap.cost(neighbor);
if (!gScore.has(neighbor.key()) || tentativeGScore < gScore.get(neighbor.key())!) {
cameFrom.set(neighbor.key(), current);
gScore.set(neighbor.key(), tentativeGScore);
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
const fScore = tentativeGScore + this.heuristic(
neighbor,
isForward ? this.dst : this.src
@@ -107,10 +109,10 @@ export class SerialAStar implements AStar {
}
}
private heuristic(a: TerrainTile, b: TerrainTile): number {
private heuristic(a: TileRef, b: TileRef): number {
// TODO use wrapped
try {
return 1.1 * Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y);
return 1.1 * Math.abs(this.gameMap.x(a) - this.gameMap.x(b)) + Math.abs(this.gameMap.y(a) - this.gameMap.y(b));
} catch {
consolex.log('uh oh')
}
@@ -120,20 +122,20 @@ export class SerialAStar implements AStar {
if (!this.meetingPoint) return [];
// Reconstruct path from start to meeting point
const fwdPath: TerrainTile[] = [this.meetingPoint];
const fwdPath: TileRef[] = [this.meetingPoint];
let current = this.meetingPoint;
while (this.fwdCameFrom.has(current.key())) {
current = this.fwdCameFrom.get(current.key())!;
while (this.fwdCameFrom.has(current)) {
current = this.fwdCameFrom.get(current)!;
fwdPath.unshift(current);
}
// Reconstruct path from meeting point to goal
current = this.meetingPoint;
while (this.bwdCameFrom.has(current.key())) {
current = this.bwdCameFrom.get(current.key())!;
while (this.bwdCameFrom.has(current)) {
current = this.bwdCameFrom.get(current)!;
fwdPath.push(current);
}
return fwdPath.map(sn => sn.cell());
return fwdPath.map(sn => new Cell(this.gameMap.x(sn), this.gameMap.y(sn)));
}
}