import { InfoMap } from "simulation/ai/common-api/map-module.js"; import * as terrainStates from "simulation/ai/common-api/terrain-states.js"; /** * TerrainAnalysis, inheriting from the Map Component. * * This creates a suitable passability map. * This is part of the Shared Script, and thus should only be used for things that are non-player specific. * This.map is a map of the world, where particular stuffs are pointed with a value * For example, impassable land is 0, water is 200, areas near tree (ie forest grounds) are 41… * This is intended for use with 8 bit maps for reduced memory usage. * Upgraded from QuantumState's original TerrainAnalysis for qBot. */ export class TerrainAnalysis extends InfoMap { constructor(sharedScript, rawState) { const passabilityMap = rawState.passabilityMap; const obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"]; const obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"]; const obstructionTiles = new Uint8Array(passabilityMap.data.length); for (let i = 0; i < passabilityMap.data.length; ++i) { obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? terrainStates.IMPASSABLE : terrainStates.LAND; if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === terrainStates.IMPASSABLE) { obstructionTiles[i] = terrainStates.DEEP_WATER; } else if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === terrainStates.LAND) { obstructionTiles[i] = terrainStates.SHALLOW_WATER; } } super(rawState, "passability", obstructionTiles); this.width = passabilityMap.width; this.height = passabilityMap.height; this.cellSize = passabilityMap.cellSize; } } /** * Accessibility inherits from TerrainAnalysis * * This can easily and efficiently determine if any two points are connected. * it can also determine if any point is "probably" reachable, assuming the unit can get close enough * for optimizations it's called after the TerrainAnalyser has finished initializing his map * so this can use the land regions already. */ export class Accessibility extends InfoMap { constructor(rawState, terrainAnalyser) { super(rawState, "passability", terrainAnalyser.map); this.landPassMap = new Uint16Array(terrainAnalyser.length); this.navalPassMap = new Uint16Array(terrainAnalyser.length); this.maxRegions = 65535; this.regionSize = []; this.regionType = []; // "inaccessible", "land" or "water"; // ID of the region associated with an array of region IDs. this.regionLinks = []; // initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch); // However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.) // So start at 2. this.regionID = 2; for (let i = 0; i < this.landPassMap.length; ++i) { if (this.map[i] !== terrainStates.IMPASSABLE) { // any non-painted, non-inacessible area. if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false)) this.regionType[this.regionID++] = "land"; if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true)) this.regionType[this.regionID++] = "water"; } else if (this.landPassMap[i] == 0) { // any non-painted, inacessible area. this.floodFill(i, 1, false); this.floodFill(i, 1, true); } } // calculating region links. Regions only touching diagonaly are not linked. // since we're checking all of them, we'll check from the top left to the bottom right const w = this.width; for (let x = 0; x < this.width-1; ++x) { for (let y = 0; y < this.height-1; ++y) { // checking right. const thisLID = this.landPassMap[x+y*w]; const thisNID = this.navalPassMap[x+y*w]; const rightLID = this.landPassMap[x+1+y*w]; const rightNID = this.navalPassMap[x+1+y*w]; const bottomLID = this.landPassMap[x+y*w+w]; const bottomNID = this.navalPassMap[x+y*w+w]; if (thisLID > 1) { if (rightNID > 1) if (this.regionLinks[thisLID].indexOf(rightNID) === -1) this.regionLinks[thisLID].push(rightNID); if (bottomNID > 1) if (this.regionLinks[thisLID].indexOf(bottomNID) === -1) this.regionLinks[thisLID].push(bottomNID); } if (thisNID > 1) { if (rightLID > 1) if (this.regionLinks[thisNID].indexOf(rightLID) === -1) this.regionLinks[thisNID].push(rightLID); if (bottomLID > 1) if (this.regionLinks[thisNID].indexOf(bottomLID) === -1) this.regionLinks[thisNID].push(bottomLID); if (thisLID > 1) if (this.regionLinks[thisNID].indexOf(thisLID) === -1) this.regionLinks[thisNID].push(thisLID); } } } // Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255); // Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255); } getAccessValue(position, onWater) { const gamePos = this.gamePosToMapPos(position); if (onWater) return this.navalPassMap[gamePos[0] + this.width*gamePos[1]]; let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]]; if (ret === 1) { // quick spiral search. const indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]]; for (const i of indx) { const id0 = gamePos[0] + i[0]; const id1 = gamePos[1] + i[1]; if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width) continue; ret = this.landPassMap[id0 + this.width*id1]; if (ret !== 1) return ret; } } return ret; } getTrajectTo(start, end) { const pstart = this.gamePosToMapPos(start); const istart = pstart[0] + pstart[1]*this.width; const pend = this.gamePosToMapPos(end); const iend = pend[0] + pend[1]*this.width; let onLand = true; if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1) onLand = false; if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1) return false; let endRegion = this.landPassMap[iend]; if (endRegion <= 1 && this.navalPassMap[iend] > 1) endRegion = this.navalPassMap[iend]; else if (endRegion <= 1) return false; const startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart]; return this.getTrajectToIndex(startRegion, endRegion); } /** * Return a "path" of accessibility indexes from one point to another, including the start and the end indexes * this can tell you what sea zone you need to have a dock on, for example. * assumes a land unit unless start point is over deep water. */ getTrajectToIndex(istart, iend) { if (istart === iend) return [istart]; const trajects = new Set(); const explored = new Set(); trajects.add([istart]); explored.add(istart); while (trajects.size) { for (const traj of trajects) { const ilast = traj[traj.length-1]; for (const inew of this.regionLinks[ilast]) { if (inew === iend) return traj.concat(iend); if (explored.has(inew)) continue; trajects.add(traj.concat(inew)); explored.add(inew); } trajects.delete(traj); } } return undefined; } getRegionSize(position, onWater) { const pos = this.gamePosToMapPos(position); const index = pos[0] + pos[1]*this.width; const ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index]; if (this.regionSize[ID] === undefined) return 0; return this.regionSize[ID]; } getRegionSizei(index, onWater) { if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined)) return 0; if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]]) return this.regionSize[this.navalPassMap[index]]; return this.regionSize[this.landPassMap[index]]; } /** Implementation of a fast flood fill. Reasonably good performances for JS. */ floodFill(startIndex, value, onWater) { if (value > this.maxRegions) { error("AI accessibility map: too many regions."); this.landPassMap[startIndex] = 1; this.navalPassMap[startIndex] = 1; return false; } if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0) return false; // already painted. let floodFor = "land"; if (this.map[startIndex] === terrainStates.IMPASSABLE) { this.landPassMap[startIndex] = 1; this.navalPassMap[startIndex] = 1; return false; } if (onWater === true) { if (this.map[startIndex] !== terrainStates.DEEP_WATER && this.map[startIndex] !== terrainStates.SHALLOW_WATER) { this.navalPassMap[startIndex] = 1; // impassable for naval return false; // do nothing } floodFor = "water"; } else if (this.map[startIndex] === terrainStates.DEEP_WATER) { this.landPassMap[startIndex] = 1; // impassable for land return false; } // here we'll be able to start. for (let i = this.regionSize.length; i <= value; ++i) { this.regionLinks.push([]); this.regionSize.push(0); this.regionType.push("inaccessible"); } const w = this.width; const h = this.height; // Get x and y from index const IndexArray = [startIndex]; let newIndex; while (IndexArray.length) { newIndex = IndexArray.pop(); let y = 0; // vertical iteration while (true) { --y; const index = newIndex + w*y; if (index < 0) break; if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== terrainStates.IMPASSABLE && this.map[index] !== terrainStates.DEEP_WATER) { continue; } if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === terrainStates.DEEP_WATER || this.map[index] === terrainStates.SHALLOW_WATER)) { continue; } break; } // should actually break ++y; let reachLeft = false; let reachRight = false; let index; do { index = newIndex + w*y; if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== terrainStates.IMPASSABLE && this.map[index] !== terrainStates.DEEP_WATER) { this.landPassMap[index] = value; this.regionSize[value]++; } else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === terrainStates.DEEP_WATER || this.map[index] === terrainStates.SHALLOW_WATER)) { this.navalPassMap[index] = value; this.regionSize[value]++; } else break; if (index%w > 0) { if (floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== terrainStates.IMPASSABLE && this.map[index -1] !== terrainStates.DEEP_WATER) { if (!reachLeft) { IndexArray.push(index -1); reachLeft = true; } } else if (floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === terrainStates.DEEP_WATER || this.map[index -1] === terrainStates.SHALLOW_WATER)) { if (!reachLeft) { IndexArray.push(index -1); reachLeft = true; } } else if (reachLeft) reachLeft = false; } if (index%w < w - 1) { if (floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== terrainStates.IMPASSABLE && this.map[index +1] !== terrainStates.DEEP_WATER) { if (!reachRight) { IndexArray.push(index +1); reachRight = true; } } else if (floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === terrainStates.DEEP_WATER || this.map[index +1] === terrainStates.SHALLOW_WATER)) { if (!reachRight) { IndexArray.push(index +1); reachRight = true; } } else if (reachRight) reachRight = false; } ++y; } while (index/w < h-1); // should actually break } return true; } }