Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 21212) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 21213) @@ -1,376 +1,402 @@ var PETRA = function(m) { /** returns true if this unit should be considered as a siege unit */ m.isSiegeUnit = function(ent) { return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee") && ent.hasClass("Champion"); }; /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ m.getMaxStrength = function(ent, againstClass) { let strength = 0; let attackTypes = ent.attackTypes(); if (!attackTypes) return strength; for (let type of attackTypes) { if (type == "Slaughter") continue; let attackStrength = ent.attackStrengths(type); for (let str in attackStrength) { let val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); switch (str) { case "Crush": strength += val * 0.085 / 3; break; case "Hack": strength += val * 0.075 / 3; break; case "Pierce": strength += val * 0.065 / 3; break; default: API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength"); } } let attackRange = ent.attackRange(type); if (attackRange) strength += attackRange.max * 0.0125; let attackTimes = ent.attackTimes(type); for (let str in attackTimes) { let val = parseFloat(attackTimes[str]); switch (str) { case "repeat": strength += val / 100000; break; case "prepare": strength -= val / 100000; break; default: API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); } } } let armourStrength = ent.armourStrengths(); for (let str in armourStrength) { let val = parseFloat(armourStrength[str]); switch (str) { case "Crush": strength += val * 0.085 / 3; break; case "Hack": strength += val * 0.075 / 3; break; case "Pierce": strength += val * 0.065 / 3; break; default: API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength"); } } return strength * ent.maxHitpoints() / 100.0; }; /** Get access and cache it in metadata if not already done */ m.getLandAccess = function(gameState, ent) { let access = ent.getMetadata(PlayerID, "access"); if (!access) { access = gameState.ai.accessibility.getAccessValue(ent.position()); + // Docks are sometimes not as expected + if (access < 2 && ent.buildPlacementType() == "shore") + { + let entPos = ent.position(); + let cosa = Math.cos(ent.angle()); + let sina = Math.sin(ent.angle()); + for (let d = 3; d < 15; d += 3) + { + let pos = [ entPos[0] - d * sina, + entPos[1] - d * cosa]; + access = gameState.ai.accessibility.getAccessValue(pos); + if (access > 1) + break; + } + } ent.setMetadata(PlayerID, "access", access); } return access; }; -m.getSeaAccess = function(gameState, ent, warning = true) +m.getSeaAccess = function(gameState, ent) { let sea = ent.getMetadata(PlayerID, "sea"); if (!sea) { sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); - if (sea < 2) // pre-positioned docks are sometimes not well positionned + // Docks are sometimes not as expected + if (sea < 2 && ent.buildPlacementType() == "shore") { let entPos = ent.position(); - let radius = ent.footprintRadius(); - for (let i = 0; i < 16; ++i) + let cosa = Math.cos(ent.angle()); + let sina = Math.sin(ent.angle()); + for (let d = 3; d < 15; d += 3) { - let pos = [ entPos[0] + radius*Math.cos(i*Math.PI/8), - entPos[1] + radius*Math.sin(i*Math.PI/8) ]; + let pos = [ entPos[0] + d * sina, + entPos[1] + d * cosa]; sea = gameState.ai.accessibility.getAccessValue(pos, true); - if (sea >= 2) + if (sea > 1) break; } } - if (warning && sea < 2) - API3.warn("ERROR in Petra getSeaAccess because of position with sea index " + sea); ent.setMetadata(PlayerID, "sea", sea); } return sea; }; +m.setAccessIndices = function(gameState, ent) +{ + m.getLandAccess(gameState, ent); + m.getSeaAccess(gameState, ent); +}; + +m.setLandAccess = function(gameState, ent) +{ + m.getLandAccess(gameState, ent); +}; + /** Decide if we should try to capture (returns true) or destroy (return false) */ m.allowCapture = function(gameState, ent, target) { if (!target.isCapturable() || !ent.canCapture(target)) return false; if (target.isInvulnerable()) return true; // always try to recapture cp from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) return !target.decaying(); let antiCapture = target.defaultRegenRate(); if (target.isGarrisonHolder() && target.garrisoned()) antiCapture += target.garrisonRegenRate() * target.garrisoned().length; if (target.decaying()) antiCapture -= target.territoryDecayRate(); let capture; let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { capture = ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else { let capturable = capturableTargets.get(target.id()); if (!capturable.ents.has(ent.id())) { capturable.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturable.ents.add(ent.id()); } capture = capturable.strength; } capture *= 1 / ( 0.1 + 0.9*target.healthLevel()); let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) return capture > antiCapture + sumCapturePoints/50; return capture > antiCapture + sumCapturePoints/80; }; m.getAttackBonus = function(ent, target, type) { let attackBonus = 1; if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) return attackBonus; let bonuses = ent.get("Attack/" + type + "/Bonuses"); for (let key in bonuses) { let bonus = bonuses[key]; if (bonus.Civ && bonus.Civ !== target.civ()) continue; if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) continue; attackBonus *= bonus.Multiplier; } return attackBonus; }; /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ m.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) return false; let resource = ent.resourceCarrying()[0].type; let closestDropsite; let distmin = Math.min(); let access = gameState.ai.accessibility.getAccessValue(ent.position()); let dropsiteCollection = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); for (let dropsite of dropsiteCollection.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; let dropsiteAccess = dropsite.getMetadata(PlayerID, "access"); if (!dropsiteAccess) { dropsiteAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", dropsiteAccess); } if (dropsiteAccess !== access) continue; let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) continue; distmin = dist; closestDropsite = dropsite; } if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; /** is supply full taking into account gatherers affected during this turn */ m.IsSupplyFull = function(gameState, ent) { return ent.isFull() === true || ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); }; /** * get the best base (in terms of distance and accessIndex) for an entity */ m.getBestBase = function(gameState, ent, onlyConstructedBase = false) { let pos = ent.position(); if (!pos) { let holder = m.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); m.dumpEntity(ent); return gameState.ai.HQ.baseManagers[0]; } pos = holder.position(); } let distmin = Math.min(); let bestbase; let accessIndex = gameState.ai.accessibility.getAccessValue(pos); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || onlyConstructedBase && base.anchor.foundationProgress() !== undefined) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (base.accessIndex !== accessIndex) dist += 100000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase) bestbase = gameState.ai.HQ.baseManagers[0]; return bestbase; }; m.getHolder = function(gameState, ent) { for (let holder of gameState.getEntities().values()) { if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) return holder; } return undefined; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ m.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) return true; } return false; }; /** * Check if the straight line between the two positions crosses an enemy territory */ m.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) { let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; let stepx = (pos2[0] - pos1[0]) / n; let stepy = (pos2[1] - pos1[1]) / n; for (let i = 1; i < n; ++i) { let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; let owner = gameState.ai.HQ.territoryMap.getOwner(pos); if (owner && gameState.isPlayerEnemy(owner)) return true; } return false; }; m.gatherTreasure = function(gameState, ent, water = false) { if (!gameState.ai.HQ.treasures.hasEntities()) return false; if (!ent || !ent.position()) return false; let rates = ent.resourceGatherRates(); if (!rates || !rates.treasure || rates.treasure <= 0) return false; let treasureFound; let distmin = Math.min(); let access = gameState.ai.accessibility.getAccessValue(ent.position(), water); for (let treasure of gameState.ai.HQ.treasures.values()) { if (m.IsSupplyFull(gameState, treasure)) continue; // let some time for the previous gatherer to reach the treasure before trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; - if (!water && access !== m.getLandAccess(gameState, treasure)) + if (!water && access != m.getLandAccess(gameState, treasure)) continue; - if (water && access !== m.getSeaAccess(gameState, treasure, false)) + if (water && access != m.getSeaAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); - if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) + if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); - if (dist > 120000 || territoryOwner !== PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit + if (dist > 120000 || 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); ent.gather(treasureFound); gameState.ai.HQ.AddTCGatherer(treasureFound.id()); ent.setMetadata(PlayerID, "supply", treasureFound.id()); return true; }; m.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + " foundationProgress " + ent.foundationProgress()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; 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 21212) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 21213) @@ -1,793 +1,791 @@ 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) * -Transport of units over water (a few units). * -Scouting, ultimately. * Also deals with handling docks, making sure we have access and stuffs like that. */ 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) { // docks this.docks = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Dock", "Shipyard"])); 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(); let availableFishes = {}; for (let fish of gameState.getFishableSupplies().values()) { let sea = this.getFishSea(gameState, fish); if (sea && availableFishes[sea]) availableFishes[sea] += fish.resourceSupplyAmount(); else if (sea) 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); } } if (deserializing) return; // determination of the possible landing zones let width = gameState.getPassabilityMap().width; let length = width * gameState.getPassabilityMap().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.setAccessIndices(gameState, dock); + m.setAccessIndices(gameState, dock); }; m.NavalManager.prototype.updateFishingBoats = function(sea, num) { if (this.wantedFishShips[sea]) this.wantedFishShips[sea] = num; }; m.NavalManager.prototype.resetFishingBoats = function(gameState, sea) { if (sea !== undefined) this.wantedFishShips[sea] = 0; else this.wantedFishShips.fill(0); }; -m.NavalManager.prototype.setAccessIndices = function(gameState, ent) -{ - m.getLandAccess(gameState, ent); - m.getSeaAccess(gameState, ent); -}; - m.NavalManager.prototype.setShipIndex = function(gameState, ship) { let sea = gameState.ai.accessibility.getAccessValue(ship.position(), true); ship.setMetadata(PlayerID, "sea", sea); }; /** Get the sea, cache it if not yet done and check if in opensea */ m.NavalManager.prototype.getFishSea = function(gameState, fish) { let sea = fish.getMetadata(PlayerID, "sea"); if (sea) return sea; const ntry = 4; 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] ]; let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); let width = gameState.ai.accessibility.width; let k = pos[0] + pos[1]*width; sea = gameState.ai.accessibility.navalPassMap[k]; fish.setMetadata(PlayerID, "sea", sea); let radius = 120 / gameState.ai.accessibility.cellSize / ntry; if (around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); if (i < 0 || i >= width || j < 0 || j >= width) continue; if (gameState.ai.accessibility.landPassMap[i + j*width] === 1) { let navalPass = gameState.ai.accessibility.navalPassMap[i + j*width]; if (navalPass === sea) return true; else if (navalPass === 1) // we could be outside the map continue; } return false; } return true; })) fish.setMetadata(PlayerID, "opensea", true); return sea; }; /** check if we can safely fish at the fish position */ m.NavalManager.prototype.canFishSafely = function(gameState, fish) { if (fish.getMetadata(PlayerID, "opensea")) return true; const ntry = 4; 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] ]; let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let radius = 140 / territoryMap.cellSize / ntry; let pos = territoryMap.gamePosToMapPos(fish.position()); return around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*t); let j = pos[1] + Math.round(a[1]*radius*t); if (i < 0 || i >= width || j < 0 || j >= width) break; let owner = territoryMap.getOwnerIndex(i + j*width); if (owner !== 0 && gameState.isPlayerEnemy(owner)) return false; } return true; }); }; /** get the list of seas (or lands) around this region not connected by a dock */ m.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { let seas = gameState.ai.accessibility.regionLinks[region].slice(); this.docks.forEach(function (dock) { if (!dock.hasClass("Dock") || 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.Create) { if (!evt.entity) continue; let ent = gameState.getEntityById(evt.entity); if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - this.setAccessIndices(gameState, ent); + m.setAccessIndices(gameState, ent); } for (let evt of events.TrainingFinished) { if (!evt.entities) continue; for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) continue; this.setShipIndex(gameState, ent); } } 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; let plan = this.getPlan(evt.metadata[PlayerID].transporter); if (!plan) continue; let 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.hasEntities(); } else if (plan.state === "sailing") { let endIndex = plan.endIndex; for (let ent of plan.units.values()) { if (!ent.position()) // unit from another ship of this plan ... do nothing continue; 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) this.requireTransport(gameState, ent, access, endIndex, endPos); } } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (ent && (ent.hasClass("Dock") || ent.hasClass("Shipyard"))) - this.setAccessIndices(gameState, ent); + m.setAccessIndices(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.canGarrison()) return false; 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 <<<<"); let 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); let 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.hasQueuedUnits()) 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.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) return; // check if we have enough transport ships per region. for (let 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]) { let 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) { for (let ship of this.ships.values()) { if (ship.hasClass("FishingBoat")) // small ships should not be a problem continue; let shipPosition = ship.position(); if (!shipPosition) continue; let sea = ship.getMetadata(PlayerID, "sea"); if (ship.getMetadata(PlayerID, "transporter") === undefined) { if (ship.isIdle()) { // Check if there are some treasure around let treasurePosChecked = ship.getMetadata(PlayerID, "treasurePosChecked"); if ((!treasurePosChecked || treasurePosChecked[0] != shipPosition[0] || treasurePosChecked[1] != shipPosition[1]) && m.gatherTreasure(gameState, ship, true)) continue; ship.setMetadata(PlayerID, "treasurePosChecked", shipPosition); // Do not stay idle near a dock to not disturb other ships for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) { if (dock.getMetadata(PlayerID, "sea") != sea) continue; if (API3.SquareVectorDistance(shipPosition, dock.position()) > 2500) continue; ship.moveApart(dock.position(), 50); } } continue; } // if transporter ship not idle, move away other ships which could block it for (let blockingShip of this.seaShips[sea].values()) { if (blockingShip == ship || !blockingShip.isIdle()) continue; if (API3.SquareVectorDistance(shipPosition, blockingShip.position()) > 900) continue; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(shipPosition, 12); else blockingShip.moveApart(shipPosition, 6); } } for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) { if (ship.getMetadata(PlayerID, "route") === undefined) continue; let shipPosition = ship.position(); if (!shipPosition) continue; let sea = ship.getMetadata(PlayerID, "sea"); for (let blockingShip of this.seaShips[sea].values()) { if (blockingShip == ship || !blockingShip.isIdle()) continue; if (API3.SquareVectorDistance(shipPosition, blockingShip.position()) > 900) continue; if (blockingShip.getMetadata(PlayerID, "transporter") === undefined) blockingShip.moveApart(shipPosition, 12); else blockingShip.moveApart(shipPosition, 6); } } }; m.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) return; if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("NavalMarket") === 0 && !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("NavalMarket"), API3.Filters.isFoundation())).hasEntities() && 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; 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.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || queues.militaryBuilding.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) return; + // Use in priority resources to build a market + if (!gameState.getOwnEntitiesByClass("BarterMarket", true).hasEntities() && + gameState.ai.HQ.canBuild(gameState, "structures/{civ}_market")) + return; let template; if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_super_dock")) template = "structures/{civ}_super_dock"; else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_shipyard")) template = "structures/{civ}_shipyard"; else return; let wantedLand = {}; for (let base of gameState.ai.HQ.baseManagers) if (base.anchor) wantedLand[base.accessIndex] = true; let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); queues.militaryBuilding.addPlan(new m.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); }; /** 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) { let civ = gameState.getPlayerCiv(); let trainableShips = []; gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { let trainables = ent.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) trainableShips.push(trainable); } }); let best = 0; let bestShip; let limits = gameState.getEntityLimits(); let 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/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 21212) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 21213) @@ -1,583 +1,583 @@ var PETRA = function(m) { /** * determines the strategy to adopt when starting a new game, depending on the initial conditions */ m.HQ.prototype.gameAnalysis = function(gameState) { // Analysis of the terrain and the different access regions if (!this.regionAnalysis(gameState)) return; this.attackManager.init(gameState); this.buildManager.init(gameState); this.navalManager.init(gameState); this.tradeManager.init(gameState); this.diplomacyManager.init(gameState); // Make a list of buildable structures from the config file this.structureAnalysis(gameState); // Let's get our initial situation here. let nobase = new m.BaseManager(gameState, this.Config); nobase.init(gameState); nobase.accessIndex = 0; this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let newbase = new m.BaseManager(gameState, this.Config); newbase.init(gameState); newbase.setAnchor(gameState, cc); this.baseManagers.push(newbase); } this.updateTerritories(gameState); // Assign entities and resources in the different bases this.assignStartingEntities(gameState); // Sandbox difficulty should not try to expand this.canExpand = this.Config.difficulty != 0; // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks this.canBuildUnits = true; if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) { let template = gameState.applyCiv("structures/{civ}_civil_centre"); if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) { if (this.Config.debug > 1) API3.warn(" this AI is unable to produce any units"); this.canBuildUnits = false; this.dispatchUnits(gameState); } else this.buildFirstBase(gameState); } // configure our first base strategy if (this.baseManagers.length > 1) this.configFirstBase(gameState); }; /** * Assign the starting entities to the different bases */ m.HQ.prototype.assignStartingEntities = function(gameState) { for (let ent of gameState.getOwnEntities().values()) { // do not affect merchant ship immediately to trade as they may-be useful for transport if (ent.hasClass("Trader") && !ent.hasClass("Ship")) this.tradeManager.assignTrader(ent); let pos = ent.position(); if (!pos) { // TODO should support recursive garrisoning. Make a warning for now if (ent.isGarrisonHolder() && ent.garrisoned().length) API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); continue; } // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; if (land > 1 && !this.landRegions[land]) this.landRegions[land] = true; let sea = gameState.ai.accessibility.navalPassMap[index]; if (sea > 1 && !this.navalRegions[sea]) 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); - ent.setMetadata(PlayerID, "access", gameState.ai.accessibility.getAccessValue(pos)); + m.setLandAccess(gameState, ent); let bestbase; let territorypos = this.territoryMap.gamePosToMapPos(pos); let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; for (let i = 1; i < this.baseManagers.length; ++i) { let base = this.baseManagers[i]; if (base.territoryIndices.indexOf(territoryIndex) === -1) continue; base.assignEntity(gameState, ent); bestbase = base; break; } if (!bestbase) // entity outside our territory { bestbase = m.getBestBase(gameState, ent); bestbase.assignEntity(gameState, ent); } // now assign entities garrisoned inside this entity if (ent.isGarrisonHolder() && ent.garrisoned().length) for (let id of ent.garrisoned()) bestbase.assignEntity(gameState, gameState.getEntityById(id)); // and find something useful to do if we already have a base if (pos && bestbase.ID !== this.baseManagers[0].ID) { bestbase.assignRolelessUnits(gameState, [ent]); if (ent.getMetadata(PlayerID, "role") === "worker") { bestbase.reassignIdleWorkers(gameState, [ent]); bestbase.workerObject.update(gameState, ent); } } } }; /** * determine the main land Index (or water index if none) * as well as the list of allowed (land andf water) regions */ m.HQ.prototype.regionAnalysis = function(gameState) { let accessibility = gameState.ai.accessibility; let landIndex; let seaIndex; let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let land = accessibility.getAccessValue(cc.position()); if (land > 1) { landIndex = land; break; } } if (!landIndex) { let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnEntities().values()) { if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) continue; let land = accessibility.getAccessValue(ent.position()); if (land > 1) { landIndex = land; break; } let sea = accessibility.getAccessValue(ent.position(), true); if (!seaIndex && sea > 1) seaIndex = sea; } } if (!landIndex && !seaIndex) { API3.warn("Petra error: it does not know how to interpret this map"); return false; } let passabilityMap = gameState.getPassabilityMap(); let totalSize = passabilityMap.width * passabilityMap.width; let minLandSize = Math.floor(0.1*totalSize); let minWaterSize = Math.floor(0.2*totalSize); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; for (let i = 0; i < accessibility.regionSize.length; ++i) { if (landIndex && i == landIndex) this.landRegions[i] = true; else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) { if (landIndex) { let sea = this.getSeaBetweenIndices(gameState, landIndex, i); if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[sea] = true; } } else { let traject = accessibility.getTrajectToIndex(seaIndex, i); if (traject && traject.length === 2) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[seaIndex] = true; } } } else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) { this.navalMap = true; this.navalRegions[i] = true; } else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) this.navalRegions[i] = true; } if (this.Config.debug < 3) return true; for (let region in this.landRegions) API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); API3.warn(" navalMap " + this.navalMap); API3.warn(" landRegions " + uneval(this.landRegions)); API3.warn(" navalRegions " + uneval(this.navalRegions)); return true; }; /** * load units and buildings from the config files * TODO: change that to something dynamic */ m.HQ.prototype.structureAnalysis = function(gameState) { let civref = gameState.playerData.civ; let civ = civref in this.Config.buildings.advanced ? civref : 'default'; this.bAdvanced = []; for (let advanced of this.Config.buildings.advanced[civ]) if (gameState.isTemplateAvailable(gameState.applyCiv(advanced))) this.bAdvanced.push(gameState.applyCiv(advanced)); }; /** * build our first base * if not enough resource, try first to do a dock */ m.HQ.prototype.buildFirstBase = function(gameState) { if (gameState.ai.queues.civilCentre.hasQueuedUnits()) return; let templateName = gameState.applyCiv("structures/{civ}_civil_centre"); if (gameState.isTemplateDisabled(templateName)) return; let template = gameState.getTemplate(templateName); if (!template) return; let total = gameState.getResources(); let goal = "civil_centre"; if (!total.canAfford(new API3.Resources(template.cost()))) { let totalExpected = gameState.getResources(); // Check for treasures around available in some maps at startup for (let ent of gameState.getOwnUnits().values()) { if (!ent.position()) continue; // If we can get a treasure around, just do it if (ent.isIdle()) m.gatherTreasure(gameState, ent); // Then count the resources from the treasures being collected let supplyId = ent.getMetadata(PlayerID, "supply"); if (!supplyId) continue; let supply = gameState.getEntityById(supplyId); if (!supply || supply.resourceSupplyType().generic != "treasure") continue; let type = supply.resourceSupplyType().specific; if (!(type in totalExpected)) continue; totalExpected[type] += supply.resourceSupplyMax(); // If we can collect enough resources from these treasures, wait for them if (totalExpected.canAfford(new API3.Resources(template.cost()))) return; } // not enough resource to build a cc, try with a dock to accumulate resources if none yet if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) { if (gameState.ai.queues.dock.hasQueuedUnits()) return; templateName = gameState.applyCiv("structures/{civ}_dock"); if (gameState.isTemplateDisabled(templateName)) return; template = gameState.getTemplate(templateName); if (!template || !total.canAfford(new API3.Resources(template.cost()))) return; goal = "dock"; } } if (!this.canBuild(gameState, templateName)) return; // We first choose as startingPoint the point where we have the more units let startingPoint = []; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant"))) continue; if (ent.hasClass("Cavalry")) continue; let pos = ent.position(); if (!pos) { let holder = m.getHolder(gameState, ent); if (!holder || !holder.position()) continue; pos = holder.position(); } let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; let sea = gameState.ai.accessibility.navalPassMap[index]; let found = false; for (let point of startingPoint) { if (land !== point.land || sea !== point.sea) continue; if (API3.SquareVectorDistance(point.pos, pos) > 2500) continue; point.weight += 1; found = true; break; } if (!found) startingPoint.push({"pos": pos, "land": land, "sea": sea, "weight": 1}); } if (!startingPoint.length) return; let imax = 0; for (let i = 1; i < startingPoint.length; ++i) if (startingPoint[i].weight > startingPoint[imax].weight) imax = i; if (goal === "dock") { let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; gameState.ai.queues.dock.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos })); } else gameState.ai.queues.civilCentre.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); }; /** * set strategy if game without construction: * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack * - otherwise all units will attack */ m.HQ.prototype.dispatchUnits = function(gameState) { let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); if (allycc.length) { if (this.Config.debug > 1) API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); let units = gameState.getOwnUnits(); let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); let num1 = Math.floor(num / 2); let num2 = num1; // first pass to affect ranged infantry units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(function (ent) { if (!num || !num1) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; --num; --num1; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); // second pass to affect melee infantry units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(function (ent) { if (!num || !num2) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; --num; --num2; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); // and now complete the affectation, including all support units units.forEach(function (ent) { if (!num && !ent.hasClass("Support")) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let cc of allycc) { if (!cc.position()) continue; if (gameState.ai.accessibility.getAccessValue(cc.position()) != access) continue; if (!ent.hasClass("Support")) --num; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range); break; } }); } }; /** * configure our first base expansion * - if on a small island, favor fishing * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) */ m.HQ.prototype.configFirstBase = function(gameState) { if (this.baseManagers.length < 2) return; this.firstBaseConfig = true; let startingSize = 0; let startingLand = []; for (let region in this.landRegions) { for (let base of this.baseManagers) { if (!base.anchor || base.accessIndex != +region) continue; startingSize += gameState.ai.accessibility.regionSize[region]; startingLand.push(base.accessIndex); break; } } let cell = gameState.getPassabilityMap().cellSize; startingSize = startingSize * cell * cell; if (this.Config.debug > 1) API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); if (startingSize < 25000) { this.saveSpace = true; this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); let num = Math.max(this.Config.Economy.targetNumFishers, 2); for (let land of startingLand) { for (let sea of gameState.ai.accessibility.regionLinks[land]) if (gameState.ai.HQ.navalRegions[sea]) this.navalManager.updateFishingBoats(sea, num); } this.maxFields = 1; this.needCorral = true; } else if (startingSize < 60000) this.maxFields = 2; else this.maxFields = false; // - count the available wood resource, and react accordingly let startingFood = gameState.getResources().food; let check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.food[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingFood += supply.ent.resourceSupplyAmount(); } } } if (startingFood < 800) { if (startingSize < 25000) { this.needFish = true; this.Config.Economy.popForDock = 1; } else this.needFarm = true; } // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) let startingWood = gameState.getResources().wood; check = {}; for (let proxim of ["nearby", "medium", "faraway"]) { for (let base of this.baseManagers) { for (let supply of base.dropsiteSupplies.wood[proxim]) { if (check[supply.id]) // avoid double counting as same resource can appear several time continue; check[supply.id] = true; startingWood += supply.ent.resourceSupplyAmount(); } } } if (this.Config.debug > 1) API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); if (startingWood < 6000) { this.saveResources = true; this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand if (startingWood < 2000 && this.needFarm) { this.needCorral = true; this.needFarm = false; } } if (startingWood > 8500 && this.canBuildUnits) { let allowed = Math.ceil((startingWood - 8500) / 3000); // Not useful to prepare rushing if too long ceasefire if (gameState.isCeasefireActive()) { if (gameState.ceasefireTimeRemaining > 900) allowed = 0; else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) allowed = 1; } this.attackManager.setRushes(allowed); } // immediatly build a wood dropsite if possible. let template = gameState.applyCiv("structures/{civ}_storehouse"); if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) { let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (newDP.quality > 40) { // if we start with enough workers, put our available resources in this first dropsite // same thing if our pop exceed the allowed one, as we will need several houses let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; if (numWorkers > 12 && newDP.quality > 60 || gameState.getPopulation() > gameState.getPopulationLimit() + 20) { let cost = new API3.Resources(gameState.getTemplate(template).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } gameState.ai.queues.dropsites.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { template = gameState.applyCiv("structures/{civ}_corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) gameState.ai.queues.corral.addPlan(new m.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); } }; return m; }(PETRA);