Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json (revision 17704) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/data.json (revision 17705) @@ -1,7 +1,7 @@ { "name": "Petra Bot", - "description": "Petra is the default 0AD AI bot. Please report issues to Wildfire Games (see the link in the main menu).\n\nThe AI has a malus-bonus on resource stockpiling (either gathering rate or trade gain) varying from 0.5 for Sandbox to 1.6 for Very Hard (Medium = 1.0). In addition, the Sandbox level does not expand nor attack.", + "description": "Petra is the default 0AD AI bot. Please report issues to Wildfire Games (see the link in the main menu).\n\nThe AI has a bonus/penalty on resource stockpiling (either gathering rate or trade gain) varying from 0.5 for Sandbox to 1.6 for Very Hard (Medium = 1.0). In addition, the Sandbox level does not expand nor attack.", "moduleName" : "PETRA", "constructor": "PetraBot", "useShared": true } Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 17704) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/mapModule.js (revision 17705) @@ -1,273 +1,231 @@ var PETRA = function(m) { // other map functions m.TERRITORY_PLAYER_MASK = 0x1F; 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"; var buildOwn = true; var buildAlly = true; var buildNeutral = true; var 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"); } var obstructionTiles = new Uint8Array(passabilityMap.data.length); var passMap; var 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); 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) { 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); if (template && template.buildDistance()) { let minDist = +template.buildDistance().MinDistance; let fromClass = template.buildDistance().FromClass; if (minDist && 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) { var map = gameState.ai.territoryMap; var 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; }; // flag cells around the border of the map (2 if all points into that cell are inaccessible, 1 otherwise) m.createBorderMap = function(gameState) { var map = new API3.Map(gameState.sharedScript, "territory"); var width = map.width; var border = Math.round(80 / map.cellSize); var passabilityMap = gameState.sharedScript.passabilityMap; var obstructionMask = gameState.getPassabilityClassMask("unrestricted"); if (gameState.ai.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] = 2; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = 1; 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] = 2; let ind = API3.getMapIndices(j, map, passabilityMap); for (let k of ind) { if (passabilityMap.data[k] & obstructionMask) continue; map.map[j] = 1; break; } } } } // map.dumpIm("border.png", 5); return map; }; // map of our frontier : 2 means narrow border, 1 means large border m.createFrontierMap = function(gameState) { var territoryMap = gameState.ai.HQ.territoryMap; var borderMap = gameState.ai.HQ.borderMap; const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; var map = new API3.Map(gameState.sharedScript, "territory"); var width = map.width; var insideSmall = Math.round(45 / map.cellSize); var insideLarge = Math.round(80 / map.cellSize); // should be about the range of towers for (var j = 0; j < territoryMap.length; ++j) { if (territoryMap.getOwnerIndex(j) !== PlayerID || borderMap.map[j] > 1) continue; var ix = j%width; var iz = Math.floor(j/width); for (var a of around) { var jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; var jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (!gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) { map.map[j] = 2; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] > 1) continue; if (!gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) map.map[j] = 1; } } // m.debugMap(gameState, map); return map; }; -// return a measure of the proximity to our frontier (including our allies) -// 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 64m, 4=less than 96m, 5=above 96m -m.getFrontierProximity = function(gameState, j) -{ - var territoryMap = gameState.ai.HQ.territoryMap; - var borderMap = gameState.ai.HQ.borderMap; - const around = [ [-0.7,0.7], [0,1], [0.7,0.7], [1,0], [0.7,-0.7], [0,-1], [-0.7,-0.7], [-1,0] ]; - - var width = territoryMap.width; - var step = Math.round(24 / territoryMap.cellSize); - - if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(j))) - return 0; - - var ix = j%width; - var iz = Math.floor(j/width); - var best = 5; - for (let a of around) - { - for (let i = 1; i < 5; ++i) - { - let jx = ix + Math.round(i*step*a[0]); - if (jx < 0 || jx >= width) - continue; - var jz = iz + Math.round(i*step*a[1]); - if (jz < 0 || jz >= width) - continue; - if (borderMap.map[jx+width*jz] > 1) - continue; - if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) - { - best = Math.min(best, i); - break; - } - } - if (best === 1) - break; - } - - return best; -}; - m.debugMap = function(gameState, map) { var width = map.width; var cell = map.cellSize; gameState.getEntities().forEach( function (ent) { var pos = ent.position(); if (!pos) return; var x = Math.round(pos[0] / cell); var z = Math.round(pos[1] / cell); var 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); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 17704) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 17705) @@ -1,758 +1,756 @@ var PETRA = function(m) { /* Naval Manager Will deal with anything ships. -Basically trade over water (with fleets and goals commissioned by the economy manager) -Defense over water (commissioned by the defense manager) -subtask being patrols, escort, naval superiority. -Transport of units over water (a few units). -Scouting, ultimately. Also deals with handling docks, making sure we have access and stuffs like that. Does not build them though, that's for the base manager to handle. */ m.NavalManager = function(Config) { this.Config = Config; // ship subCollections. Also exist for land zones, idem, not caring. this.seaShips = []; this.seaTransportShips = []; this.seaWarShips = []; this.seaFishShips = []; // wanted NB per zone. this.wantedTransportShips = []; this.wantedWarShips = []; this.wantedFishShips = []; // needed NB per zone. this.neededTransportShips = []; this.neededWarShips = []; this.transportPlans = []; // shore-line regions where we can load and unload units this.landingZones = {}; }; // More initialisation for stuff that needs the gameState m.NavalManager.prototype.init = function(gameState, deserializing) { // finished docks this.docks = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClassesOr(["Dock", "Shipyard"]), API3.Filters.not(API3.Filters.isFoundation()))); this.docks.registerUpdates(); this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); // note: those two can overlap (some transport ships are warships too and vice-versa). this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.warShips.registerUpdates(); this.fishShips.registerUpdates(); var fishes = gameState.getFishableSupplies(); var availableFishes = {}; for (let fish of fishes.values()) { let sea = gameState.ai.accessibility.getAccessValue(fish.position(), true); fish.setMetadata(PlayerID, "sea", sea); if (availableFishes[sea]) availableFishes[sea] += fish.resourceSupplyAmount(); else availableFishes[sea] = fish.resourceSupplyAmount(); } for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (!gameState.ai.HQ.navalRegions[i]) { // push dummies this.seaShips.push(undefined); this.seaTransportShips.push(undefined); this.seaWarShips.push(undefined); this.seaFishShips.push(undefined); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } else { let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaShips.push(collec); collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaTransportShips.push(collec); collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaWarShips.push(collec); collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaFishShips.push(collec); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); if (availableFishes[i] && availableFishes[i] > 1000) this.wantedFishShips.push(this.Config.Economy.targetNumFishers); else this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } } // load units and buildings from the config files let civ = gameState.civ(); if (civ in this.Config.buildings.naval) this.bNaval = this.Config.buildings.naval[civ]; else this.bNaval = this.Config.buildings.naval['default']; for (let i in this.bNaval) this.bNaval[i] = gameState.applyCiv(this.bNaval[i]); if (deserializing) return; // determination of the possible landing zones var width = gameState.getMap().width; var length = width * gameState.getMap().height; for (let i = 0; i < length; ++i) { let land = gameState.ai.accessibility.landPassMap[i]; if (land < 2) continue; let naval = gameState.ai.accessibility.navalPassMap[i]; if (naval < 2) continue; if (!this.landingZones[land]) this.landingZones[land] = {}; if (!this.landingZones[land][naval]) this.landingZones[land][naval] = new Set(); this.landingZones[land][naval].add(i); } // and keep only thoses with enough room around when possible for (let land in this.landingZones) { for (let sea in this.landingZones[land]) { let landing = this.landingZones[land][sea]; let nbaround = {}; let nbcut = 0; for (let i of landing) { let nb = 0; if (landing.has(i-1)) nb++; if (landing.has(i+1)) nb++; if (landing.has(i+width)) nb++; if (landing.has(i-width)) nb++; nbaround[i] = nb; nbcut = Math.max(nb, nbcut); } nbcut = Math.min(2, nbcut); for (let i of landing) { if (nbaround[i] < nbcut) landing.delete(i); } } } // Assign our initial docks and ships for (let ship of this.ships.values()) this.setShipIndex(gameState, ship); for (let dock of this.docks.values()) this.setDockIndex(gameState, dock); }; m.NavalManager.prototype.resetFishingBoats = function(gameState) { for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) this.wantedFishShips[i] = 0; }; m.NavalManager.prototype.setShipIndex = function(gameState, ship) { let sea = gameState.ai.accessibility.getAccessValue(ship.position(), true); ship.setMetadata(PlayerID, "sea", sea); }; m.NavalManager.prototype.setDockIndex = function(gameState, dock) { var land = dock.getMetadata(PlayerID, "access"); if (land === undefined) { land = this.getDockIndex(gameState, dock, false); dock.setMetadata(PlayerID, "access", land); } var sea = dock.getMetadata(PlayerID, "sea"); if (sea === undefined) { sea = this.getDockIndex(gameState, dock, true); dock.setMetadata(PlayerID, "sea", sea); } }; // get the indices for our starting docks and those of our allies // land index when onWater=false, sea indes when true m.NavalManager.prototype.getDockIndex = function(gameState, dock, onWater) { var index = gameState.ai.accessibility.getAccessValue(dock.position(), onWater); if (index < 2) { // pre-positioned docks are sometimes not well positionned var dockPos = dock.position(); var radius = dock.footprintRadius(); for (let i = 0; i < 16; i++) { let pos = [ dockPos[0] + radius*Math.cos(i*Math.PI/8), dockPos[1] + radius*Math.sin(i*Math.PI/8)]; index = gameState.ai.accessibility.getAccessValue(pos, onWater); if (index >= 2) break; } } if (index < 2) API3.warn("ERROR in Petra navalManager because of dock position (onWater=" + onWater + ") index " + index); return index; }; // get the list of seas around this region not connected by a dock m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { var seas = gameState.ai.accessibility.regionLinks[region].slice(); var docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); docks.forEach(function (dock) { if (dock.getMetadata(PlayerID, "access") !== region) return; let i = seas.indexOf(dock.getMetadata(PlayerID, "sea")); if (i !== -1) seas.splice(i--,1); }); return seas; }; m.NavalManager.prototype.checkEvents = function(gameState, queues, events) { for (let evt of events.ConstructionFinished) { if (!evt || !evt.newentity) continue; let entity = gameState.getEntityById(evt.newentity); if (entity && entity.hasClass("Dock") && entity.isOwn(PlayerID)) this.setDockIndex(gameState, entity); } for (let evt of events.TrainingFinished) { if (!evt || !evt.entities) continue; for (let entId of evt.entities) { let entity = gameState.getEntityById(entId); if (!entity || !entity.hasClass("Ship") || !entity.isOwn(PlayerID)) continue; this.setShipIndex(gameState, entity); } } for (let evt of events.Destroy) { if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) continue; if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) continue; var plan = this.getPlan(evt.metadata[PlayerID].transporter); if (!plan) continue; var shipId = evt.entityObj.id(); if (this.Config.debug > 1) API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); if (plan.state === "boarding") { // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan plan.units.forEach(function (ent) { if ((ent.getMetadata(PlayerID, "onBoard") === "onBoard" && ent.position()) || ent.getMetadata(PlayerID, "onBoard") === shipId) ent.setMetadata(PlayerID, "onBoard", undefined); }); plan.needTransportShips = (plan.transportShips.length === 0); } else if (plan.state === "sailing") { var endIndex = plan.endIndex; var self = this; plan.units.forEach(function (ent) { if (!ent.position()) // unit from another ship of this plan ... do nothing return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); let endPos = ent.getMetadata(PlayerID, "endPos"); ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // nothing else to do if access = endIndex as already at destination // otherwise, we should require another transport // TODO if attacking and no more ships available, remove the units from the attack // to avoid delaying it too much if (access !== endIndex) self.requireTransport(gameState, ent, access, endIndex, endPos); }); } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to === PlayerID) { let ent = gameState.getEntityById(evt.entity); if (ent && ent.hasClass("Dock")) this.setDockIndex(gameState, ent); } } }; m.NavalManager.prototype.getPlan = function(ID) { for (let plan of this.transportPlans) if (plan.ID === ID) return plan; return undefined; }; m.NavalManager.prototype.addPlan = function(plan) { this.transportPlans.push(plan); }; // complete already existing plan or create a new one for this requirement // (many units can then call this separately and end up in the same plan) // TODO check garrison classes m.NavalManager.prototype.requireTransport = function(gameState, entity, startIndex, endIndex, endPos) { if (entity.getMetadata(PlayerID, "transport") !== undefined) { if (this.Config.debug > 0) API3.warn("Petra naval manager error: unit " + entity.id() + " has already required a transport"); return false; } for (let plan of this.transportPlans) { if (plan.startIndex !== startIndex || plan.endIndex !== endIndex) continue; if (plan.state !== "boarding") continue; plan.addUnit(entity, endPos); return true; } let plan = new m.TransportPlan(gameState, [entity], startIndex, endIndex, endPos); if (plan.failed) { if (this.Config.debug > 1) API3.warn(">>>> transport plan aborted <<<<"); return false; } plan.init(gameState); this.transportPlans.push(plan); return true; }; // split a transport plan in two, moving all entities not yet affected to a ship in the new plan m.NavalManager.prototype.splitTransport = function(gameState, plan) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan started <<<<"); var newplan = new m.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); if (newplan.failed) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan aborted <<<<"); return false; } newplan.init(gameState); var nbUnits = 0; plan.units.forEach(function (ent) { if (ent.getMetadata(PlayerID, "onBoard")) return; ++nbUnits; newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); }); if (this.Config.debug > 1) API3.warn(">>>> previous plan left with units " + plan.units.length); if (nbUnits) this.transportPlans.push(newplan); return (nbUnits !== 0); }; /** * 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, toAccess) { let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); if (fromAccess !== 1) return; if (toAccess < 2) return; for (let ship of this.ships.values()) { if (!ship.isGarrisonHolder() || !ship.garrisoned().length) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined) continue; let units = []; for (let entId of ship.garrisoned()) units.push(gameState.getEntityById(entId)); // TODO check that the garrisoned units have not another purpose let plan = new m.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); if (plan.failed) continue; plan.init(gameState); this.transportPlans.push(plan); } }; // set minimal number of needed ships when a new event (new base or new attack plan) m.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) { if (!sea) return; if (this.wantedTransportShips[sea] < number ) this.wantedTransportShips[sea] = number; }; // bumps up the number of ships we want if we need more. m.NavalManager.prototype.checkLevels = function(gameState, queues) { if (queues.ships.length() !== 0) return; for (let sea = 0; sea < this.neededTransportShips.length; sea++) this.neededTransportShips[sea] = 0; for (let plan of this.transportPlans) { if (!plan.needTransportShips || plan.units.length < 2) continue; let sea = plan.sea; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) continue; ++this.neededTransportShips[sea]; if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) { ++this.wantedTransportShips[sea]; return; } } for (let sea = 0; sea < this.neededTransportShips.length; sea++) if (this.neededTransportShips[sea] > 2) ++this.wantedTransportShips[sea]; }; m.NavalManager.prototype.maintainFleet = function(gameState, queues) { if (queues.ships.length() > 0) return; if (gameState.getOwnEntitiesByClass("Dock", true).filter(API3.Filters.isBuilt()).length + gameState.getOwnEntitiesByClass("Shipyard", true).filter(API3.Filters.isBuilt()).length === 0) return; // check if we have enough transport ships per region. for (var sea = 0; sea < this.seaShips.length; ++sea) { if (this.seaShips[sea] === undefined) continue; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) continue; if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) { var template = this.getBestShip(gameState, sea, "transport"); if (template) { queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); continue; } } if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) { let template = this.getBestShip(gameState, sea, "fishing"); if (template) { queues.ships.addPlan(new m.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); continue; } } } }; // assigns free ships to plans that need some m.NavalManager.prototype.assignShipsToPlans = function(gameState) { for (let plan of this.transportPlans) if (plan.needTransportShips) plan.assignShip(gameState); }; // let blocking ships move apart from active ships (waiting for a better pathfinder) m.NavalManager.prototype.moveApart = function(gameState) { var self = this; this.ships.forEach(function(ship) { if (ship.hasClass("FishingBoat")) // small ships should not be a problem return; var sea = ship.getMetadata(PlayerID, "sea"); if (ship.getMetadata(PlayerID, "transporter") === undefined) { if (ship.isIdle()) // do not stay idle near a dock to not disturb other ships { gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).forEach(function(dock) { if (dock.getMetadata(PlayerID, "sea") !== sea) return; if (API3.SquareVectorDistance(ship.position(), dock.position()) > 2500) return; ship.moveApart(dock.position(), 50); }); } return; } // if transporter ship not idle, move away other ships which could block it self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).forEach(function(ship) { if (ship.getMetadata(PlayerID, "route") === undefined) return; var sea = ship.getMetadata(PlayerID, "sea"); self.seaShips[sea].forEach(function(blockingShip) { if (blockingShip === ship || !blockingShip.isIdle()) return; if (API3.SquareVectorDistance(ship.position(), blockingShip.position()) > 900) return; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(ship.position(), 12); else blockingShip.moveApart(ship.position(), 6); }); }); }; m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) return; if (gameState.getPopulation() > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).length === 0 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_dock")) { let dockStarted = false; for (let base of gameState.ai.HQ.baseManagers) { if (dockStarted) break; if (!base.anchor || base.constructing) continue; let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); for (let sea of remaining) { if (!gameState.ai.HQ.navalRegions[sea]) continue; - queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": [base.accessIndex], "sea": sea })); + let wantedLand = {}; + wantedLand[base.accessIndex] = true; + queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "land": wantedLand, "sea": sea })); dockStarted = true; break; } } } } if (gameState.currentPhase() < 2 || gameState.getPopulation() < this.Config.Economy.popForTown + 15 || queues.militaryBuilding.length() !== 0 || this.bNaval.length === 0) return; var docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!docks.length) return; var nNaval = 0; for (let naval of this.bNaval) nNaval += gameState.countEntitiesAndQueuedByType(naval, true); if (nNaval === 0 || (nNaval < this.bNaval.length && gameState.getPopulation() > 120)) { for (let naval of this.bNaval) { if (gameState.countEntitiesAndQueuedByType(naval, true) < 1 && gameState.ai.HQ.canBuild(gameState, naval)) { - let land = []; + let wantedLand = {}; for (let base of gameState.ai.HQ.baseManagers) - { - if (!base.anchor) - continue; - if (land.indexOf(base.accessIndex) === -1) - land.push(base.accessIndex); - } + if (base.anchor) + wantedLand[base.accessIndex] = true; let sea = docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); - queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, naval, { "land": land, "sea": sea })); + queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, naval, { "land": wantedLand, "sea": sea })); break; } } } }; // goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) m.NavalManager.prototype.getBestShip = function(gameState, sea, goal) { var civ = gameState.civ(); var trainableShips = []; gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { var trainables = ent.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isDisabledTemplates(trainable)) continue; let template = gameState.getTemplate(trainable); if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) trainableShips.push(trainable); } }); var best = 0; var bestShip; var limits = gameState.getEntityLimits(); var current = gameState.getEntityCounts(); for (let trainable of trainableShips) { let template = gameState.getTemplate(trainable); if (!template.available(gameState)) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; let arrows = +(template.getDefaultArrow() || 0); if (goal === "attack") // choose the maximum default arrows { if (best > arrows) continue; best = arrows; } else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport { let capacity = +(template.garrisonMax() || 0); if (capacity < 2) continue; capacity += 10*arrows; if (MatchesClassList(template.garrisonableClasses(), "Siege")) capacity += 50; if (best > capacity) continue; best = capacity; } else if (goal === "fishing") if (!template.hasClass("FishingBoat")) continue; bestShip = trainable; } return bestShip; }; m.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); this.checkEvents(gameState, queues, events); // close previous transport plans if finished for (let i = 0; i < this.transportPlans.length; ++i) { let remaining = this.transportPlans[i].update(gameState); if (remaining) continue; if (this.Config.debug > 1) API3.warn("no more units on transport plan " + this.transportPlans[i].ID); this.transportPlans[i].releaseAll(); this.transportPlans.splice(i--, 1); } // assign free ships to plans which need them this.assignShipsToPlans(gameState); // and require for more ships/structures if needed if (gameState.ai.playedTurn % 3 === 0) { this.checkLevels(gameState, queues); this.maintainFleet(gameState, queues); this.buildNavalStructures(gameState, queues); } // let inactive ships move apart from active ones (waiting for a better pathfinder) this.moveApart(gameState); Engine.ProfileStop(); }; m.NavalManager.prototype.Serialize = function() { let properties = { "wantedTransportShips": this.wantedTransportShips, "wantedWarShips": this.wantedWarShips, "wantedFishShips": this.wantedFishShips, "neededTransportShips": this.neededTransportShips, "neededWarShips": this.neededWarShips, "landingZones": this.landingZones }; let transports = {}; for (let plan in this.transportPlans) transports[plan] = this.transportPlans[plan].Serialize(); return { "properties": properties, "transports": transports }; }; m.NavalManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.transportPlans = []; for (let i in data.transports) { let dataPlan = data.transports[i]; let plan = new m.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); plan.Deserialize(dataPlan); plan.init(gameState); this.transportPlans.push(plan); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 17704) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 17705) @@ -1,752 +1,801 @@ var PETRA = function(m) { // Defines a construction plan, ie a building. // We'll try to fing a good position if non has been provided m.ConstructionPlan = function(gameState, type, metadata, position) { if (!m.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; m.ConstructionPlan.prototype = Object.create(m.QueuePlan.prototype); // checks other than resource ones. // TODO: change this. // TODO: if there are specific requirements here, maybe try to do them? m.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; return (gameState.findBuilders(this.type).length > 0); }; m.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); var builders = gameState.findBuilders(this.type).toEntityArray(); // We don't care which builder we assign, since they won't actually // do the building themselves - all we care about is that there is // some unit that can start the foundation var pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.stopBuild(gameState, this.type); Engine.ProfileStop(); return; } else if (this.metadata && this.metadata.expectedGain) { // Check if this market is still worth building (others may have been built making it useless) let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) 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") { // 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.xx === undefined || (pos.x == pos.xx && pos.z == pos.zz)) builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builders[0].construct(this.type, (step*pos.x + (1-step)*pos.xx), (step*pos.z + (1-step)*pos.zz), pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); // 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], this.metadata.access); }; // TODO for dock, we should allow building them outside territory, and we should check that we are along the right sea m.ConstructionPlan.prototype.findGoodPosition = function(gameState) { var template = this.template; if (template.buildCategory() === "Dock") return this.findDockPosition(gameState); if (template.hasClass("Storehouse") && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = gameState.ai.HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; let newpos = base.findBestDropsiteLocation(gameState, type); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = gameState.ai.HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = gameState.ai.HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; else return false; } else if (template.hasClass("DefenseTower") || template.hasClass("Fortress") || template.hasClass("ArmyCamp")) { let pos = gameState.ai.HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; else if (template.hasClass("DefenseTower") || gameState.civ() === "mace" || gameState.civ() === "maur" || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_fortress"), true) > 0 || gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_army_camp"), true) > 0) return false; // if this fortress is our first siege unit builder, just try the standard placement as we want siege units } else if (template.hasClass("Market")) // Docks (i.e. NavalMarket) are done before { let pos = gameState.ai.HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: var placement = new API3.Map(gameState.sharedScript, "territory"); var cellSize = placement.cellSize; // size of each tile var alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. townPhasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] == base) placement.map[j] = 45; } else { for (let j = 0; j < placement.map.length; ++j) if (gameState.ai.HQ.basesMap.map[j] != 0) placement.map[j] = 45; } if (!gameState.ai.HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1) { if (template.hasClass("Field")) placement.addInfluence(x, z, 80/cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80/cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60/cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else if (!ent.hasClass("StoneWall") || ent.hasClass("Gates")) placement.addInfluence(x, z, 60/cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && (!ent.hasClass("Field") && (!ent.hasClass("StoneWall") || ent.hasClass("Gates")))) placement.addInfluence(x, z, 100/cellSize, -25); // move farmsteads away to make room (StoneWall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.genericName() == "House") placement.addInfluence(x, z, 120/cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40/cellSize, -40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - (gameState.sharedScript.resourceMaps.wood.map[j])/3; placement.map[j] = value >= 0 ? value : 0; if (gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] /= 2; // we need space around farmstead, so disfavor map border } } } // requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next market var favorBorder = template.hasClass("BarterMarket"); var disfavorBorder = (gameState.currentPhase() > 1 && !template.hasDefensiveFire()); var preferredBase = (this.metadata && this.metadata.preferredBase); if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] != base) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] == 0 && placement.map[j] > 0) placement.map[j] += 10; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { if (gameState.ai.HQ.basesMap.map[j] == 0) placement.map[j] = 0; else if (favorBorder && gameState.ai.HQ.borderMap.map[j] > 0) placement.map[j] += 50; else if (disfavorBorder && gameState.ai.HQ.borderMap.map[j] == 0 && placement.map[j] > 0) placement.map[j] += 10; if (preferredBase && gameState.ai.HQ.basesMap.map[j] == this.metadata.preferredBase) placement.map[j] += 200; if (placement.map[j] > 0) { let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (gameState.ai.HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } // 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")) radius = Math.floor((template.obstructionRadius() + 12) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClass("House") && !template.hasClass("Field")) radius = Math.ceil((template.obstructionRadius() + 4) / obstructions.cellSize); else radius = Math.ceil((template.obstructionRadius() + 0.5) / obstructions.cellSize); var bestTile; var bestVal; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); bestVal = bestTile[1]; } if (bestVal === undefined || bestVal === -1) { bestTile = placement.findBestTile(radius, obstructions); bestVal = bestTile[1]; } var bestIdx = bestTile[0]; if (bestVal <= 0) return false; let x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined) { 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; } } 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, "base": gameState.ai.HQ.basesMap.map[territoryIndex] }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory */ m.ConstructionPlan.prototype.findDockPosition = function(gameState) { var template = this.template; - var territoryMap = gameState.ai.HQ.territoryMap; var obstructions = m.createObstructionMap(gameState, 0, template); //obstructions.dumpIm(template.buildCategory() + "_obstructions.png"); var bestIdx; var bestJdx; var bestAngle; var bestLand; var bestWater; var bestVal = -1; var navalPassMap = gameState.ai.accessibility.navalPassMap; var width = gameState.ai.HQ.territoryMap.width; var cellSize = gameState.ai.HQ.territoryMap.cellSize; - var nbShips = gameState.ai.HQ.navalManager.transportShips.length; - var proxyAccess; - if (this.metadata.proximity) - proxyAccess = gameState.ai.accessibility.getAccessValue(this.metadata.proximity); + var nbShips = gameState.ai.HQ.navalManager.transportShips.length; + var wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; + var wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; + var proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; + if (nbShips === 0 && proxyAccess && proxyAccess > 1) + { + wantedLand = {}; + wantedLand[proxyAccess] = true; + } + var dropsiteTypes = template.resourceDropsiteTypes(); 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; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; for (let j = 0; j < territoryMap.length; ++j) { - let i = territoryMap.getNonObstructedTile(j, radius, obstructions); - if (i < 0) + if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; - - let landAccess = this.getLandAccess(gameState, i, radius+1, obstructions.width); - if (landAccess.size == 0) - continue; - if (this.metadata) + let dist; + if (!proxyAccess) { - if (this.metadata.land && !landAccess.has(+this.metadata.land)) - continue; - if (this.metadata.sea && navalPassMap[i] != +this.metadata.sea) - continue; - if (nbShips === 0 && proxyAccess && proxyAccess > 1 && !landAccess.has(proxyAccess)) + // if not in our (or allied) territory, we do not want it too far to be able to defend it + dist = this.getFrontierProximity(gameState, j); + if (dist > 4) continue; } + let i = territoryMap.getNonObstructedTile(j, radius, obstructions); + if (i < 0) + continue; + if (wantedSea && navalPassMap[i] !== wantedSea) + continue; - let dist; - let res = Math.min(maxRes, this.getResourcesAround(gameState, j, 80)); + let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - if (this.metadata.proximity) + if (proxyAccess) { // if proximity is given, we look for the nearest point dist = API3.SquareVectorDistance(this.metadata.proximity, pos); dist = Math.sqrt(dist) + 20 * (maxRes - res); } else - { - // if not in our (or allied) territory, we do not want it too far to be able to defend it - dist = m.getFrontierProximity(gameState, j); - if (dist > 4) - continue; dist += 0.6 * (maxRes - res); - } + // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] > 0) dist += 2; // do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && dist > bestVal + maxWater) continue; let x = ((i % obstructions.width) + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle === false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land]) continue; // final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && dist + maxWater - ret.water > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = dist + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) 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[bestJdx]; if (!baseIndex) { for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== bestLand) continue; baseIndex = base.ID; break; } if (!baseIndex) { if (gameState.ai.HQ.numActiveBase() > 0) API3.warn("Petra: dock constructed without base index " + baseIndex); else baseIndex = gameState.ai.HQ.baseManagers[0].ID; } } return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; // Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { var pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); var k = pos[0] + pos[1]*gameState.ai.accessibility.width; var seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { var waterPoints = []; for (let i = 0; i < numPoints; ++i) { 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); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] === seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let count = 0; for (let j = 0; j < (length-1); ++j) { if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } let start = 0; let count = 0; for (let c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; // Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js // to determine the special dock requirements // returns {"land": land index for this dock, "water": amount of water around this spot} 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 land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // 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 null; // 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] != land) return null; 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] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return {"land": land, "water": water}; }; -// get the list of all the land access from this position -m.ConstructionPlan.prototype.getLandAccess = function(gameState, i, radius, w) +/** + * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension + * if the (object) wantedLand is given, this nearest land should have one of these accessibility + * if wantedSea is given, this tile should be inside this sea + */ +const around = [ [ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], + [-1.0, 0.0], [-0.87,-0.50], [-0.50,-0.87], [ 0.0,-1.0], [ 0.50,-0.87], [ 0.87,-0.50] ]; + +m.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { - 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) + var width = gameState.ai.HQ.territoryMap.width; + var cellSize = gameState.ai.HQ.territoryMap.cellSize; + var dist = dimension + 2*cellSize; + + var x = (j%width + 0.5) * cellSize; + var z = (Math.floor(j/width) + 0.5) * cellSize; + for (let a of around) + { + let pos = gameState.ai.accessibility.gamePosToMapPos([x + dist*a[0], z + dist*a[1]]); + if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) + continue; + if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + continue; + let k = pos[0] + pos[1]*gameState.ai.accessibility.width; + let landPass = gameState.ai.accessibility.landPassMap[k]; + if (landPass < 2 || (wantedLand && !wantedLand[landPass])) + continue; + pos = gameState.ai.accessibility.gamePosToMapPos([x - dist*a[0], z - dist*a[1]]); + if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width) + continue; + if (pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) + continue; + k = pos[0] + pos[1]*gameState.ai.accessibility.width; + if (wantedSea && gameState.ai.accessibility.navalPassMap[k] !== wantedSea) + continue; + else if (!wantedSea && gameState.ai.accessibility.navalPassMap[k] < 2) + continue; + return true; + } + + return false; +}; + +/** + * return a measure of the proximity to our frontier (including our allies) + * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m + */ +m.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) +{ + var territoryMap = gameState.ai.HQ.territoryMap; + if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(j))) + return 0; + + var borderMap = gameState.ai.HQ.borderMap; + var width = territoryMap.width; + var step = Math.round(24 / territoryMap.cellSize); + var ix = j % width; + var iz = Math.floor(j / width); + var best = 5; + for (let a of around) + { + for (let i = 1; i < 5; ++i) { - if (kx + dx < 0 || kx + dx >= w) + let jx = ix + Math.round(i*step*a[0]); + if (jx < 0 || jx >= width) 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) + let jz = iz + Math.round(i*step*a[1]); + if (jz < 0 || jz >= width) + continue; + if (borderMap.map[jx+width*jz] > 1) + continue; + if (gameState.isPlayerAlly(territoryMap.getOwnerIndex(jx+width*jz))) { - land = landPassMap[xm + dx]; - if (land > 1 && !access.has(land)) - access.add(land); + best = Math.min(best, i); + break; } } + if (best === 1) + break; } - return access; + + return best; }; + // 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) +m.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, 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) + for (let k of types) { - if (k === "food") + if (k === "food" || !resourceMaps[k]) 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 = { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "lastIsGo": this.lastIsGo, }; let func = { "isGo": uneval(this.isGo), "onStart": uneval(this.onStart) }; return { "prop": prop, "func": func }; }; m.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.prop) this[key] = data.prop[key]; let cost = new API3.Resources(); cost.Deserialize(data.prop.cost); this.cost = cost; for (let fun in data.func) this[fun] = eval(data.func[fun]); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 17704) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 17705) @@ -1,865 +1,877 @@ var PETRA = function(m) { /** * This class makes a worker do as instructed by the economy manager */ m.Worker = function(base) { this.ent = undefined; this.base = base; this.baseID = base.ID; }; m.Worker.prototype.update = function(gameState, ent) { if (!ent.position() || ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3) return; // If we are waiting for a transport or we are sailing, just wait if (ent.getMetadata(PlayerID, "transport") !== undefined) return; // base 0 for unassigned entities has no accessIndex, so take the one from the entity if (this.baseID === gameState.ai.HQ.baseManagers[0].ID) this.accessIndex = gameState.ai.accessibility.getAccessValue(ent.position()); else this.accessIndex = this.base.accessIndex; var subrole = ent.getMetadata(PlayerID, "subrole"); if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... { ent.setMetadata(PlayerID, "subrole", "idle"); this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } this.ent = ent; var unitAIState = ent.unitAIState(); if (unitAIState === "INDIVIDUAL.GATHER.GATHERING" || unitAIState === "INDIVIDUAL.GATHER.APPROACHING" || unitAIState === "INDIVIDUAL.COMBAT.APPROACHING") { if (this.isInaccessibleSupply(gameState) && ((subrole === "hunter" && !this.startHunting(gameState)) || (subrole === "gatherer" && !this.startGathering(gameState)))) ent.stopMoving(); } else if (ent.getMetadata(PlayerID, "approachingTarget")) ent.setMetadata(PlayerID, "approachingTarget", undefined); var unitAIStateOrder = unitAIState.split(".")[1]; // If we're fighting or hunting, let's not start gathering if (unitAIStateOrder === "COMBAT") return; // Okay so we have a few tasks. // If we're gathering, we'll check that we haven't run idle. // And we'll also check that we're gathering a resource we want to gather. if (subrole === "gatherer") { if (ent.isIdle()) { // if we aren't storing resources or it's the same type as what we're about to gather, // let's just pick a new resource. // TODO if we already carry the max we can -> returnresources if (!ent.resourceCarrying() || !ent.resourceCarrying().length || ent.resourceCarrying()[0].type === ent.getMetadata(PlayerID, "gather-type")) { this.startGathering(gameState); } else if (!m.returnResources(gameState, ent)) // try to deposit resources { // no dropsite, abandon old resources and start gathering new ones this.startGathering(gameState); } } else if (unitAIStateOrder === "GATHER") { // we're already gathering. But let's check if there is nothing better // in case UnitAI did something bad if (ent.unitAIOrderData().length) { let supplyId = ent.unitAIOrderData()[0].target; let supply = gameState.getEntityById(supplyId); if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") && supplyId !== ent.getMetadata(PlayerID, "supply")) { let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let gatherType = ent.getMetadata(PlayerID, "gather-type"); let nearby = this.base.dropsiteSupplies[gatherType].nearby; let isNearby = nearby.some(sup => sup.id === supplyId); if (nearby.length === 0 || isNearby) ent.setMetadata(PlayerID, "supply", supplyId); else { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } } } } } else if (unitAIState === "INDIVIDUAL.RETURNRESOURCE.APPROACHING" && gameState.ai.playedTurn % 10 === 0) { // Check from time to time that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.returnResources(gameState, this.ent); } } } else if (subrole === "builder") { if (unitAIStateOrder === "REPAIR") { // update our target in case UnitAI sent us to a different foundation because of autocontinue if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && ent.getMetadata(PlayerID, "target-foundation") !== ent.unitAIOrderData()[0].target) ent.setMetadata(PlayerID, "target-foundation", ent.unitAIOrderData()[0].target); return; } // okay so apparently we aren't working. // Unless we've been explicitely told to keep our role, make us idle. var target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); if (!target || (target.foundationProgress() === undefined && target.needsRepair() === false)) { ent.setMetadata(PlayerID, "subrole", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); // If worker elephant, move away to avoid being trapped in between constructions if (ent.hasClass("Elephant")) this.moveAway(gameState); else if (this.baseID !== gameState.ai.HQ.baseManagers[0].ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } else { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = target.getMetadata(PlayerID, "access"); if (!goalAccess) { goalAccess = gameState.ai.accessibility.getAccessValue(target.position()); target.setMetadata(PlayerID, "access", goalAccess); } if (access === goalAccess) ent.repair(target, target.hasClass("House")); // autocontinue=true for houses else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, goalAccess, target.position()); } } else if (subrole === "hunter") { let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); if (ent.isIdle() && (!lastHuntSearch || (gameState.ai.elapsedTime - lastHuntSearch) > 20)) { if (!this.startHunting(gameState)) { // nothing to hunt around. Try another region if any let nowhereToHunt = true; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let basePos = base.anchor.position(); if (this.startHunting(gameState, basePos)) { ent.setMetadata(PlayerID, "base", base.ID); let access = gameState.ai.accessibility.getAccessValue(ent.position()); if (base.accessIndex === access) ent.move(basePos[0], basePos[1]); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, base.accessIndex, basePos); nowhereToHunt = false; break; } } if (nowhereToHunt) ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } else // Perform some sanity checks { if (unitAIStateOrder === "GATHER" || unitAIStateOrder === "RETURNRESOURCE") { // we may have drifted towards ennemy territory during the hunt, if yes go home let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startHunting(gameState); else if (unitAIState === "INDIVIDUAL.RETURNRESOURCE.APPROACHING") { // Check that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.returnResources(gameState, ent); } } } } } else if (subrole === "fisher") { if (ent.isIdle()) this.startFishing(gameState); else // if we have drifted towards ennemy territory during the fishing, go home { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startFishing(gameState); } } }; m.Worker.prototype.startGathering = function(gameState) { var self = this; var access = gameState.ai.accessibility.getAccessValue(this.ent.position()); // First look for possible treasure if any if (this.gatherTreasure(gameState)) return true; var resource = this.ent.getMetadata(PlayerID, "gather-type"); // If we are gathering food, try to hunt first if (resource === "food" && this.startHunting(gameState)) return true; var findSupply = function(ent, supplies) { var ret = false; for (let i = 0; i < supplies.length; ++i) { // exhausted resource, remove it from this list if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) { supplies.splice(i--, 1); continue; } if (m.IsSupplyFull(gameState, supplies[i].ent)) continue; let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; // check if available resource is worth one additionnal gatherer (except for farms) var nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); if (supplies[i].ent.resourceSupplyType().specific !== "grain" && nbGatherers > 0 && supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; // not in ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally continue; gameState.ai.HQ.AddTCGatherer(supplies[i].id); ent.setMetadata(PlayerID, "supply", supplies[i].id); ret = supplies[i].ent; break; } return ret; }; var navalManager = gameState.ai.HQ.navalManager; var supply; // first look in our own base if accessible from our present position if (this.accessIndex === access) { if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby))) { this.ent.gather(supply); return true; } // --> for food, try to gather from fields if any, otherwise build one if any if (resource === "food") { if ((supply = this.gatherNearestField(gameState, this.baseID))) { this.ent.gather(supply); return true; } else if ((supply = this.buildAnyField(gameState, this.baseID))) { this.ent.repair(supply); return true; } } if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium))) { this.ent.gather(supply); return true; } } // So if we're here we have checked our whole base for a proper resource (or it was not accessible) // --> check other bases directly accessible for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } if (resource === "food") // --> for food, try to gather from fields if any, otherwise build one if any { for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = this.gatherNearestField(gameState, base.ID))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } if ((supply = this.buildAnyField(gameState, base.ID))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.repair(supply); return true; } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } // Okay may-be we haven't found any appropriate dropsite anywhere. // Try to help building one if any accessible foundation available var foundations = gameState.getOwnFoundations().toEntityArray(); var shouldBuild = foundations.some(function(foundation) { if (!foundation || foundation.getMetadata(PlayerID, "access") !== access) return false; if (foundation.resourceDropsiteTypes() && foundation.resourceDropsiteTypes().indexOf(resource) !== -1) { if (foundation.getMetadata(PlayerID, "base") !== self.baseID) self.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); self.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); self.ent.repair(foundation); return true; } return false; }); if (shouldBuild) return true; // Still nothing ... try bases which need a transport for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby))) { - if (base.ID !== this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()); - return true; + if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) + { + if (base.ID !== this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } } } if (resource === "food") // --> for food, try to gather from fields if any, otherwise build one if any { for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = this.gatherNearestField(gameState, base.ID))) { - if (base.ID !== this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()); - return true; + if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) + { + if (base.ID !== this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } } if ((supply = this.buildAnyField(gameState, base.ID))) { - if (base.ID !== this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()); - return true; + if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) + { + if (base.ID !== this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium))) { - if (base.ID !== this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()); - return true; + if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) + { + if (base.ID !== this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } } } // Okay so we haven't found any appropriate dropsite anywhere. // Try to help building one if any non-accessible foundation available shouldBuild = foundations.some(function(foundation) { if (!foundation || foundation.getMetadata(PlayerID, "access") === access) return false; if (foundation.resourceDropsiteTypes() && foundation.resourceDropsiteTypes().indexOf(resource) !== -1) { - if (foundation.getMetadata(PlayerID, "base") !== self.baseID) - self.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); - self.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); let foundationAccess = foundation.getMetadata(PlayerID, "access"); if (!foundationAccess) { foundationAccess = gameState.ai.accessibility.getAccessValue(foundation.position()); foundation.setMetadata(PlayerID, "access", foundationAccess); } - navalManager.requireTransport(gameState, self.ent, access, foundationAccess, foundation.position()); - return true; + if (navalManager.requireTransport(gameState, self.ent, access, foundationAccess, foundation.position())) + { + if (foundation.getMetadata(PlayerID, "base") !== self.baseID) + self.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); + self.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); + return true; + } } return false; }); if (shouldBuild) return true; // Still nothing, we look now for faraway resources, first in the accessible ones, then in the others if (this.accessIndex === access) { if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway))) { this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway))) { - if (base.ID !== this.baseID) - this.ent.setMetadata(PlayerID, "base", base.ID); - navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position()); - return true; + if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) + { + if (base.ID !== this.baseID) + this.ent.setMetadata(PlayerID, "base", base.ID); + return true; + } } } // If we are here, we have nothing left to gather ... certainly no more resources of this type gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime; if (gameState.ai.Config.debug > 2) warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; /** * if position is given, we only check if we could hunt from this position but do nothing * otherwise the position of the entity is taken, and if something is found, we directly start the hunt */ m.Worker.prototype.startHunting = function(gameState, position) { // First look for possible treasure if any if (!position && this.gatherTreasure(gameState)) return true; var resources = gameState.getHuntableSupplies(); if (resources.length === 0) return false; var nearestSupplyDist = Math.min(); var nearestSupply; var isCavalry = this.ent.hasClass("Cavalry"); var isRanged = this.ent.hasClass("Ranged"); var foodDropsites = gameState.getOwnDropsites("food"); var entPosition = position ? position : this.ent.position(); var access = gameState.ai.accessibility.getAccessValue(entPosition); var nearestDropsiteDist = function(supply) { let distMin = 1000000; let pos = supply.position(); foodDropsites.forEach(function (dropsite) { if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== access) return; let dist = API3.SquareVectorDistance(pos, dropsite.position()); if (dist < distMin) distMin = dist; }); return distMin; }; resources.forEach(function(supply) { if (!supply.position()) return; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return; if (m.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) var nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; var canFlee = (!supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1); // Only cavalry and range units should hunt fleeing animals if (canFlee && !isCavalry && !isRanged) return; var supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position()); if (supplyAccess !== access) return; // measure the distance to the resource var dist = API3.SquareVectorDistance(entPosition, supply.position()); // Only cavalry should hunt faraway if (!isCavalry && dist > 25000) return; // Avoid ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally return; var dropsiteDist = nearestDropsiteDist(supply); if (dropsiteDist > 35000) return; // Only cavalry should hunt far from dropsite (specially for non domestic animals which flee) if (!isCavalry && (dropsiteDist > 12000 || ((dropsiteDist > 7000 || territoryOwner == 0 ) && canFlee))) return; if (dist < nearestSupplyDist) { nearestSupplyDist = dist; nearestSupply = supply; } }); if (nearestSupply) { if (position) return true; gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } return false; }; m.Worker.prototype.startFishing = function(gameState) { if (!this.ent.position()) return false; // So here we're doing it basic. We check what we can hunt, we hunt it. No fancies. var resources = gameState.getFishableSupplies(); if (resources.length === 0) { gameState.ai.HQ.navalManager.resetFishingBoats(gameState); this.ent.destroy(); return false; } var nearestSupplyDist = Math.min(); var nearestSupply; var entPosition = this.ent.position(); var fisherSea = this.ent.getMetadata(PlayerID, "sea"); var docks = gameState.getOwnEntitiesByClass("Dock", true).filter(API3.Filters.isBuilt()); var nearestDropsiteDist = function(supply) { var distMin = 1000000; var pos = supply.position(); docks.forEach(function (dock) { if (!dock.position() || dock.getMetadata(PlayerID, "sea") !== fisherSea) return; let dist = API3.SquareVectorDistance(pos, dock.position()); if (dist < distMin) distMin = dist; }); return distMin; }; resources.forEach(function(supply) { if (!supply.position()) return; if (m.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) var nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; // check that it is accessible if (!supply.getMetadata(PlayerID, "sea")) supply.setMetadata(PlayerID, "sea", gameState.ai.accessibility.getAccessValue(supply.position(), true)); if (supply.getMetadata(PlayerID, "sea") !== fisherSea) return; // measure the distance to the resource var dist = API3.SquareVectorDistance(entPosition, supply.position()); if (dist > 40000) return; // Avoid ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally return; var dropsiteDist = nearestDropsiteDist(supply); if (dropsiteDist > 35000) return; if (dist < nearestSupplyDist) { nearestSupplyDist = dist; nearestSupply = supply; } }); if (nearestSupply) { gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } else { if (this.ent.getMetadata(PlayerID,"subrole") === "fisher") this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; } }; m.Worker.prototype.gatherNearestField = function(gameState, baseID) { var ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); var bestFarmEnt = false; var bestFarmDist = 10000000; for (var field of ownFields.values()) { if (m.IsSupplyFull(gameState, field)) continue; var dist = API3.SquareVectorDistance(field.position(), this.ent.position()); if (dist < bestFarmDist) { bestFarmEnt = field; bestFarmDist = dist; } } if (bestFarmEnt) { gameState.ai.HQ.AddTCGatherer(bestFarmEnt.id()); this.ent.setMetadata(PlayerID, "supply", bestFarmEnt.id()); } return bestFarmEnt; }; /** * WARNING with the present options of AI orders, the unit will not gather after building the farm. * This is done by calling the gatherNearestField function when construction is completed. */ m.Worker.prototype.buildAnyField = function(gameState, baseID) { var baseFoundations = gameState.getOwnFoundations().filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); var maxGatherers = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field")).maxGatherers(); var bestFarmEnt = false; var bestFarmDist = 10000000; var pos = this.ent.position(); for (let found of baseFoundations.values()) { if (!found.hasClass("Field")) continue; let current = found.getBuildersNb(); if (current === undefined || current >= maxGatherers) continue; let dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) continue; bestFarmEnt = found; bestFarmDist = dist; } return bestFarmEnt; }; /** * Look for some treasure to gather */ m.Worker.prototype.gatherTreasure = function(gameState) { var rates = this.ent.resourceGatherRates(); if (!rates || !rates.treasure || rates.treasure <= 0) return false; var treasureFound; var distmin = Math.min(); var access = gameState.ai.accessibility.getAccessValue(this.ent.position()); for (var treasure of gameState.ai.HQ.treasures.values()) { // let some time for the previous gatherer to reach the treasure befor trying again var lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; var treasureAccess = treasure.getMetadata(PlayerID, "access"); if (!treasureAccess) { treasureAccess = gameState.ai.accessibility.getAccessValue(treasure.position()); treasure.setMetadata(PlayerID, "access", treasureAccess); } if (treasureAccess !== access) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) continue; var dist = API3.SquareVectorDistance(this.ent.position(), treasure.position()); if (territoryOwner !== PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit continue; if (dist > distmin) continue; distmin = dist; treasureFound = treasure; } if (!treasureFound) return false; treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); this.ent.gather(treasureFound); gameState.ai.HQ.AddTCGatherer(treasureFound.id()); this.ent.setMetadata(PlayerID, "supply", treasureFound.id()); return true; }; /** * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions * For the time being, we move towards the nearest gatherer (providing him a dropsite) */ m.Worker.prototype.moveAway = function(gameState) { var gatherers = this.base.workersBySubrole(gameState, "gatherer"); var pos = this.ent.position(); var dist = Math.min(); var destination = pos; for (var gatherer of gatherers.values()) { if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined) continue; if (gatherer.isIdle()) continue; var distance = API3.SquareVectorDistance(pos, gatherer.position()); if (distance > dist) continue; dist = distance; destination = gatherer.position(); } this.ent.move(destination[0], destination[1]); }; // Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes // inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case // it will be cleared later). m.Worker.prototype.isInaccessibleSupply = function(gameState) { if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) return false; let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); if (!approachingTarget || approachingTarget !== this.ent.unitAIOrderData()[0].target) { this.ent.setMetadata(PlayerID, "approachingTarget", this.ent.unitAIOrderData()[0].target); this.ent.setMetadata(PlayerID, "approachingTime", undefined); this.ent.setMetadata(PlayerID, "approachingPos", undefined); this.ent.setMetadata(PlayerID, "carriedAmount", undefined); } let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; if (this.ent.getMetadata(PlayerID, "carriedAmount") === undefined || this.ent.getMetadata(PlayerID, "carriedAmount") !== carriedAmount) { this.ent.setMetadata(PlayerID, "carriedAmount", carriedAmount); this.ent.setMetadata(PlayerID, "approachingTime", undefined); this.ent.setMetadata(PlayerID, "approachingPos", undefined); return false; } let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 5) { let presentPos = this.ent.position(); let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) { this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); this.ent.setMetadata(PlayerID, "approachingPos", presentPos); } else if (gameState.ai.elapsedTime - approachingTime > 15) { let targetId = this.ent.unitAIOrderData()[0].target; let target = gameState.getEntityById(targetId); if (target) target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); return true; } } return false; }; return m; }(PETRA);