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.
This commit is contained in:
scamiv
2025-12-27 16:35:10 +01:00
parent e08acdf09c
commit 2776294220
7 changed files with 96 additions and 3 deletions
+2 -1
View File
@@ -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.
+1
View File
@@ -70,6 +70,7 @@ export async function createGameRunner(
nations,
gameMap.gameMap,
gameMap.miniGameMap,
gameMap.microGameMap,
config,
);
+1
View File
@@ -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;
+15 -1
View File
@@ -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);
+9
View File
@@ -6,6 +6,7 @@ export type TerrainMapData = {
nations: Nation[];
gameMap: GameMap;
miniGameMap: GameMap;
microGameMap: GameMap;
};
const loadedMaps = new Map<GameMapType, TerrainMapData>();
@@ -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;
@@ -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]);
});
});
+10 -1
View File
@@ -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 {