Line data Source code
1 : /**
2 : * Determines the strategy to adopt when starting a new game,
3 : * depending on the initial conditions
4 : */
5 :
6 0 : PETRA.HQ.prototype.gameAnalysis = function(gameState)
7 : {
8 : // Analysis of the terrain and the different access regions
9 0 : if (!this.regionAnalysis(gameState))
10 0 : return;
11 :
12 0 : this.attackManager.init(gameState);
13 0 : this.buildManager.init(gameState);
14 0 : this.navalManager.init(gameState);
15 0 : this.tradeManager.init(gameState);
16 0 : this.diplomacyManager.init(gameState);
17 :
18 : // Make a list of buildable structures from the config file
19 0 : this.structureAnalysis(gameState);
20 :
21 : // Let's get our initial situation here.
22 0 : this.basesManager.init(gameState);
23 0 : this.updateTerritories(gameState);
24 :
25 : // Assign entities and resources in the different bases
26 0 : this.assignStartingEntities(gameState);
27 :
28 :
29 : // Sandbox difficulty should not try to expand
30 0 : this.canExpand = this.Config.difficulty != PETRA.DIFFICULTY_SANDBOX;
31 : // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks
32 0 : this.canBuildUnits = true;
33 0 : if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities())
34 : {
35 0 : let template = gameState.applyCiv("structures/{civ}/civil_centre");
36 0 : if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState))
37 : {
38 0 : if (this.Config.debug > 1)
39 0 : API3.warn(" this AI is unable to produce any units");
40 0 : this.canBuildUnits = false;
41 0 : this.dispatchUnits(gameState);
42 : }
43 : else
44 0 : this.buildFirstBase(gameState);
45 : }
46 :
47 : // configure our first base strategy
48 0 : if (this.hasPotentialBase())
49 0 : this.configFirstBase(gameState);
50 : };
51 :
52 : /**
53 : * Assign the starting entities to the different bases
54 : */
55 0 : PETRA.HQ.prototype.assignStartingEntities = function(gameState)
56 : {
57 0 : for (let ent of gameState.getOwnEntities().values())
58 : {
59 : // do not affect merchant ship immediately to trade as they may-be useful for transport
60 0 : if (ent.hasClasses(["Trader+!Ship"]))
61 0 : this.tradeManager.assignTrader(ent);
62 :
63 0 : let pos = ent.position();
64 0 : if (!pos)
65 : {
66 : // TODO should support recursive garrisoning. Make a warning for now
67 0 : if (ent.isGarrisonHolder() && ent.garrisoned().length)
68 0 : API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented");
69 0 : continue;
70 : }
71 :
72 : // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units)
73 0 : let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
74 0 : let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width;
75 0 : let land = gameState.ai.accessibility.landPassMap[index];
76 0 : if (land > 1 && !this.landRegions[land])
77 0 : this.landRegions[land] = true;
78 0 : let sea = gameState.ai.accessibility.navalPassMap[index];
79 0 : if (sea > 1 && !this.navalRegions[sea])
80 0 : this.navalRegions[sea] = true;
81 :
82 : // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport
83 : // when a construction will start (see createTransportIfNeeded)
84 0 : if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship"))
85 0 : for (let id of ent.garrisoned())
86 0 : ent.unload(id);
87 :
88 0 : let territorypos = this.territoryMap.gamePosToMapPos(pos);
89 0 : let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width;
90 :
91 0 : this.basesManager.assignEntity(gameState, ent, territoryIndex);
92 : }
93 : };
94 :
95 : /**
96 : * determine the main land Index (or water index if none)
97 : * as well as the list of allowed (land andf water) regions
98 : */
99 0 : PETRA.HQ.prototype.regionAnalysis = function(gameState)
100 : {
101 0 : let accessibility = gameState.ai.accessibility;
102 : let landIndex;
103 : let seaIndex;
104 0 : let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre"));
105 0 : for (let cc of ccEnts.values())
106 : {
107 0 : let land = accessibility.getAccessValue(cc.position());
108 0 : if (land > 1)
109 : {
110 0 : landIndex = land;
111 0 : break;
112 : }
113 : }
114 0 : if (!landIndex)
115 : {
116 0 : let civ = gameState.getPlayerCiv();
117 0 : for (let ent of gameState.getOwnEntities().values())
118 : {
119 0 : if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ))
120 0 : continue;
121 0 : let land = accessibility.getAccessValue(ent.position());
122 0 : if (land > 1)
123 : {
124 0 : landIndex = land;
125 0 : break;
126 : }
127 0 : let sea = accessibility.getAccessValue(ent.position(), true);
128 0 : if (!seaIndex && sea > 1)
129 0 : seaIndex = sea;
130 : }
131 : }
132 0 : if (!landIndex && !seaIndex)
133 : {
134 0 : API3.warn("Petra error: it does not know how to interpret this map");
135 0 : return false;
136 : }
137 :
138 0 : let passabilityMap = gameState.getPassabilityMap();
139 0 : let totalSize = passabilityMap.width * passabilityMap.width;
140 0 : let minLandSize = Math.floor(0.1*totalSize);
141 0 : let minWaterSize = Math.floor(0.2*totalSize);
142 0 : let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
143 0 : for (let i = 0; i < accessibility.regionSize.length; ++i)
144 : {
145 0 : if (landIndex && i == landIndex)
146 0 : this.landRegions[i] = true;
147 0 : else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320)
148 : {
149 0 : if (landIndex)
150 : {
151 0 : let sea = this.getSeaBetweenIndices(gameState, landIndex, i);
152 0 : if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize))
153 : {
154 0 : this.navalMap = true;
155 0 : this.landRegions[i] = true;
156 0 : this.navalRegions[sea] = true;
157 : }
158 : }
159 : else
160 : {
161 0 : let traject = accessibility.getTrajectToIndex(seaIndex, i);
162 0 : if (traject && traject.length === 2)
163 : {
164 0 : this.navalMap = true;
165 0 : this.landRegions[i] = true;
166 0 : this.navalRegions[seaIndex] = true;
167 : }
168 : }
169 : }
170 0 : else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize)
171 : {
172 0 : this.navalMap = true;
173 0 : this.navalRegions[i] = true;
174 : }
175 0 : else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600)
176 0 : this.navalRegions[i] = true;
177 : }
178 :
179 0 : if (this.Config.debug < 3)
180 0 : return true;
181 0 : for (let region in this.landRegions)
182 0 : API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]);
183 0 : API3.warn(" navalMap " + this.navalMap);
184 0 : API3.warn(" landRegions " + uneval(this.landRegions));
185 0 : API3.warn(" navalRegions " + uneval(this.navalRegions));
186 0 : return true;
187 : };
188 :
189 : /**
190 : * load units and buildings from the config files
191 : * TODO: change that to something dynamic
192 : */
193 0 : PETRA.HQ.prototype.structureAnalysis = function(gameState)
194 : {
195 0 : let civref = gameState.playerData.civ;
196 0 : let civ = civref in this.Config.buildings ? civref : 'default';
197 0 : this.bAdvanced = [];
198 0 : for (let building of this.Config.buildings[civ])
199 0 : if (gameState.isTemplateAvailable(gameState.applyCiv(building)))
200 0 : this.bAdvanced.push(gameState.applyCiv(building));
201 : };
202 :
203 : /**
204 : * build our first base
205 : * if not enough resource, try first to do a dock
206 : */
207 0 : PETRA.HQ.prototype.buildFirstBase = function(gameState)
208 : {
209 0 : if (gameState.ai.queues.civilCentre.hasQueuedUnits())
210 0 : return;
211 0 : let templateName = gameState.applyCiv("structures/{civ}/civil_centre");
212 0 : if (gameState.isTemplateDisabled(templateName))
213 0 : return;
214 0 : let template = gameState.getTemplate(templateName);
215 0 : if (!template)
216 0 : return;
217 0 : let total = gameState.getResources();
218 0 : let goal = "civil_centre";
219 0 : if (!total.canAfford(new API3.Resources(template.cost())))
220 : {
221 0 : let totalExpected = gameState.getResources();
222 : // Check for treasures around available in some maps at startup
223 0 : for (let ent of gameState.getOwnUnits().values())
224 : {
225 0 : if (!ent.position())
226 0 : continue;
227 : // If we can get a treasure around, just do it
228 0 : if (ent.isIdle())
229 0 : PETRA.gatherTreasure(gameState, ent);
230 : // Then count the resources from the treasures being collected
231 0 : let treasureId = ent.getMetadata(PlayerID, "treasure");
232 0 : if (!treasureId)
233 0 : continue;
234 0 : let treasure = gameState.getEntityById(treasureId);
235 0 : if (!treasure)
236 0 : continue;
237 0 : let types = treasure.treasureResources();
238 0 : for (let type in types)
239 0 : if (type in totalExpected)
240 0 : totalExpected[type] += types[type];
241 : // If we can collect enough resources from these treasures, wait for them.
242 0 : if (totalExpected.canAfford(new API3.Resources(template.cost())))
243 0 : return;
244 : }
245 :
246 : // not enough resource to build a cc, try with a dock to accumulate resources if none yet
247 0 : if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities())
248 : {
249 0 : if (gameState.ai.queues.dock.hasQueuedUnits())
250 0 : return;
251 0 : templateName = gameState.applyCiv("structures/{civ}/dock");
252 0 : if (gameState.isTemplateDisabled(templateName))
253 0 : return;
254 0 : template = gameState.getTemplate(templateName);
255 0 : if (!template || !total.canAfford(new API3.Resources(template.cost())))
256 0 : return;
257 0 : goal = "dock";
258 : }
259 : }
260 0 : if (!this.canBuild(gameState, templateName))
261 0 : return;
262 :
263 : // We first choose as startingPoint the point where we have the more units
264 0 : let startingPoint = [];
265 0 : for (let ent of gameState.getOwnUnits().values())
266 : {
267 0 : if (!ent.hasClass("Worker"))
268 0 : continue;
269 0 : if (PETRA.isFastMoving(ent))
270 0 : continue;
271 0 : let pos = ent.position();
272 0 : if (!pos)
273 : {
274 0 : let holder = PETRA.getHolder(gameState, ent);
275 0 : if (!holder || !holder.position())
276 0 : continue;
277 0 : pos = holder.position();
278 : }
279 0 : let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
280 0 : let index = gamepos[0] + gamepos[1] * gameState.ai.accessibility.width;
281 0 : let land = gameState.ai.accessibility.landPassMap[index];
282 0 : let sea = gameState.ai.accessibility.navalPassMap[index];
283 0 : let found = false;
284 0 : for (let point of startingPoint)
285 : {
286 0 : if (land !== point.land || sea !== point.sea)
287 0 : continue;
288 0 : if (API3.SquareVectorDistance(point.pos, pos) > 2500)
289 0 : continue;
290 0 : point.weight += 1;
291 0 : found = true;
292 0 : break;
293 : }
294 0 : if (!found)
295 0 : startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 });
296 : }
297 0 : if (!startingPoint.length)
298 0 : return;
299 :
300 0 : let imax = 0;
301 0 : for (let i = 1; i < startingPoint.length; ++i)
302 0 : if (startingPoint[i].weight > startingPoint[imax].weight)
303 0 : imax = i;
304 :
305 0 : if (goal == "dock")
306 : {
307 0 : let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined;
308 0 : gameState.ai.queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "sea": sea, "proximity": startingPoint[imax].pos }));
309 : }
310 : else
311 0 : gameState.ai.queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos }));
312 : };
313 :
314 : /**
315 : * set strategy if game without construction:
316 : * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack
317 : * - otherwise all units will attack
318 : */
319 0 : PETRA.HQ.prototype.dispatchUnits = function(gameState)
320 : {
321 0 : let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
322 0 : if (allycc.length)
323 : {
324 0 : if (this.Config.debug > 1)
325 0 : API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units ");
326 0 : let units = gameState.getOwnUnits();
327 0 : let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5);
328 0 : let num1 = Math.floor(num / 2);
329 0 : let num2 = num1;
330 : // first pass to affect ranged infantry
331 0 : units.filter(API3.Filters.byClasses(["Infantry+Ranged"])).forEach(ent => {
332 0 : if (!num || !num1)
333 0 : return;
334 0 : if (ent.getMetadata(PlayerID, "allied"))
335 0 : return;
336 0 : let access = PETRA.getLandAccess(gameState, ent);
337 0 : for (let cc of allycc)
338 : {
339 0 : if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
340 0 : continue;
341 0 : --num;
342 0 : --num1;
343 0 : ent.setMetadata(PlayerID, "allied", true);
344 0 : let range = 1.5 * cc.footprintRadius();
345 0 : ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
346 0 : break;
347 : }
348 : });
349 : // second pass to affect melee infantry
350 0 : units.filter(API3.Filters.byClasses(["Infantry+Melee"])).forEach(ent => {
351 0 : if (!num || !num2)
352 0 : return;
353 0 : if (ent.getMetadata(PlayerID, "allied"))
354 0 : return;
355 0 : let access = PETRA.getLandAccess(gameState, ent);
356 0 : for (let cc of allycc)
357 : {
358 0 : if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
359 0 : continue;
360 0 : --num;
361 0 : --num2;
362 0 : ent.setMetadata(PlayerID, "allied", true);
363 0 : let range = 1.5 * cc.footprintRadius();
364 0 : ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
365 0 : break;
366 : }
367 : });
368 : // and now complete the affectation, including all support units
369 0 : units.forEach(ent => {
370 0 : if (!num && !ent.hasClass("Support"))
371 0 : return;
372 0 : if (ent.getMetadata(PlayerID, "allied"))
373 0 : return;
374 0 : let access = PETRA.getLandAccess(gameState, ent);
375 0 : for (let cc of allycc)
376 : {
377 0 : if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
378 0 : continue;
379 0 : if (!ent.hasClass("Support"))
380 0 : --num;
381 0 : ent.setMetadata(PlayerID, "allied", true);
382 0 : let range = 1.5 * cc.footprintRadius();
383 0 : ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5);
384 0 : break;
385 : }
386 : });
387 : }
388 : };
389 :
390 : /**
391 : * configure our first base expansion
392 : * - if on a small island, favor fishing
393 : * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
394 : */
395 0 : PETRA.HQ.prototype.configFirstBase = function(gameState)
396 : {
397 0 : if (!this.hasPotentialBase())
398 0 : return;
399 :
400 0 : this.firstBaseConfig = true;
401 :
402 0 : let startingSize = 0;
403 0 : let startingLand = [];
404 0 : for (let region in this.landRegions)
405 : {
406 0 : for (const base of this.baseManagers())
407 : {
408 0 : if (!base.anchor || base.accessIndex != +region)
409 0 : continue;
410 0 : startingSize += gameState.ai.accessibility.regionSize[region];
411 0 : startingLand.push(base.accessIndex);
412 0 : break;
413 : }
414 : }
415 0 : let cell = gameState.getPassabilityMap().cellSize;
416 0 : startingSize = startingSize * cell * cell;
417 0 : if (this.Config.debug > 1)
418 0 : API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)");
419 0 : if (startingSize < 25000)
420 : {
421 0 : this.saveSpace = true;
422 0 : this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16);
423 0 : let num = Math.max(this.Config.Economy.targetNumFishers, 2);
424 0 : for (let land of startingLand)
425 : {
426 0 : for (let sea of gameState.ai.accessibility.regionLinks[land])
427 0 : if (gameState.ai.HQ.navalRegions[sea])
428 0 : this.navalManager.updateFishingBoats(sea, num);
429 : }
430 0 : this.maxFields = 1;
431 0 : this.needCorral = true;
432 : }
433 0 : else if (startingSize < 60000)
434 0 : this.maxFields = 2;
435 : else
436 0 : this.maxFields = false;
437 :
438 : // - count the available food resource, and react accordingly
439 0 : let startingFood = gameState.getResources().food;
440 0 : startingFood += this.getTotalResourceLevel(gameState, ["food"], ["nearby", "medium", "faraway"]).food;
441 :
442 0 : if (startingFood < 800)
443 : {
444 0 : if (startingSize < 25000)
445 : {
446 0 : this.needFish = true;
447 0 : this.Config.Economy.popForDock = 1;
448 : }
449 : else
450 0 : this.needFarm = true;
451 : }
452 : // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
453 0 : let startingWood = gameState.getResources().wood;
454 0 : startingWood += this.getTotalResourceLevel(gameState, ["wood"], ["nearby", "medium", "faraway"]).wood;
455 :
456 0 : if (this.Config.debug > 1)
457 0 : API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)");
458 0 : if (startingWood < 6000)
459 : {
460 0 : this.saveResources = true;
461 0 : this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand
462 :
463 0 : if (startingWood < 2000 && this.needFarm)
464 : {
465 0 : this.needCorral = true;
466 0 : this.needFarm = false;
467 : }
468 : }
469 0 : if (startingWood > 8500 && this.canBuildUnits)
470 : {
471 0 : let allowed = Math.ceil((startingWood - 8500) / 3000);
472 : // Not useful to prepare rushing if too long ceasefire
473 0 : if (gameState.isCeasefireActive())
474 : {
475 0 : if (gameState.ceasefireTimeRemaining > 900)
476 0 : allowed = 0;
477 0 : else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1)
478 0 : allowed = 1;
479 : }
480 0 : this.attackManager.setRushes(allowed);
481 : }
482 :
483 : // immediatly build a wood dropsite if possible.
484 0 : if (!gameState.getOwnEntitiesByClass("DropsiteWood", true).hasEntities())
485 : {
486 0 : const newDP = this.baseManagers()[0].findBestDropsiteAndLocation(gameState, "wood");
487 0 : if (newDP.quality > 40 && this.canBuild(gameState, newDP.templateName))
488 : {
489 : // if we start with enough workers, put our available resources in this first dropsite
490 : // same thing if our pop exceed the allowed one, as we will need several houses
491 0 : let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length;
492 0 : if (numWorkers > 12 && newDP.quality > 60 ||
493 : gameState.getPopulation() > gameState.getPopulationLimit() + 20)
494 : {
495 0 : const cost = new API3.Resources(gameState.getTemplate(newDP.templateName).cost());
496 0 : gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites");
497 : }
498 0 : gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers()[0].ID }, newDP.pos));
499 : }
500 : }
501 : // and build immediately a corral if needed
502 0 : if (this.needCorral)
503 : {
504 0 : const template = gameState.applyCiv("structures/{civ}/corral");
505 0 : if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template))
506 0 : gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers()[0].ID }));
507 : }
508 : };
|