diff --git a/binaries/data/mods/public/simulation/ai/common-api/map-module.js b/binaries/data/mods/public/simulation/ai/common-api/map-module.js index a67173130e..0e48488935 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/map-module.js +++ b/binaries/data/mods/public/simulation/ai/common-api/map-module.js @@ -388,7 +388,7 @@ m.Map.prototype.add = function(map) } }; -m.Map.prototype.findBestTile = function(radius, obstructionTiles) +m.Map.prototype.findBestTile = function(radius, obstruction) { // Find the best non-obstructed tile let bestIdx = 0; @@ -398,8 +398,8 @@ m.Map.prototype.findBestTile = function(radius, obstructionTiles) let v = this.map[i]; if (v > bestVal) { - var j = API3.getMaxMapIndex(i, this, obstructionTiles); - if (obstructionTiles.map[j] <= radius) + let j = this.getNonObstructedTile(i, radius, obstruction); + if (j < 0) continue; bestVal = v; bestIdx = j; @@ -409,6 +409,94 @@ m.Map.prototype.findBestTile = function(radius, obstructionTiles) 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 is 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; + for (let dy = 0; dy <= radius; ++dy) + { + let dxmax = radius - 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(i, radius) +{ + var w = this.width; + var ix = i % w; + var iy = Math.floor(i / w); + var 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; +}; + // returns the point with the lowest (but still > radius) point in the immediate vicinity m.Map.prototype.findLowestNeighbor = function(x,y,radius) { diff --git a/binaries/data/mods/public/simulation/ai/common-api/shared.js b/binaries/data/mods/public/simulation/ai/common-api/shared.js index 2715266bb0..ae468f3cd3 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/shared.js +++ b/binaries/data/mods/public/simulation/ai/common-api/shared.js @@ -31,9 +31,9 @@ m.SharedScript = function(settings) this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. // Resource maps data. - // By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4"). + // By how much to divide the resource amount when filling the map (ie a tree having 200 wood is "4"). this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; -} +}; //Return a simple object (using no classes etc) that will be serialized //into saved games diff --git a/binaries/data/mods/public/simulation/ai/petra/baseManager.js b/binaries/data/mods/public/simulation/ai/petra/baseManager.js index da2c45095d..0d49c05767 100644 --- a/binaries/data/mods/public/simulation/ai/petra/baseManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/baseManager.js @@ -294,7 +294,6 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) // The AI will currently not build a CC if it wouldn't connect with an existing CC. var obstructions = m.createObstructionMap(gameState, this.accessIndex, template); - obstructions.expandInfluences(); var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))).toEntityArray(); var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); @@ -303,15 +302,15 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) var bestVal = undefined; var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); - var territoryMap = gameState.sharedScript.territoryMap; + var territoryMap = gameState.ai.HQ.territoryMap; var width = territoryMap.width; var cellSize = territoryMap.cellSize; for (var p = 0; p < this.territoryIndices.length; ++p) { var j = this.territoryIndices[p]; - var i = API3.getMaxMapIndex(j, territoryMap, obstructions); - if (obstructions.map[i] <= radius) // check room around + var i = territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) // no room around continue; // we add 3 times the needed resource and once the other two (not food) @@ -384,7 +383,7 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) if (bestVal !== undefined && total < bestVal) continue; bestVal = total; - bestIdx = j; + bestIdx = i; } if (this.Config.debug > 2) @@ -392,9 +391,9 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource) if (bestVal <= 0) return {"quality": bestVal, "pos": [0, 0]}; - var i = API3.getMaxMapIndex(bestIdx, territoryMap, obstructions); - var x = ((i % obstructions.width) + 0.5) * obstructions.cellSize; - var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + + var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return {"quality": bestVal, "pos": [x, z]}; }; diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js index 64217f99b2..7b20313855 100644 --- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -552,7 +552,6 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); - obstructions.expandInfluences(); var ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); var dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"]))); @@ -588,16 +587,16 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, { if (this.territoryMap.getOwnerIndex(j) != 0) continue; - // We require that it is accessible - var index = gameState.ai.accessibility.landPassMap[j]; + // with enough room around to build the cc + var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + // we require that it is accessible + var index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips === 0 && proxyAccess !== index) continue; - // and with enough room around to build the cc - var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions); - if (obstructions.map[i] <= radius) - continue; var norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // checking distance to other cc @@ -680,7 +679,7 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, if (bestVal !== undefined && val < bestVal) continue; bestVal = val; - bestIdx = j; + bestIdx = i; } Engine.ProfileStop(); @@ -693,13 +692,12 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, // not good enough. if (bestVal < cut) return false; - - var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions); - var x = (i % obstructions.width + 0.5) * obstructions.cellSize; - var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + + var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base - var index = gameState.ai.accessibility.landPassMap[i]; + var index = gameState.ai.accessibility.landPassMap[bestIdx]; for (var base of this.baseManagers) { if (!base.anchor || base.accessIndex === index) @@ -709,7 +707,7 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, this.navalManager.setMinimalTransportShips(gameState, sea, 1); } - return [x,z]; + return [x, z]; }; // Returns the best position to build a new Civil Centre @@ -738,7 +736,6 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); - obstructions.expandInfluences(); var bestIdx = undefined; var bestVal = undefined; @@ -753,13 +750,13 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; - // We require that it is accessible - var index = gameState.ai.accessibility.landPassMap[j]; - if (!this.landRegions[index]) + // with enough room around to build the cc + var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) continue; - // and with enough room around to build the cc - var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions); - if (obstructions.map[i] <= radius) + // we require that it is accessible + var index = gameState.ai.accessibility.landPassMap[i]; + if (!this.landRegions[index]) continue; // checking distances to other cc @@ -818,7 +815,7 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) if (bestVal !== undefined && currentVal > bestVal) continue; bestVal = currentVal; - bestIdx = j; + bestIdx = i; } if (this.Config.debug > 1) @@ -829,12 +826,11 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) if (bestVal === undefined) return undefined; - var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions); - var x = (i % obstructions.width + 0.5) * obstructions.cellSize; - var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base - var index = gameState.ai.accessibility.landPassMap[i]; + var index = gameState.ai.accessibility.landPassMap[bestIdx]; for (var base of this.baseManagers) { if (!base.anchor || base.accessIndex === index) @@ -844,7 +840,7 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } - return [x,z]; + return [x, z]; }; // Returns the best position to build a new market: if the allies already have a market, build it as far as possible @@ -862,7 +858,6 @@ m.HQ.prototype.findMarketLocation = function(gameState, template) // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); - obstructions.expandInfluences(); var bestIdx = undefined; var bestVal = undefined; @@ -880,8 +875,8 @@ m.HQ.prototype.findMarketLocation = function(gameState, template) if (this.basesMap.map[j] == 0) // only in our territory continue; // with enough room around to build the cc - var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions); - if (obstructions.map[i] <= radius) + var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) continue; var index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) @@ -907,7 +902,7 @@ m.HQ.prototype.findMarketLocation = function(gameState, template) if (bestVal !== undefined && maxDist < bestVal) continue; bestVal = maxDist; - bestIdx = j; + bestIdx = i; } if (this.Config.debug > 1) @@ -924,9 +919,8 @@ m.HQ.prototype.findMarketLocation = function(gameState, template) (expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnStructures().filter(API3.Filters.byClass("BarterMarket")).length > 0))) return false; - var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions); - var x = (i % obstructions.width + 0.5) * obstructions.cellSize; - var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, this.basesMap.map[bestIdx], expectedGain]; }; @@ -953,9 +947,9 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template) // obstruction map var obstructions = m.createObstructionMap(gameState, 0, template); - obstructions.expandInfluences(); var bestIdx = undefined; + var bestJdx = undefined; var bestVal = undefined; var width = this.territoryMap.width; var cellSize = this.territoryMap.cellSize; @@ -979,9 +973,9 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template) } if (this.basesMap.map[j] == 0) // inaccessible cell continue; - // and with enough room around to build the cc - var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions); - if (obstructions.map[i] <= radius) + // with enough room around to build the cc + var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) continue; var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; @@ -1037,16 +1031,16 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template) if (bestVal !== undefined && minDist > bestVal) continue; bestVal = minDist; - bestIdx = j; + bestIdx = i; + bestJdx = j; } if (bestVal === undefined) return undefined; - var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions); - var x = (i % obstructions.width + 0.5) * obstructions.cellSize; - var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.basesMap.map[bestIdx]]; + var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + return [x, z, this.basesMap.map[bestJdx]]; }; m.HQ.prototype.buildTemple = function(gameState, queues) diff --git a/binaries/data/mods/public/simulation/ai/petra/map-module.js b/binaries/data/mods/public/simulation/ai/petra/map-module.js index 8c55a0d9ea..04d159acc6 100644 --- a/binaries/data/mods/public/simulation/ai/petra/map-module.js +++ b/binaries/data/mods/public/simulation/ai/petra/map-module.js @@ -8,6 +8,7 @@ m.createObstructionMap = function(gameState, accessIndex, template) { var passabilityMap = gameState.getMap(); var territoryMap = gameState.ai.territoryMap; + var ratio = territoryMap.cellSize / passabilityMap.cellSize; // default values var placementType = "land"; @@ -28,110 +29,43 @@ m.createObstructionMap = function(gameState, accessIndex, template) if (placementType == "shore") { - if (passabilityMap.cellSize == 4) - { - var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") - | gameState.getPassabilityClassMask("building-shore") - | gameState.getPassabilityClassMask("default"); - var obstructionDefault = gameState.getPassabilityClassMask("default"); - } - else // new pathFinder branch - { - var obstructionMask = gameState.getPassabilityClassMask("default-no-clearance") - | gameState.getPassabilityClassMask("building-shore") - | gameState.getPassabilityClassMask("default-terrain-only"); - var obstructionDefault = gameState.getPassabilityClassMask("default-terrain-only"); - } - var okay = false; - for (var x = 0; x < passabilityMap.width; ++x) - { - for (var y = 0; y < passabilityMap.height; ++y) - { - var i = x + y*passabilityMap.width; - var xter = Math.floor((x+0.5)*passabilityMap.cellSize / territoryMap.cellSize); - var yter = Math.floor((y+0.5)*passabilityMap.cellSize / territoryMap.cellSize); - var iter = xter + yter*territoryMap.width; - var tilePlayer = (territoryMap.data[iter] & m.TERRITORY_PLAYER_MASK); - - if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0) - { - obstructionTiles[i] = 0; - continue; - } - if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | obstructionDefault))) - { - obstructionTiles[i] = 0; - continue; - } - - okay = false; - var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]]; - var available = 0; - for (var stuff of positions) - { - var index = x + stuff[0] + (y+stuff[1])*passabilityMap.width; - var index2 = x + stuff[0]*2 + (y+stuff[1]*2)*passabilityMap.width; - var index3 = x + stuff[0]*3 + (y+stuff[1]*3)*passabilityMap.width; - var index4 = x + stuff[0]*4 + (y+stuff[1]*4)*passabilityMap.width; - - if ((passabilityMap.data[index] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index,true) > 500) - if ((passabilityMap.data[index2] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index2,true) > 500) - if ((passabilityMap.data[index3] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index3,true) > 500) - if ((passabilityMap.data[index4] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index4,true) > 500) - { - if (available < 2) - available++; - else - okay = true; - } - } - // checking for accessibility: if a neighbor is inaccessible, this is too. If it's not on the same "accessible map" as us, we crash-i~u. - var radius = 3; - for (var xx = -radius;xx <= radius; xx++) - for (var yy = -radius;yy <= radius; yy++) - { - var id = x + xx + (y+yy)*passabilityMap.width; - if (id > 0 && id < passabilityMap.data.length) - if (gameState.ai.terrainAnalyzer.map[id] === 0 || gameState.ai.terrainAnalyzer.map[id] == 30 || gameState.ai.terrainAnalyzer.map[id] == 40) - okay = false; - } - obstructionTiles[i] = okay ? 255 : 0; - } - - } + var passMap = gameState.ai.accessibility.navalPassMap; + var obstructionMask = gameState.getPassabilityClassMask("building-shore"); } else { - var playerID = PlayerID; - if (passabilityMap.cellSize == 4) - var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction") - | gameState.getPassabilityClassMask("building-land"); - else // new pathFinder branch - var obstructionMask = gameState.getPassabilityClassMask("default-no-clearance") - | gameState.getPassabilityClassMask("building-land"); - - for (var i = 0; i < passabilityMap.data.length; ++i) + var passMap = gameState.ai.accessibility.landPassMap; + var obstructionMask = gameState.getPassabilityClassMask("building-land"); + } + + if (passabilityMap.cellSize == 4) + var obstructionMask = obstructionMask | gameState.getPassabilityClassMask("foundationObstruction"); + else // new pathFinder branch + var obstructionMask = obstructionMask | gameState.getPassabilityClassMask("default-no-clearance"); + + for (var k = 0; k < territoryMap.data.length; ++k) + { + let tilePlayer = (territoryMap.data[k] & m.TERRITORY_PLAYER_MASK); + if ((!buildNeutral && tilePlayer == 0) || + (!buildOwn && tilePlayer == PlayerID) || + (!buildAlly && tilePlayer != PlayerID && gameState.isPlayerAlly(tilePlayer)) || + (!buildEnemy && tilePlayer != 0 && gameState.isPlayerEnemy(tilePlayer))) + continue; + let x = ratio * (k % territoryMap.width); + let y = ratio * (Math.floor(k / territoryMap.width)); + for (let ix = 0; ix < ratio; ++ix) { - var x = i % passabilityMap.width; - var y = Math.floor(i / passabilityMap.width); - var xter = Math.floor((x+0.5)*passabilityMap.cellSize / territoryMap.cellSize); - var yter = Math.floor((y+0.5)*passabilityMap.cellSize / territoryMap.cellSize); - var iter = xter + yter*territoryMap.width; - var tilePlayer = (territoryMap.data[iter] & m.TERRITORY_PLAYER_MASK); - var invalidTerritory = ( - (!buildOwn && tilePlayer == playerID) || - (!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) || - (!buildNeutral && tilePlayer == 0) || - (!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0) - ); - if (accessIndex) - var tileAccessible = (accessIndex === gameState.ai.accessibility.landPassMap[i]); - else - var tileAccessible = true; - obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 255; + 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; + } } } - + var map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles); map.setMaxVal(255); @@ -145,9 +79,9 @@ m.createObstructionMap = function(gameState, accessIndex, template) if (ent.buildCategory() === category && ent.position()) { var pos = ent.position(); - var x = Math.round(pos[0] / gameState.cellSize); - var z = Math.round(pos[1] / gameState.cellSize); - map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant'); + var x = Math.round(pos[0] / passabilityMap.cellSize); + var z = Math.round(pos[1] / passabilityMap.cellSize); + map.addInfluence(x, z, minDist/passability.cellSize, -255, 'constant'); } }); } diff --git a/binaries/data/mods/public/simulation/ai/petra/navalManager.js b/binaries/data/mods/public/simulation/ai/petra/navalManager.js index 560238e43d..1d2dbd9e69 100644 --- a/binaries/data/mods/public/simulation/ai/petra/navalManager.js +++ b/binaries/data/mods/public/simulation/ai/petra/navalManager.js @@ -407,12 +407,11 @@ m.NavalManager.prototype.splitTransport = function(gameState, plan) * create a transport from a garrisoned ship to a land location * needed at start game when starting with a garrisoned ship */ -m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos) +m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) { let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); if (fromAccess !== 1) return; - let toAccess = gameState.ai.accessibility.getAccessValue(toPos); if (toAccess < 2) return; diff --git a/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js b/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js index fc3355ddb4..9bd3720ead 100644 --- a/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js +++ b/binaries/data/mods/public/simulation/ai/petra/queueplan-building.js @@ -71,22 +71,23 @@ m.ConstructionPlan.prototype.start = function(gameState) this.metadata.base = pos.base; if (pos.access) - this.metadata.access = pos.access; // needed for Docks for the position is on water + this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildCategory() === "Dock") { - // try to place it a bit inside the land if possible - let cosang = Math.cos(pos.angle); - let sinang = Math.sin(pos.angle); - if (this.template.get("Obstruction") && this.template.get("Obstruction/Static")) - var radius = (+this.template.get("Obstruction/Static/@depth"))/2; - else - var radius = 0; - for (let step = 0; step < radius; step += 4) - builders[0].construct(this.type, pos.x+step*sinang, pos.z+step*cosang, - pos.angle, this.metadata); + // adjust a bit the position if needed + // TODO we would need groundLevel and waterLevel to do it properly + let cosa = Math.cos(pos.angle); + let sina = Math.sin(pos.angle); + let shiftMax = gameState.ai.HQ.territoryMap.cellSize; + for (let shift = 0; shift <= shiftMax; shift += 2) + { + builders[0].construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); + if (shift > 0) + builders[0].construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); + } } else if (pos.x == pos.xx && pos.z == pos.zz) builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); @@ -99,9 +100,9 @@ m.ConstructionPlan.prototype.start = function(gameState) this.onStart(gameState); Engine.ProfileStop(); - // TODO should have a ConstructionStarted event in case the construct order fails + // TODO should have a ConstructionStarted even in case the construct order fails if (this.metadata && this.metadata.proximity) - gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z]); + gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; // TODO for dock, we should allow building them outside territory, and we should check that we are along the right sea @@ -157,13 +158,6 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState) } } - // First, find all tiles that are far enough away from obstructions: - - var obstructions = m.createObstructionMap(gameState, 0, template); - obstructions.expandInfluences(); - - //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); - // Compute each tile's closeness to friendly structures: var placement = new API3.Map(gameState.sharedScript, "territory"); @@ -291,12 +285,16 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState) } } } - - // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this - // allows room for units to walk between buildings. + + // Find the best non-obstructed: + // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, + // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit + var obstructions = m.createObstructionMap(gameState, 0, template); + //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); + var radius = 0; if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop") || this.type === gameState.applyCiv("structures/{civ}_elephant_stables")) @@ -306,7 +304,6 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState) else radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); - // Find the best non-obstructed if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first @@ -327,17 +324,35 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState) var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize; var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; + var xx = x; + var zz = z; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) - var secondBest = obstructions.findLowestNeighbor(x,z); - else - var secondBest = [x,z]; + { + if (obstructions.cellSize != 4) // new pathFinder branch + { + let secondBest = obstructions.findNearestObstructed(bestIdx, radius); + if (secondBest >= 0) + { + x = ((secondBest % obstructions.width) + 0.5) * obstructions.cellSize; + z = (Math.floor(secondBest / obstructions.width) + 0.5) * obstructions.cellSize; + xx = x; + zz = z; + } + } + else + { + obstructions.expandInfluences(); + let secondBest = obstructions.findLowestNeighbor(x,z); + xx = secondBest[0]; + zz = secondBest[1]; + } + } - var territorypos = placement.gamePosToMapPos([x,z]); - var territoryIndex = territorypos[0] + territorypos[1]*placement.width; + let territorypos = placement.gamePosToMapPos([x,z]); + let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": secondBest[0], "zz": secondBest[1], - "base": gameState.ai.HQ.basesMap.map[territoryIndex] }; + return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": xx, "zz": zz, "base": gameState.ai.HQ.basesMap.map[territoryIndex] }; }; /** @@ -348,14 +363,16 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { var template = this.template; - var cellSize = gameState.cellSize; // size of each tile var territoryMap = gameState.ai.HQ.territoryMap; var obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); var bestIdx = undefined; - var bestVal = 0; + var bestJdx = undefined; + var bestAngle = undefined; + var bestLand = undefined; + var bestVal = -1; var landPassMap = gameState.ai.accessibility.landPassMap; var navalPassMap = gameState.ai.accessibility.navalPassMap; @@ -366,69 +383,98 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState) if (this.metadata.proximity) proxyAccess = gameState.ai.accessibility.getAccessValue(this.metadata.proximity); + var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize); + + var halfSize = 0; // used for dock angle + var halfDepth = 0; // used by checkPlacement + var halfWidth = 0; // used by checkPlacement + if (template.get("Footprint/Square")) + { + halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; + halfDepth = +template.get("Footprint/Square/@depth") / 2; + halfWidth = +template.get("Footprint/Square/@width") / 2; + } + else if (template.get("Footprint/Circle")) + { + halfSize = +template.get("Footprint/Circle/@radius"); + halfDepth = halfSize; + halfWidth = halfSize; + } + + var maxres = 10; for (let j = 0; j < territoryMap.length; ++j) { - if (obstructions.map[j] <= 0) + var i = territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + + var landAccess = this.getLandAccess(gameState, i, radius+1, obstructions.width); + if (landAccess.size == 0) continue; if (this.metadata) { - if (this.metadata.land && this.metadata.land.indexOf(landPassMap[j]) === -1) + if (this.metadata.land && !landAccess.has(+this.metadata.land)) continue; - if (this.metadata.sea && navalPassMap[j] !== this.metadata.sea) + if (this.metadata.sea && navalPassMap[i] != +this.metadata.sea) continue; - if (nbShips === 0 && proxyAccess && proxyAccess > 1 && landPassMap[j] !== proxyAccess) + if (nbShips === 0 && proxyAccess && proxyAccess > 1 && !landAccess.has(proxyAccess)) continue; } - let tileOwner = territoryMap.getOwnerIndex(j); - if (tileOwner !== 0 && gameState.isPlayerEnemy(tileOwner)) - continue; + var res = Math.min(maxres, this.getResourcesAround(gameState, j, 80)); + + var dist; if (this.metadata.proximity) { // if proximity is given, we look for the nearest point - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - let dist = API3.SquareVectorDistance(this.metadata.proximity, pos); - if (bestIdx !== undefined && dist > bestVal) - continue; - bestVal = dist; - bestIdx = j; + var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + dist = API3.SquareVectorDistance(this.metadata.proximity, pos); + dist = Math.sqrt(dist) + 15 * (maxres - res); } else { // if not in our (or allied) territory, we do not want it too far to be able to defend it - let nearby = m.getFrontierProximity(gameState, j); - if (nearby > 4) + dist = m.getFrontierProximity(gameState, j); + if (dist > 4) continue; - bestVal = 1; - bestIdx = j; + dist = dist + 0.4 * (maxres - res) } + if (bestIdx !== undefined && dist > bestVal) + continue; + + var x = ((i % obstructions.width) + 0.5) * obstructions.cellSize; + var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; + var angle = this.getDockAngle(gameState, x, z, halfSize); + if (angle === false) + continue; + var land = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); + if (land < 2 || !gameState.ai.HQ.landRegions[land]) + continue; + if (this.metadata.proximity && gameState.ai.accessibility.regionSize[land] < 4000) + continue; + + bestVal = dist; + bestIdx = i; + bestJdx = j; + bestAngle = angle; + bestLand = land; } - if (bestVal <= 0) + if (bestVal < 0) return false; - var x = ((bestIdx % territoryMap.width) + 0.5) * cellSize; - var z = (Math.floor(bestIdx / territoryMap.width) + 0.5) * cellSize; - - // Needed for dock placement whose position will be changed - var access = gameState.ai.accessibility.getAccessValue([x, z]); - - // for Dock placement, we need to improve the position of the building as the position given here - // is only the position on the shore, while the need the position of the center of the building - // We also need to find the angle of the building - var angle = this.getDockAngle(gameState, x, z); - if (angle === false) - return false; + var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize; + var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base - var baseIndex = gameState.ai.HQ.basesMap.map[bestIdx]; + var baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; if (!baseIndex) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; - if (base.accessIndex !== access) + if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; @@ -442,32 +488,28 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState) } } - return { "x": x, "z": z, "angle": angle, "xx": x, "zz": z, "base": baseIndex, "access": access }; + return { "x": x, "z": z, "angle": bestAngle, "xx": x, "zz": z, "base": baseIndex, "access": bestLand }; }; -// Algorithm taken from the function GetDockAngle in helpers/Commands.js -m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z) +// Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js +m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { - var radius = this.template.obstructionRadius(); - if (!radius) - return false; - var pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); var j = pos[0] + pos[1]*gameState.ai.accessibility.width; var seaRef = gameState.ai.accessibility.navalPassMap[j]; + if (seaRef < 2) + return false; const numPoints = 16; - for (var dist = 0; dist < 2; ++dist) + for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { - var angle = (i/numPoints)*2*Math.PI; - var pos = [ x - (2+dist)*radius*Math.sin(angle), z + (2+dist)*radius*Math.cos(angle)]; - var pos = gameState.ai.accessibility.gamePosToMapPos(pos); - var j = pos[0] + pos[1]*gameState.ai.accessibility.width; - var seaAccess = gameState.ai.accessibility.navalPassMap[j]; - var landAccess = gameState.ai.accessibility.landPassMap[j]; - if (seaAccess == seaRef && landAccess < 2) + let angle = (i/numPoints)*2*Math.PI; + pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; + pos = gameState.ai.accessibility.gamePosToMapPos(pos); + let j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.navalPassMap[j] == seaRef) waterPoints.push(i); } var length = waterPoints.length; @@ -504,6 +546,126 @@ m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z) return false; }; +// Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js +// to determine the special dock requirements +m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) +{ + let sz = halfDepth * Math.sin(angle); + let cz = halfDepth * Math.cos(angle); + // center back position + let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); + let j = pos[0] + pos[1]*gameState.ai.accessibility.width; + let ret = gameState.ai.accessibility.landPassMap[j]; + if (ret < 2) + return 0; + // center front position + pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) + return 0; + // additional constraints compared to BuildRestriction.js to assure we have enough place to build + let sw = halfWidth * Math.cos(angle) * 3 / 4; + let cw = halfWidth * Math.sin(angle) * 3 / 4; + pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] != ret) + return 0; + pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); + j = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (gameState.ai.accessibility.landPassMap[j] != ret) + return 0; + return ret; +}; + +// get the list of all the land access from this position +m.ConstructionPlan.prototype.getLandAccess = function(gameState, i, radius, w) +{ + var access = new Set(); + var landPassMap = gameState.ai.accessibility.landPassMap; + var kx = i % w; + var ky = Math.floor(i / w); + var land; + for (let dy = 0; dy <= radius; ++dy) + { + let dxmax = radius - dy; + let xp = kx + (ky + dy)*w; + let xm = kx + (ky - dy)*w; + for (let dx = -dxmax; dx <= dxmax; ++dx) + { + if (kx + dx < 0 || kx + dx >= w) + continue; + if (ky + dy >= 0 && ky + dy < w) + { + land = landPassMap[xp + dx]; + if (land > 1 && !access.has(land)) + access.add(land); + } + if (ky - dy >= 0 && ky - dy < w) + { + land = landPassMap[xm + dx]; + if (land > 1 && !access.has(land)) + access.add(land); + } + } + } + return access; +}; + +// get the sum of the resources (except food) around, inside a given radius +// resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood +m.ConstructionPlan.prototype.getResourcesAround = function(gameState, i, radius) +{ + let resourceMaps = gameState.sharedScript.resourceMaps; + let w = resourceMaps["wood"].width; + let cellSize = resourceMaps["wood"].cellSize; + let size = Math.floor(radius / cellSize); + let ix = i % w; + let iy = Math.floor(i / w); + let total = 0; + let nbcell = 0; + for (let k in resourceMaps) + { + if (k === "food") + continue; + let weigh0 = (k === "wood") ? 2 : 1; + for (let dy = 0; dy <= size; ++dy) + { + let dxmax = size - dy; + let ky = iy + dy; + if (ky >= 0 && ky < w) + { + for (let dx = -dxmax; dx <= dxmax; ++dx) + { + let kx = ix + dx; + if (kx < 0 || kx >= w) + continue; + let ddx = (dx > 0) ? dx : -dx; + let weight = weigh0 * (dxmax - ddx) / size; + total += weight * resourceMaps[k].map[kx + w * ky]; + nbcell += weight; + } + } + if (dy == 0) + continue; + ky = iy - dy; + if (ky >= 0 && ky < w) + { + for (let dx = -dxmax; dx <= dxmax; ++dx) + { + let kx = ix + dx; + if (kx < 0 || kx >= w) + continue; + let ddx = (dx > 0) ? dx : -dx; + let weight = weigh0 * (dxmax - ddx) / size; + total += weight * resourceMaps[k].map[kx + w * ky]; + nbcell += weight; + } + } + } + } + return (nbcell ? (total / nbcell) : 0); +}; + m.ConstructionPlan.prototype.Serialize = function() { let prop = { diff --git a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js index fbfdedc316..edccce1210 100644 --- a/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js +++ b/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js @@ -88,6 +88,7 @@ m.HQ.prototype.assignStartingEntities = function(gameState) this.navalRegions[sea] = true; // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport + // when a construction will start (see createTransportIfNeeded) if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) for (let id of ent.garrisoned()) ent.unload(id);