Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js (revision 20055) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/map-module.js (revision 20056) @@ -1,342 +1,349 @@ var API3 = function(m) { /** * The map module. * Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values. */ /** The function needs to be named too because of the copyConstructor functionality */ m.Map = function Map(sharedScript, type, originalMap, actualCopy) { // get the correct dimensions according to the map type let map = type === "territory" || type === "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap; this.width = map.width; this.height = map.height; this.cellSize = map.cellSize; this.length = this.width * this.height; this.maxVal = 255; // sanity check if (originalMap && originalMap.length !== this.length) warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length); if (originalMap && actualCopy) { this.map = new Uint8Array(this.length); for (let i = 0; i < this.length; ++i) this.map[i] = originalMap[i]; } else if (originalMap) this.map = originalMap; else this.map = new Uint8Array(this.length); }; m.Map.prototype.setMaxVal = function(val) { this.maxVal = val; }; m.Map.prototype.gamePosToMapPos = function(p) { return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)]; }; m.Map.prototype.point = function(p) { let q = this.gamePosToMapPos(p); q[0] = q[0] >= this.width ? this.width-1 : (q[0] < 0 ? 0 : q[0]); q[1] = q[1] >= this.width ? this.width-1 : (q[1] < 0 ? 0 : q[1]); return this.map[q[0] + this.width * q[1]]; }; m.Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type = "linear") { strength = strength ? strength : maxDist; let x0 = Math.floor(Math.max(0, cx - maxDist)); let y0 = Math.floor(Math.max(0, cy - maxDist)); let x1 = Math.floor(Math.min(this.width-1, cx + maxDist)); let y1 = Math.floor(Math.min(this.height-1, cy + maxDist)); let maxDist2 = maxDist * maxDist; // code duplicating for speed if (type === "linear") { let str = strength / maxDist; for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist - Math.sqrt(r2)); let w = x + y * this.width; if (this.map[w] + quant < 0) this.map[w] = 0; else if (this.map[w] + quant > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += quant; } } } else if (type === "quadratic") { let str = strength / maxDist2; for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist2 - r2); let w = x + y * this.width; if (this.map[w] + quant < 0) this.map[w] = 0; else if (this.map[w] + quant > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += quant; } } } else { for (let y = y0; y < y1; ++y) { let dy = y - cy; for (let x = x0; x < x1; ++x) { let dx = x - cx; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let w = x + y * this.width; if (this.map[w] + strength < 0) this.map[w] = 0; else if (this.map[w] + strength > this.maxVal) this.map[w] = this.maxVal; // avoids overflow. else this.map[w] += strength; } } } }; m.Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type = "constant") { strength = strength ? +strength : +maxDist; let x0 = Math.max(0, cx - maxDist); let y0 = Math.max(0, cy - maxDist); let x1 = Math.min(this.width, cx + maxDist); let y1 = Math.min(this.height, cy + maxDist); let maxDist2 = maxDist * maxDist; if (type === "linear") { let str = strength / maxDist; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist - Math.sqrt(r2)) * this.map[x + y * this.width]; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } else if (type === "quadratic") { let str = strength / maxDist2; for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = str * (maxDist2 - r2) * this.map[x + y * this.width]; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } else { for (let y = y0; y < y1; ++y) { for (let x = x0; x < x1; ++x) { let dx = x - cx; let dy = y - cy; let r2 = dx*dx + dy*dy; if (r2 >= maxDist2) continue; let quant = this.map[x + y * this.width] * strength; if (quant < 0) this.map[x + y * this.width] = 0; else if (quant > this.maxVal) this.map[x + y * this.width] = this.maxVal; else this.map[x + y * this.width] = quant; } } } }; /** add to current map by the parameter map pixelwise */ m.Map.prototype.add = function(map) { for (let i = 0; i < this.length; ++i) { if (this.map[i] + map.map[i] < 0) this.map[i] = 0; else if (this.map[i] + map.map[i] > this.maxVal) this.map[i] = this.maxVal; else this.map[i] += map.map[i]; } }; /** Find the best non-obstructed tile */ m.Map.prototype.findBestTile = function(radius, obstruction) { let bestIdx; let bestVal = -1; for (let j = 0; j < this.length; ++j) { if (this.map[j] <= bestVal) continue; let i = this.getNonObstructedTile(j, radius, obstruction); if (i < 0) continue; bestVal = this.map[j]; bestIdx = i; } return [bestIdx, bestVal]; }; /** return any non obstructed (small) tile inside the (big) tile i from obstruction map */ m.Map.prototype.getNonObstructedTile = function(i, radius, obstruction) { let ratio = this.cellSize / obstruction.cellSize; let ix = (i % this.width) * ratio; let iy = Math.floor(i / this.width) * ratio; let w = obstruction.width; for (let kx = ix; kx < ix + ratio; ++kx) { if (kx < radius || kx >= w - radius) continue; for (let ky = iy; ky < iy + ratio; ++ky) { if (ky < radius || ky >= w - radius) continue; if (obstruction.isObstructedTile(kx, ky, radius)) continue; return kx + ky*w; } } return -1; }; /** return true if the area centered on tile kx-ky and with radius is obstructed */ m.Map.prototype.isObstructedTile = function(kx, ky, radius) { let w = this.width; if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] === 0) return true; + if (!this.pattern || this.pattern[0] != radius) + { + this.pattern = [radius]; + let r2 = radius * radius; + for (let i = 1; i <= radius; ++i) + this.pattern.push(Math.floor(Math.sqrt(r2 - (i-0.5)*(i-0.5)) + 0.5)); + } for (let dy = 0; dy <= radius; ++dy) { - let dxmax = dy === 0 ? radius : Math.ceil(Math.sqrt(radius*radius - (dy-0.5)*(dy-0.5))); + let dxmax = this.pattern[dy]; let xp = kx + (ky + dy)*w; let xm = kx + (ky - dy)*w; for (let dx = -dxmax; dx <= dxmax; ++dx) if (this.map[xp + dx] === 0 || this.map[xm + dx] === 0) return true; } return false; }; /** * returns the nearest obstructed point * TODO check that the landpassmap index is the same */ m.Map.prototype.findNearestObstructed = function(k, radius) { let w = this.width; let ix = k % w; let iy = Math.floor(k / w); let n = this.cellSize > 8 ? 1 : Math.floor(8 / this.cellSize); for (let i = 1; i <= n; ++i) { let kx = ix - i; let ky = iy + i; for (let j = 1; j <= 8*i; ++j) { if (this.isObstructedTile(kx, ky, radius)) { let akx = Math.abs(kx-ix); let aky = Math.abs(ky-iy); if (akx >= aky) { if (kx > ix) --kx; else ++kx; } if (aky >= akx) { if (ky > iy) --ky; else ++ky; } return kx + w*ky; } if (j < 2*i+1) ++kx; else if (j < 4*i+1) --ky; else if (j < 6*i+1) --kx; else ++ky; } } return -1; }; m.Map.prototype.dumpIm = function(name = "default.png", threshold = this.maxVal) { Engine.DumpImage(name, this.map, this.width, this.height, threshold); }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 20055) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 20056) @@ -1,217 +1,220 @@ var PETRA = function(m) { /** map functions */ m.TERRITORY_PLAYER_MASK = 0x1F; m.TERRITORY_BLINKING_MASK = 0x40; m.createObstructionMap = function(gameState, accessIndex, template) { let passabilityMap = gameState.getPassabilityMap(); let territoryMap = gameState.ai.territoryMap; let ratio = territoryMap.cellSize / passabilityMap.cellSize; // default values let placementType = "land"; let buildOwn = true; let buildAlly = true; let buildNeutral = true; let buildEnemy = false; // If there is a template then replace the defaults if (template) { placementType = template.buildPlacementType(); buildOwn = template.hasBuildTerritory("own"); buildAlly = template.hasBuildTerritory("ally"); buildNeutral = template.hasBuildTerritory("neutral"); buildEnemy = template.hasBuildTerritory("enemy"); } let obstructionTiles = new Uint8Array(passabilityMap.data.length); let passMap; let obstructionMask; if (placementType == "shore") { passMap = gameState.ai.accessibility.navalPassMap; obstructionMask = gameState.getPassabilityClassMask("building-shore"); } else { passMap = gameState.ai.accessibility.landPassMap; obstructionMask = gameState.getPassabilityClassMask("building-land"); } for (let k = 0; k < territoryMap.data.length; ++k) { let tilePlayer = territoryMap.data[k] & m.TERRITORY_PLAYER_MASK; let isConnected = (territoryMap.data[k] & m.TERRITORY_BLINKING_MASK) == 0; if (tilePlayer === PlayerID) { if (!buildOwn || !buildNeutral && !isConnected) continue; } else if (gameState.isPlayerMutualAlly(tilePlayer)) { if (!buildAlly || !buildNeutral && !isConnected) continue; } else if (tilePlayer === 0) { if (!buildNeutral) continue; } else { if (!buildEnemy) continue; } let x = ratio * (k % territoryMap.width); let y = ratio * Math.floor(k / territoryMap.width); for (let ix = 0; ix < ratio; ++ix) { for (let iy = 0; iy < ratio; ++iy) { let i = x + ix + (y + iy)*passabilityMap.width; if (placementType != "shore" && accessIndex && accessIndex !== passMap[i]) continue; if (!(passabilityMap.data[i] & obstructionMask)) obstructionTiles[i] = 255; } } } let map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); map.setMaxVal(255); if (template && template.buildDistance()) { let distance = template.buildDistance(); - let minDist = +distance.MinDistance; + let minDist = distance && distance.MinDistance ? +distance.MinDistance : 0; if (minDist) { + let obstructionRadius = template.obstructionRadius(); + if (obstructionRadius) + minDist -= obstructionRadius.min; let fromClass = distance.FromClass; let cellSize = passabilityMap.cellSize; let cellDist = 1 + minDist / cellSize; let structures = gameState.getOwnStructures().filter(API3.Filters.byClass(fromClass)); for (let ent of structures.values()) { if (!ent.position()) continue; let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); map.addInfluence(x, z, cellDist, -255, "constant"); } } } return map; }; m.createTerritoryMap = function(gameState) { let map = gameState.ai.territoryMap; let ret = new API3.Map(gameState.sharedScript, "territory", map.data); ret.getOwner = function(p) { return this.point(p) & m.TERRITORY_PLAYER_MASK; }; ret.getOwnerIndex = function(p) { return this.map[p] & m.TERRITORY_PLAYER_MASK; }; return ret; }; /** * The borderMap contains some border and frontier information: * - border of the map filled once: * - all mini-cells (1x1) from the big cell (8x8) inaccessibles => bit 0 * - inside a given distance to the border => bit 1 * - frontier of our territory (updated regularly in updateFrontierMap) * - narrow border (inside our territory) => bit 2 * - large border (inside our territory, exclusive of narrow) => bit 3 */ m.outside_Mask = 1; m.border_Mask = 2; m.fullBorder_Mask = m.outside_Mask | m.border_Mask; m.narrowFrontier_Mask = 4; m.largeFrontier_Mask = 8; m.fullFrontier_Mask = m.narrowFrontier_Mask | m.largeFrontier_Mask; m.createBorderMap = function(gameState) { let map = new API3.Map(gameState.sharedScript, "territory"); let width = map.width; let border = Math.round(80 / map.cellSize); let passabilityMap = gameState.getPassabilityMap(); let obstructionMask = gameState.getPassabilityClassMask("unrestricted"); if (gameState.circularMap) { let ic = (width - 1) / 2; let radcut = (ic - border) * (ic - border); for (let j = 0; j < map.length; ++j) { let dx = j%width - ic; let dy = Math.floor(j/width) - ic; let radius = dx*dx + dy*dy; if (radius < radcut) continue; map.map[j] = m.outside_Mask; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = m.border_Mask; break; } } } else { let borderCut = width - border; for (let j = 0; j < map.length; ++j) { let ix = j%width; let iy = Math.floor(j/width); if (ix < border || ix >= borderCut || iy < border || iy >= borderCut) { map.map[j] = m.outside_Mask; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = m.border_Mask; break; } } } } // map.dumpIm("border.png", 5); return map; }; m.debugMap = function(gameState, map) { let width = map.width; let cell = map.cellSize; gameState.getEntities().forEach( function (ent) { let pos = ent.position(); if (!pos) return; let x = Math.round(pos[0] / cell); let z = Math.round(pos[1] / cell); let id = x + width*z; if (map.map[id] == 1) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); else if (map.map[id] == 2) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); else if (map.map[id] == 3) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,2]}); }); }; return m; }(PETRA);