From 27762942201f778b8719a8d2cc2bc3763c14e460 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:35:10 +0100 Subject: [PATCH] Enhance game map handling with microGameMap integration - Added `microGameMap` to support a new resolution level for compact games, allowing for more efficient map loading and pathfinding. - Updated `createGame` and `GameImpl` to incorporate `microGameMap`, ensuring proper handling of different map resolutions. - Modified `loadTerrainMap` to always expose the 16x map for coarse heuristics, improving navigation capabilities. --- docs/CoarseToFine.md | 3 +- src/core/GameRunner.ts | 1 + src/core/game/Game.ts | 1 + src/core/game/GameImpl.ts | 16 ++++- src/core/game/TerrainMapLoader.ts | 9 +++ .../MultiSourceAnyTargetBFS.test.ts | 58 +++++++++++++++++++ tests/util/Setup.ts | 11 +++- 7 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/core/pathfinding/MultiSourceAnyTargetBFS.test.ts diff --git a/docs/CoarseToFine.md b/docs/CoarseToFine.md index 463a50556..f79a5b058 100644 --- a/docs/CoarseToFine.md +++ b/docs/CoarseToFine.md @@ -13,10 +13,11 @@ Yes. The terrain loader already ships multiple resolutions per map: - `manifest.map4x` + `map4x.bin` (coarser) - `manifest.map16x` + `map16x.bin` (even coarser) -At runtime we already load both: +At runtime we load: - `gameMap`: full res for normal games (or `map4x` for compact games) - `miniGameMap`: lower res (`map4x` for normal games, or `map16x` for compact games) +- `microGameMap`: always `map16x` (in compact games this is the same instance as `miniGameMap`) So we can prototype coarse-to-fine without extending mapgen first. diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index ed8c8cd7b..421f36630 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -70,6 +70,7 @@ export async function createGameRunner( nations, gameMap.gameMap, gameMap.miniGameMap, + gameMap.microGameMap, config, ); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 9c5ef95ff..46d6fdfa9 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -673,6 +673,7 @@ export interface Game extends GameMap { height(): number; map(): GameMap; miniMap(): GameMap; + microMap(): GameMap; forEachTile(fn: (tile: TileRef) => void): void; // Zero-allocation neighbor iteration (cardinal only) to avoid creating arrays forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index e76d9fdaf..073f5000b 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -49,14 +49,24 @@ export function createGame( nations: Nation[], gameMap: GameMap, miniGameMap: GameMap, + microGameMap: GameMap, config: Config, ): Game { // Precompute and cache water-component IDs once per map instance. getWaterComponentIds(gameMap); getWaterComponentIds(miniGameMap); + getWaterComponentIds(microGameMap); const stats = new StatsImpl(); - return new GameImpl(humans, nations, gameMap, miniGameMap, config, stats); + return new GameImpl( + humans, + nations, + gameMap, + miniGameMap, + microGameMap, + config, + stats, + ); } export type CellString = string; @@ -95,6 +105,7 @@ export class GameImpl implements Game { private _nations: Nation[], private _map: GameMap, private miniGameMap: GameMap, + private microGameMap: GameMap, private _config: Config, private _stats: Stats, ) { @@ -205,6 +216,9 @@ export class GameImpl implements Game { miniMap(): GameMap { return this.miniGameMap; } + microMap(): GameMap { + return this.microGameMap; + } addUpdate(update: GameUpdate) { (this.updates[update.type] as GameUpdate[]).push(update); diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 56e998d32..f96d61032 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -6,6 +6,7 @@ export type TerrainMapData = { nations: Nation[]; gameMap: GameMap; miniGameMap: GameMap; + microGameMap: GameMap; }; const loadedMaps = new Map(); @@ -53,6 +54,13 @@ export async function loadTerrainMap( ) : await genTerrainFromBin(manifest.map16x, await mapFiles.map16xBin()); + // Always expose the 16x map (micro map) for coarse heuristics/corridors. + // In compact games, miniMap already is 16x, so we can reuse it. + const microMap = + mapSize === GameMapSize.Normal + ? await genTerrainFromBin(manifest.map16x, await mapFiles.map16xBin()) + : miniMap; + if (mapSize === GameMapSize.Compact) { manifest.nations.forEach((nation) => { nation.coordinates = [ @@ -66,6 +74,7 @@ export async function loadTerrainMap( nations: manifest.nations, gameMap: gameMap, miniGameMap: miniMap, + microGameMap: microMap, }; loadedMaps.set(map, result); return result; diff --git a/tests/core/pathfinding/MultiSourceAnyTargetBFS.test.ts b/tests/core/pathfinding/MultiSourceAnyTargetBFS.test.ts new file mode 100644 index 000000000..9390ed563 --- /dev/null +++ b/tests/core/pathfinding/MultiSourceAnyTargetBFS.test.ts @@ -0,0 +1,58 @@ +import { MultiSourceAnyTargetBFS } from "../../../src/core/pathfinding/MultiSourceAnyTargetBFS"; + +type TileRef = number; + +function makeGridWaterMap(w: number, h: number, water: boolean[]) { + const num = w * h; + if (water.length !== num) throw new Error("bad water array"); + return { + width: () => w, + height: () => h, + x: (ref: TileRef) => ref % w, + y: (ref: TileRef) => Math.floor(ref / w), + isWater: (ref: TileRef) => water[ref] === true, + neighbors: (ref: TileRef) => { + const out: TileRef[] = []; + const x = ref % w; + if (ref >= w) out.push(ref - w); + if (ref < (h - 1) * w) out.push(ref + w); + if (x !== 0) out.push(ref - 1); + if (x !== w - 1) out.push(ref + 1); + return out; + }, + } as any; +} + +describe("MultiSourceAnyTargetBFS", () => { + it("returns king-move (Chebyshev) diagonal routes when enabled", () => { + // 3x3, all water. + const gm = makeGridWaterMap(3, 3, new Array(9).fill(true)); + const bfs = new MultiSourceAnyTargetBFS(9); + + const res = bfs.findWaterPath(gm, [0], [8], { kingMoves: true }); + expect(res).not.toBeNull(); + expect(res!.path).toEqual([0, 4, 8]); + }); + + it("prevents diagonal corner cutting when enabled", () => { + // 2x2: + // S (water) X (land) + // X (land) T (water) + const gm = makeGridWaterMap(2, 2, [true, false, false, true]); + const bfs = new MultiSourceAnyTargetBFS(4); + + const blocked = bfs.findWaterPath(gm, [0], [3], { + kingMoves: true, + noCornerCutting: true, + }); + expect(blocked).toBeNull(); + + const allowed = bfs.findWaterPath(gm, [0], [3], { + kingMoves: true, + noCornerCutting: false, + }); + expect(allowed).not.toBeNull(); + expect(allowed!.path).toEqual([0, 3]); + }); +}); + diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index e9b2722ee..345480ae2 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -39,6 +39,10 @@ export async function setup( currentDir, `../testdata/maps/${mapName}/map4x.bin`, ); + const microMapBinPath = path.join( + currentDir, + `../testdata/maps/${mapName}/map16x.bin`, + ); const manifestPath = path.join( currentDir, `../testdata/maps/${mapName}/manifest.json`, @@ -46,12 +50,17 @@ export async function setup( const mapBinBuffer = fs.readFileSync(mapBinPath); const miniMapBinBuffer = fs.readFileSync(miniMapBinPath); + const microMapBinBuffer = fs.readFileSync(microMapBinPath); const manifest = JSON.parse( fs.readFileSync(manifestPath, "utf8"), ) satisfies MapManifest; const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer); const miniGameMap = await genTerrainFromBin(manifest.map4x, miniMapBinBuffer); + const microGameMap = await genTerrainFromBin( + manifest.map16x, + microMapBinBuffer, + ); // Configure the game const serverConfig = new TestServerConfig(); @@ -78,7 +87,7 @@ export async function setup( false, ); - return createGame(humans, [], gameMap, miniGameMap, config); + return createGame(humans, [], gameMap, miniGameMap, microGameMap, config); } export function playerInfo(name: string, type: PlayerType): PlayerInfo {