1 | # |
||
2 | # Copyright 2001 - 2016 Ludek Smid [http://www.ospace.net/] |
||
3 | # |
||
4 | # This file is part of Outer Space. |
||
5 | # |
||
6 | # Outer Space is free software; you can redistribute it and/or modify |
||
7 | # it under the terms of the GNU General Public License as published by |
||
8 | # the Free Software Foundation; either version 2 of the License, or |
||
9 | # (at your option) any later version. |
||
10 | # |
||
11 | # Outer Space is distributed in the hope that it will be useful, |
||
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | # GNU General Public License for more details. |
||
15 | # |
||
16 | # You should have received a copy of the GNU General Public License |
||
17 | # along with Outer Space; if not, write to the Free Software |
||
18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
||
19 | # |
||
20 | |||
21 | import copy |
||
22 | import math |
||
23 | from ige import log |
||
24 | from ige.IDataHolder import IDataHolder |
||
25 | from ige.ospace import Const |
||
26 | from ige.ospace import Rules |
||
27 | from ige.ospace import Utils |
||
28 | |||
29 | |||
30 | |||
31 | def tool_parseDB(client, db, enemyTypes): |
||
32 | """ Parses all data in db for needs of other tools. Other in the name |
||
33 | means other players. |
||
34 | |||
35 | """ |
||
36 | data = IDataHolder() |
||
37 | data.myPlanets = set() |
||
38 | data.myProdPlanets = set() |
||
39 | data.mySystems = set() |
||
40 | data.freePlanets = set() |
||
41 | data.freeSystems = set() |
||
42 | data.nonhabPlanets = set() |
||
43 | data.unknownSystems = set() |
||
44 | data.otherPlanets = set() |
||
45 | data.enemyPlanets = set() |
||
46 | data.otherSystems = set() |
||
47 | data.enemySystems = set() |
||
48 | data.systems = set() |
||
49 | data.myFleets = set() |
||
50 | data.myMPPerSystem = {} |
||
51 | data.myTargetedSystems = set() |
||
52 | data.endangeredSystems = {} |
||
53 | data.otherFleets = set() |
||
54 | data.otherInboundFleets = set() |
||
55 | data.idleFleets = set() |
||
56 | data.myFleetsWithDesign = {} |
||
57 | data.myFleetSheets = {} |
||
58 | data.pirateSystems = set() |
||
59 | data.relevantSystems = set() |
||
60 | data.myRelevantSystems = set() |
||
61 | data.distanceToRelevance = {} |
||
62 | playerID = client.getPlayerID() |
||
63 | player = client.getPlayer() |
||
64 | owners = {} |
||
65 | for objID in db.keys(): |
||
66 | try: |
||
67 | obj = db[objID] |
||
68 | except KeyError: |
||
69 | # TODO find out why there are these errors |
||
70 | continue |
||
71 | objType = getattr(obj, 'type', None) |
||
72 | if objType == Const.T_PLANET: |
||
73 | ownerID = getattr(obj, 'owner', None) |
||
74 | plSlots = getattr(obj, 'plSlots', 0) |
||
75 | slots = getattr(obj, 'slots', []) |
||
76 | prodProd = getattr(obj, 'prodProd', 0) |
||
77 | plType = getattr(obj, 'plType', None) |
||
78 | if plType == u'G' or plType == u'A': |
||
79 | data.nonhabPlanets.add(objID) |
||
80 | continue |
||
81 | if ownerID == playerID and prodProd: |
||
82 | data.myProdPlanets.add(objID) |
||
83 | data.mySystems.add(obj.compOf) |
||
84 | elif ownerID == playerID and not prodProd: |
||
85 | # myPlanets are later joined by myProdPlanets |
||
86 | data.myPlanets.add(objID) |
||
87 | data.mySystems.add(obj.compOf) |
||
88 | elif ownerID == Const.OID_NONE and plSlots: |
||
89 | data.freePlanets.add(objID) |
||
90 | elif not plSlots: |
||
91 | data.unknownSystems.add(obj.compOf) |
||
92 | else: |
||
93 | # systems with owner other than myself, ignore EDEN planets |
||
94 | if not ownerID: |
||
95 | continue |
||
96 | elif ownerID not in owners: |
||
97 | owners[ownerID] = client.get(ownerID, publicOnly = 1) |
||
98 | |||
99 | if not getattr(owners[ownerID], 'type', Const.OID_NONE) == Const.T_AIEDENPLAYER: |
||
100 | data.otherSystems.add(db[objID].compOf) |
||
101 | data.otherPlanets.add(objID) |
||
102 | if getattr(owners[ownerID], 'type', Const.OID_NONE) in enemyTypes: |
||
103 | data.enemySystems.add(db[objID].compOf) |
||
104 | data.enemyPlanets.add(objID) |
||
105 | if getattr(owners[ownerID], 'type', Const.OID_NONE) in (Const.T_AIPIRPLAYER, Const.T_PIRPLAYER): |
||
106 | data.pirateSystems.add(db[objID].compOf) |
||
107 | elif objType == Const.T_SYSTEM: |
||
108 | if getattr(obj, "starClass", "a")[0] == 'b': |
||
109 | # black hole -> nothing to see here, let's ignore it completely |
||
110 | continue |
||
111 | data.systems.add(objID) |
||
112 | if not hasattr(db[objID], 'planets'): |
||
113 | data.unknownSystems.add(objID) |
||
114 | elif objType == Const.T_FLEET: |
||
115 | ownerID = getattr(obj, 'owner', None) |
||
116 | if ownerID == playerID: |
||
117 | data.myFleets.add(objID) |
||
118 | data.myFleetSheets[objID] = getFleetSheet(obj) |
||
119 | if len(obj.actions[obj.actionIndex:]) == 0: |
||
120 | data.idleFleets.add(objID) |
||
121 | for designID in data.myFleetSheets[objID].keys(): |
||
122 | if not data.myFleetsWithDesign.get(designID, set()): |
||
123 | data.myFleetsWithDesign[designID] = set([objID]) |
||
124 | else: |
||
125 | data.myFleetsWithDesign[designID] |= set([objID]) |
||
126 | else: |
||
127 | data.otherFleets.add(objID) |
||
128 | # ================== |
||
129 | # second phase |
||
130 | # analyzing fleet action queues |
||
131 | for fleetID in data.myFleets: |
||
132 | fleet = db[fleetID] |
||
133 | for orType, orTargID, orData in fleet.actions[fleet.actionIndex:]: |
||
134 | if orType == Const.FLACTION_WAIT: |
||
135 | continue |
||
136 | elif orType == Const.FLACTION_REPEATFROM: |
||
137 | continue |
||
138 | elif orType == Const.FLACTION_REDIRECT: |
||
139 | if orTargID == Const.OID_NONE: |
||
140 | continue |
||
141 | orTarg = db[orTargID] |
||
142 | if orTarg.type == Const.T_SYSTEM: |
||
143 | data.unknownSystems -= set([orTargID]) |
||
144 | elif orTarg.type == Const.T_PLANET: |
||
145 | data.unknownSystems -= set([orTarg.compOf]) |
||
146 | # deploy order removes target from free planets set |
||
147 | # if deploying to non-free planet, change order TODO [non-systematic] |
||
148 | if orType == Const.FLACTION_DEPLOY: |
||
149 | if orTargID in data.freePlanets: |
||
150 | data.freePlanets -= set([orTargID]) |
||
151 | else: |
||
152 | client.cmdProxy.deleteAction(fleetID, fleet.actionIndex) |
||
153 | # fill data.myMPPerSystem |
||
154 | if len(fleet.actions[fleet.actionIndex:]) == 0: |
||
155 | if fleet.orbiting in data.mySystems: |
||
156 | try: |
||
157 | data.myMPPerSystem[fleet.orbiting] += fleet.combatPwr |
||
158 | except KeyError: |
||
159 | data.myMPPerSystem[fleet.orbiting] = fleet.combatPwr |
||
160 | else: |
||
161 | lastOrder = fleet.actions[len(fleet.actions)-1] |
||
162 | targetID = lastOrder[1] |
||
163 | if targetID in data.myPlanets: |
||
164 | sysID = db[targetID].compOf |
||
165 | try: |
||
166 | data.myMPPerSystem[sysID] += fleet.combatPwr |
||
167 | except KeyError: |
||
168 | data.myMPPerSystem[sysID] = fleet.combatPwr |
||
169 | elif targetID in data.mySystems: |
||
170 | try: |
||
171 | data.myMPPerSystem[targetID] += fleet.combatPwr |
||
172 | except KeyError: |
||
173 | data.myMPPerSystem[targetID] = fleet.combatPwr |
||
174 | |||
175 | data.myPlanets |= data.myProdPlanets |
||
176 | # only systems with free or nonhabitable planets are considered free |
||
177 | for systemID in data.systems: |
||
178 | isEmpty = True |
||
179 | hasEmpty = False |
||
180 | planets = set(getattr(db[systemID], 'planets', [])) |
||
181 | if planets and not planets - data.freePlanets - data.nonhabPlanets: |
||
182 | data.freeSystems.add(systemID) |
||
183 | # find attacking fleets |
||
184 | for fleetID in data.otherFleets: |
||
185 | fleet = db[fleetID] |
||
186 | if getattr(fleet, 'target', None): |
||
187 | targetID = getattr(fleet, 'target', None) |
||
188 | elif not getattr(fleet, 'orbiting', Const.OID_NONE) == Const.OID_NONE: |
||
189 | targetID = getattr(fleet, 'orbiting', Const.OID_NONE) |
||
190 | if targetID: |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
191 | if targetID in data.myPlanets: |
||
192 | data.myTargetedSystems.add(db[targetID].compOf) |
||
193 | data.otherInboundFleets.add(fleetID) |
||
194 | elif targetID in data.mySystems: |
||
195 | data.myTargetedSystems.add(targetID) |
||
196 | data.otherInboundFleets.add(fleetID) |
||
197 | return data |
||
198 | |||
199 | def doRelevance(data, client, db, rangeOfRelevance): |
||
200 | """ This function finds all systems, which are nearer to the players |
||
201 | system than is defined in rangeOfRelevance. This is saved in |
||
202 | data.relevantSystems. |
||
203 | Then, it saves all players planets in relevant distance from any |
||
204 | planet in data.otherSystems. And finally, it fills dictionary |
||
205 | data.distanceToRelevance, where each system of the player got its |
||
206 | distance to nearest relevant system of the player. |
||
207 | |||
208 | """ |
||
209 | for systemID in data.systems: |
||
210 | system = db[systemID] |
||
211 | for tempID in data.mySystems: |
||
212 | temp = db[tempID] |
||
213 | distance = math.hypot(system.x - temp.x, system.y - temp.y) |
||
214 | if distance <= rangeOfRelevance: |
||
215 | data.relevantSystems.add(systemID) |
||
216 | break |
||
217 | for systemID in data.mySystems: |
||
218 | system = db[systemID] |
||
219 | for tempID in data.otherSystems: |
||
220 | temp = db[tempID] |
||
221 | distance = math.hypot(system.x - temp.x, system.y - temp.y) |
||
222 | if distance <= rangeOfRelevance: |
||
223 | data.myRelevantSystems.add(systemID) |
||
224 | break |
||
225 | for systemID in data.mySystems - data.myRelevantSystems: |
||
226 | system = db[systemID] |
||
227 | relDist = 99999 |
||
228 | for tempID in data.myRelevantSystems: |
||
229 | temp = db[tempID] |
||
230 | distance = math.hypot(system.x - temp.x, system.y - temp.y) |
||
231 | relDist = min(relDist, distance) |
||
232 | data.distanceToRelevance[systemID] = relDist |
||
233 | |||
234 | def findInfluence(data, client, db, rangeOfInfluence, objectIDList): |
||
235 | """ Returns list of all systems, which distance to any object |
||
236 | from the objectList is less than rangeOfInfluence. |
||
237 | |||
238 | objectList -- iterable of IDs, and each of the objects in the db has |
||
239 | to have .x and .y numeric parameters. |
||
240 | |||
241 | """ |
||
242 | influencedSystems = set() |
||
243 | for systemID in data.systems: |
||
244 | system = db[systemID] |
||
245 | for tempID in objectIDList: |
||
246 | temp = db[tempID] |
||
247 | distance = math.hypot(system.x - temp.x, system.y - temp.y) |
||
248 | if distance <= rangeOfInfluence: |
||
249 | influencedSystems.add(systemID) |
||
250 | return influencedSystems |
||
251 | |||
252 | def doDanger(data, client, db): |
||
253 | """ Fills data.endangeredSystems dictionary. Each system of the player, |
||
254 | to which is heading some fleet of other player with military power |
||
255 | got its own record consisting of military power and number of ships heading |
||
256 | there. (It is the sum of all fleets). |
||
257 | Medium and large ships are counted as 2 and 4 ships each respectively. |
||
258 | |||
259 | """ |
||
260 | for fleetID in data.otherInboundFleets: |
||
261 | fleet = db[fleetID] |
||
262 | if not getattr(fleet, 'combatPwr', 0): |
||
263 | continue |
||
264 | if not getattr(fleet, 'orbiting', Const.OID_NONE) == Const.OID_NONE: |
||
265 | targID = fleet.orbiting |
||
266 | elif hasattr(fleet, 'target'): |
||
267 | targID = fleet.target |
||
268 | else: |
||
269 | continue |
||
270 | if targID in data.endangeredSystems: |
||
271 | milPow, ships = data.endangeredSystems[targID] |
||
272 | else: |
||
273 | milPow = ships = 0 |
||
274 | milPow += fleet.combatPwr |
||
275 | if hasattr(fleet, 'shipScan'): |
||
276 | for (name, shipClass, isMilitary), quantity in fleet.shipScan.items(): |
||
277 | if isMilitary: |
||
278 | ships += quantity * (shipClass + 1) ** 2 |
||
279 | data.endangeredSystems[targID] = (milPow, ships) |
||
280 | elif milPow > 0: |
||
281 | data.endangeredSystems[targID] = (milPow, ships) |
||
282 | |||
283 | def orderFleet(client, db, fleetID, order, targetID, orderData): |
||
284 | """ Orders fleet to do something. It removes old actions |
||
285 | to prevent queue overflow. |
||
286 | """ |
||
287 | fleet = db[fleetID] |
||
288 | fleet.actions, fleet.actionIndex = client.cmdProxy.clearProcessedActions(fleetID) |
||
289 | client.cmdProxy.addAction(fleetID, fleet.actionIndex+1, order, targetID, orderData) |
||
290 | return |
||
291 | |||
292 | def getFleetSheet(fleet): |
||
293 | """ Returns dictionary with key being design number, and value |
||
294 | number of ships in the fleet of that design. |
||
295 | """ |
||
296 | |||
297 | sheet = {} |
||
298 | for ship in fleet.ships: |
||
299 | try: |
||
300 | sheet[ship[0]] += 1 |
||
301 | except KeyError: |
||
302 | sheet[ship[0]] = 1 |
||
303 | return sheet |
||
304 | |||
305 | def getSubfleet(fleet, ships, needsExact): |
||
306 | """ Returns subfleet roster. |
||
307 | |||
308 | fleet - fleet object from which the subfleet is taken |
||
309 | ships - is dictionary with keys being design IDs, values are |
||
310 | demanded quantities, with value = 0 meaning "return all of the type" |
||
311 | None value means "return whole fleet" |
||
312 | needsExact - if true, all items in ships has to be in place to return |
||
313 | the roster, except 0 value, which means "all" |
||
314 | """ |
||
315 | |||
316 | newShips = {} |
||
317 | wholeFleet = getFleetSheet(fleet) |
||
318 | if not ships: |
||
319 | return wholeFleet |
||
320 | for desID in ships: |
||
321 | if ships[desID] == 0: |
||
322 | try: |
||
323 | newShips[desID] = wholeFleet[desID] |
||
324 | except KeyError: |
||
325 | continue |
||
326 | else: |
||
327 | try: |
||
328 | if needsExact and wholeFleet[desID] < ships[desID]: |
||
329 | return None |
||
330 | newShips[desID] = min(ships[desID], wholeFleet[desID]) |
||
331 | except KeyError: |
||
332 | if needsExact: |
||
333 | return None |
||
334 | return newShips |
||
335 | |||
336 | def fleetContains(fleet, ships): |
||
337 | """ Tests whether fleet contains all ships in "ships" dictionary. |
||
338 | |||
339 | Returns boolean value. |
||
340 | """ |
||
341 | |||
342 | sheet = getFleetSheet(fleet) |
||
343 | for desID in ships: |
||
344 | try: |
||
345 | if ships[desID] > sheet[desID]: |
||
346 | return False |
||
347 | except KeyError: |
||
348 | return False |
||
349 | return True |
||
350 | |||
351 | def orderPartFleet(client, db, ships, needsExact, fleetID, order, targetID, orderData): |
||
352 | """ Splits part of the fleet and assign it the order. |
||
353 | |||
354 | ships - is dictionary with keys being design IDs, value is |
||
355 | demanded integer, with value = 0 it means "send all" |
||
356 | needsExact - if true, send all ships in the "ships" or don't |
||
357 | send anything, if false, send at least what you have, |
||
358 | if you don't have all of them |
||
359 | """ |
||
360 | |||
361 | sheet = getFleetSheet(db[fleetID]) |
||
362 | for key in ships: |
||
363 | try: |
||
364 | if ships[key] == 0: |
||
365 | ships[key] = sheet[key] |
||
366 | except KeyError: |
||
367 | continue |
||
368 | if sheet == ships: |
||
369 | orderFleet(client, db, fleetID, order, targetID, orderData) |
||
370 | return None, db[fleetID], client.getPlayer().fleets |
||
371 | isValid = True |
||
372 | sendShips = {} |
||
373 | for key in ships: |
||
374 | try: |
||
375 | if ships[key] > sheet[key] and needsExact: |
||
376 | return None, db[fleetID], client.getPlayer().fleets |
||
377 | elif ships[key] == 0: |
||
378 | sendShips[key] = sheet[key] |
||
379 | else: |
||
380 | sendShips[key] = min(ships[key], sheet[key]) |
||
381 | except KeyError: |
||
382 | if needsExact: |
||
383 | return None, db[fleetID], client.getPlayer().fleets |
||
384 | if sendShips: |
||
385 | newShips = [] |
||
386 | newMaxEn = 0 |
||
387 | for ship in db[fleetID].ships: |
||
388 | if sendShips.get(ship[0], 0) > 0: |
||
389 | sendShips[ship[0]] -= 1 |
||
390 | newShips.append(ship) |
||
391 | newMaxEn += client.getPlayer().shipDesigns[ship[0]].storEn |
||
392 | if newShips: |
||
393 | newFleet, origFleet, fleetsout = client.cmdProxy.splitFleet(fleetID, |
||
394 | newShips, |
||
395 | newMaxEn) |
||
396 | db[newFleet.oid] = newFleet |
||
397 | db[origFleet.oid] = origFleet |
||
398 | orderFleet(client, db, newFleet.oid, order, targetID, orderData) |
||
399 | return newFleet, origFleet, fleetsout |
||
400 | return None, db[fleetID], client.getPlayer().fleets |
||
401 | else: |
||
402 | None, db[fleetID], client.getPlayer().fleets |
||
403 | |||
404 | def subfleetMaxRange(client, db, ships, fleetID, canDamage=False): |
||
405 | """ Counts range of subfleet in parsecs |
||
406 | |||
407 | ships - is dictionary with keys being design IDs, value is |
||
408 | demanded number, with value = 0 it means "send all of the type" |
||
409 | None value means "send all" ships |
||
410 | """ |
||
411 | |||
412 | fleet = db[fleetID] |
||
413 | subfleet = getSubfleet(fleet, ships, True) |
||
414 | if not subfleet: |
||
415 | return 0.0 |
||
416 | player = client.getPlayer() |
||
417 | storEn = 0 |
||
418 | operEn = 0 |
||
419 | speed = 99999 |
||
420 | for desID in subfleet: |
||
421 | design = player.shipDesigns[desID] |
||
422 | storEn += design.storEn * subfleet[desID] |
||
423 | operEn += design.operEn * subfleet[desID] |
||
424 | speed = min(design.speed, speed) |
||
425 | storEn = min(storEn, fleet.storEn) |
||
426 | return storEn / operEn * speed / 24 |
||
427 | |||
428 | def findNearest(db, obj, targets, maxDist=99999, number=1): |
||
429 | """ Searches through the targets, and returns list consisting of number of |
||
430 | the nearest objects to the objID, sorted from the nearest to the farthest. |
||
431 | Only requirement is that every item needs to have attributes x and y. |
||
432 | |||
433 | obj - the _object_ [not ID!] we try to find neighbours for |
||
434 | targets - set of object IDs |
||
435 | maxDist - maximum allowed distance |
||
436 | number - size of the required set, when number of targets is lesser |
||
437 | than number, it will just sort the targetIDs accordingly |
||
438 | """ |
||
439 | distances = {} |
||
440 | x, y = obj.x, obj.y |
||
441 | for targID in targets: |
||
442 | target = db[targID] |
||
443 | distance = math.hypot(x - target.x, y - target.y) |
||
444 | if distance not in distances: |
||
445 | distances[distance] = set([targID]) |
||
446 | else: |
||
447 | distances[distance] |= set([targID]) |
||
448 | relevantKeys = sorted(distances.keys())[:number] |
||
449 | final = [] |
||
450 | for key in relevantKeys: |
||
451 | if key > maxDist: |
||
452 | break |
||
453 | for targID in distances[key]: |
||
454 | final.append(targID) |
||
455 | number -= 1 |
||
456 | if not number: break |
||
457 | if not number: break |
||
458 | return final |
||
459 | |||
460 | def findPopCenterPlanets(db, planetsIDs): |
||
461 | """ It finds "center of mass" of population. |
||
462 | |||
463 | Returns sorted list of all planets, beginning with those nearest to the |
||
464 | found center. |
||
465 | |||
466 | """ |
||
467 | x = 0 |
||
468 | y = 0 |
||
469 | population = 0 |
||
470 | for planetID in planetsIDs: |
||
471 | planet = db[planetID] |
||
472 | x += planet.x * planet.storPop |
||
473 | y += planet.y * planet.storPop |
||
474 | population += planet.storPop |
||
475 | x /= population |
||
476 | y /= population |
||
477 | fakeObj = IDataHolder() |
||
478 | fakeObj.x = x |
||
479 | fakeObj.y = y |
||
480 | return findNearest(db, fakeObj, planetsIDs, maxDist=99999, number=len(planetsIDs)) |
||
481 | |||
482 | def orderFromSystem(data, client, db, ships, systemID, order, targetID, orderData): |
||
483 | """ Tries to send ships defined by ships dictionary, and using all |
||
484 | idle fleets in the system. |
||
485 | ships - is dictionary with keys being design IDs, value is |
||
486 | demanded number, with value = 0 it means "send all of the type" |
||
487 | None value means "send all" ships |
||
488 | systemID - ID of the system from which are fleets send |
||
489 | order, targetID, orderData - parameters of the order |
||
490 | |||
491 | Returns dictionary of ships _which remains to be send_, ie what of the |
||
492 | ships dictionary wasn't available in the system, and military power of |
||
493 | the send ships. |
||
494 | |||
495 | """ |
||
496 | log.debug('ai_tools orderFromSystem', ships, systemID) |
||
497 | system = db[systemID] |
||
498 | fleetsIDs = set(system.fleets) & data.idleFleets |
||
499 | milPow = 0 |
||
500 | if len(fleetsIDs) == 0: |
||
501 | return ships, 0 |
||
502 | for fleetID in fleetsIDs: |
||
503 | if ships == None: |
||
504 | orderFleet(client, db, fleetID, order, targetID, orderData) |
||
505 | continue |
||
506 | fleet = db[fleetID] |
||
507 | sheet = getFleetSheet(fleet) |
||
508 | toSend = {} |
||
509 | for key in copy.copy(ships): |
||
510 | try: |
||
511 | toSend[key] = min(ships[key], sheet[key]) |
||
512 | ships[key] = max(ships[key] - sheet[key], 0) |
||
513 | if ships[key] == 0: |
||
514 | del ships[key] |
||
515 | except KeyError: |
||
516 | continue |
||
517 | if toSend == {}: |
||
518 | continue |
||
519 | log.debug('ai_tools orderFromSystem - sending', toSend, fleetID) |
||
520 | newFleet, origFleet, fleetsout = orderPartFleet(client, db, toSend, False, fleetID, order, targetID, orderData) |
||
521 | milPow += getattr(newFleet, 'combatPwr', 0) |
||
522 | hasAll = True |
||
523 | for key in ships: |
||
524 | if not ships[key] == 0: |
||
525 | hasAll = False |
||
526 | break |
||
527 | if hasAll: break |
||
528 | return ships, milPow |
||
529 | |||
530 | def sortStructures(client, db, planetID): |
||
531 | """ Moves structures on the planet, so on the left are buildings producing |
||
532 | food, then buildings producing electricity, then buildings producing CPs. |
||
533 | Those with more than one relevant parameters "such as food + CPs" are |
||
534 | in the more important group [food > en > CP > rest]. |
||
535 | |||
536 | """ |
||
537 | planet = db[planetID] |
||
538 | player = client.getPlayer() |
||
539 | structs = {} |
||
540 | bioStructs = [] |
||
541 | enStructs = [] |
||
542 | prodStructs = [] |
||
543 | # fill the groups with positions of relevant structures |
||
544 | for techID, hp, something, eff in planet.slots: |
||
545 | techBio, techEn, techProd = getSystemStatsChange(client, db, techID, planetID, 0) |
||
546 | if techBio > 0: |
||
547 | bioStructs.append(techID) |
||
548 | elif techEn > 0: |
||
549 | enStructs.append(techID) |
||
550 | elif techProd > 0: |
||
551 | prodStructs.append(techID) |
||
552 | # how many moves are necessary? As we move each group separately, we |
||
553 | # assume, that structure in "bio" group has to be in |
||
554 | # position < len(bioStructs), same with other groups |
||
555 | needMoves = len(planet.slots) |
||
556 | pos = 0 |
||
557 | for techID, hp, something, eff in planet.slots: |
||
558 | if pos < len(bioStructs) and techID not in bioStructs: |
||
559 | needMoves = pos |
||
560 | break |
||
561 | elif pos >= len(bioStructs) and pos < len(bioStructs) + len(enStructs) and techID not in enStructs: |
||
562 | needMoves = pos |
||
563 | break |
||
564 | elif pos >= len(bioStructs) + len(enStructs) and pos < len(bioStructs) + len(enStructs) + len(prodStructs) and techID not in prodStructs: |
||
565 | needMoves = pos |
||
566 | break |
||
567 | else: |
||
568 | pos += 1 |
||
569 | # pos will be used once again |
||
570 | # we are correcting the order from left to right, so next struct won't |
||
571 | # get its position changed until it is moved itself |
||
572 | # move is made to the leftmost position of the group |
||
573 | for techID, hp, something, eff in planet.slots[needMoves:]: |
||
574 | if techID in bioStructs: |
||
575 | client.cmdProxy.moveStruct(planetID, pos, 0 - pos) |
||
576 | pos += 1 |
||
577 | pos = max(needMoves, len(bioStructs)) |
||
578 | needMoves = pos |
||
579 | for techID, hp, something, eff in planet.slots[needMoves:]: |
||
580 | if techID in enStructs: |
||
581 | client.cmdProxy.moveStruct(planetID, pos, len(bioStructs) - pos) |
||
582 | pos += 1 |
||
583 | pos = max(needMoves, len(bioStructs) + len(enStructs)) |
||
584 | needMoves = pos |
||
585 | for techID, hp, something, eff in planet.slots[needMoves:]: |
||
586 | if techID in prodStructs: |
||
587 | client.cmdProxy.moveStruct(planetID, pos, len(bioStructs) + len(enStructs) - pos) |
||
588 | pos += 1 |
||
589 | return |
||
590 | |||
591 | def getSystemStructStats(data, client, db, systemID, processQueues=True): |
||
592 | """ It go through all planets and structures, and creates IDataHolder |
||
593 | object, with roster of buildings, surplus of bio and en. |
||
594 | |||
595 | processQueues - if True, it go through all buildQueues and adjust all |
||
596 | statistics as it would be all done already. |
||
597 | |||
598 | Returns IDataHolder with parameters: |
||
599 | .bio - system surplus of biomass |
||
600 | .en - system surplus of en |
||
601 | .planets - dictionary, keys are planetIDs of players or free planets, |
||
602 | and values are dictionaries (huh) with keys being techIDs |
||
603 | and values being number of those structs. |
||
604 | |||
605 | """ |
||
606 | systemStats = IDataHolder() |
||
607 | system = db[systemID] |
||
608 | player = client.getPlayer() |
||
609 | myPlanets = set(system.planets) & data.myPlanets |
||
610 | systemStats.planets = {} |
||
611 | for planetID in myPlanets: |
||
612 | systemStats.planets[planetID] = {} |
||
613 | for planetID in set(system.planets) & data.freePlanets: |
||
614 | systemStats.planets[planetID] = {} |
||
615 | # creation of the .planets dictionary |
||
616 | for planetID in myPlanets: |
||
617 | planet = db[planetID] |
||
618 | for techID, hp, something, eff in planet.slots: |
||
619 | try: |
||
620 | systemStats.planets[planetID][techID] += 1 |
||
621 | except KeyError: |
||
622 | systemStats.planets[planetID][techID] = 1 |
||
623 | if not processQueues: |
||
624 | # do not look into the queue |
||
625 | continue |
||
626 | for task in getattr(planet, 'prodQueue', []): |
||
627 | if not task.isShip: |
||
628 | techID = task.techID |
||
629 | tech = client.getFullTechInfo(task.techID) |
||
630 | if tech.isStructure: |
||
631 | if task.targetID not in systemStats.planets.keys(): |
||
632 | continue |
||
633 | try: |
||
634 | systemStats.planets[task.targetID][techID] += 1 |
||
635 | except KeyError: |
||
636 | systemStats.planets[task.targetID][techID] = 1 |
||
637 | if task.demolishStruct: |
||
638 | try: |
||
639 | systemStats.planets[task.targetID][task.demolishStruct] -= 1 |
||
640 | except KeyError: |
||
641 | systemStats.planets[task.targetID][task.demolishStruct] = -1 |
||
642 | # by parsing .planets object, fill the .bio and .en parameters |
||
643 | systemStats.bio = 0 |
||
644 | systemStats.en = 0 |
||
645 | for planetID in systemStats.planets: |
||
646 | planet = db[planetID] |
||
647 | if planetID not in myPlanets: |
||
648 | continue |
||
649 | for techID in systemStats.planets[planetID]: |
||
650 | quantity = systemStats.planets[planetID][techID] |
||
651 | deltaBio, deltaEn, deltaProd = getSystemStatsChange(client, db, techID, planetID, 0) |
||
652 | tech = client.getFullTechInfo(techID) |
||
653 | systemStats.en += quantity * deltaEn |
||
654 | systemStats.bio += quantity * deltaBio |
||
655 | return systemStats |
||
656 | |||
657 | def getSystemStatsChange(client, db, techID, targetPlanetID, targetTechID): |
||
658 | """ Find out, how are the stats going to change with build of structure |
||
659 | with techID, on targetPlanetID, over targetTechID. |
||
660 | |||
661 | deltaProd - it is RAW production, ie no morale bonuses etc. |
||
662 | |||
663 | """ |
||
664 | planet = db[targetPlanetID] |
||
665 | player = client.getPlayer() |
||
666 | deltaBio = 0 |
||
667 | deltaEn = 0 |
||
668 | tech = client.getFullTechInfo(techID) |
||
669 | deltaEn -= tech.operEn |
||
670 | deltaBio -= tech.operWorkers / 100 |
||
671 | deltaEn += tech.prodEn * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodEnMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
672 | deltaBio += tech.prodBio * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodBioMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
673 | deltaProd = tech.prodProd * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodProdMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
674 | if targetTechID: |
||
675 | tech = client.getFullTechInfo(targetTechID) |
||
676 | deltaEn += tech.operEn |
||
677 | deltaBio += tech.operWorkers / 100 |
||
678 | deltaEn -= tech.prodEn * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodEnMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
679 | deltaBio -= tech.prodBio * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodBioMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
680 | deltaProd -= tech.prodProd * Rules.techImprEff[player.techs.get(techID, 1)] * sum([x*y/100.0 for x, y in zip(tech.prodProdMod, [planet.plBio, planet.plMin, planet.plEn, 100])]) |
||
681 | return deltaBio, deltaEn, deltaProd |
||
682 | |||
683 | def checkBuildQueues(data, client, db, systemID, prodPlanets): |
||
684 | system = db[systemID] |
||
685 | player = client.getPlayer() |
||
686 | for planetID in prodPlanets: |
||
687 | planet = db[planetID] |
||
688 | validTasks = 0 |
||
689 | while len(planet.prodQueue) > validTasks: |
||
690 | # validTasks is effectively the actual index |
||
691 | task = planet.prodQueue[validTasks] |
||
692 | if task.targetID in data.myPlanets | data.freePlanets | set([Const.OID_NONE, None]): |
||
693 | validTasks += 1 |
||
694 | else: |
||
695 | planet.prodQueue, player.stratRes = client.cmdProxy.abortConstruction(planetID, validTasks) |
||
696 | |||
697 | def buildSystem(data, client, db, systemID, prodPlanets, finalSystemPlan): |
||
698 | """ Assigns tasks to all idle planets with CP > 0 in one system, according |
||
699 | to object finalSystemPlan. There is NO guaranty it will rebuild it correctly |
||
700 | as no math model was made for it. It just try to build most effectively, |
||
701 | with keeping system in each step self sufficient. |
||
702 | For functioning correctly, it is probably necessary to have some |
||
703 | reserves [one planets builds slowly big farm, another knowing it builds |
||
704 | factory over only outpost, .. :)] |
||
705 | |||
706 | finalSystemPlan - dictionary, keys are planetIDs of players or free planets, |
||
707 | and values are dictionaries with keys being techIDs |
||
708 | and values being number of those structs. |
||
709 | |||
710 | """ |
||
711 | system = db[systemID] |
||
712 | player = client.getPlayer() |
||
713 | structStats = getSystemStructStats(data, client, db, systemID) |
||
714 | structsToBuild = {} |
||
715 | structsToDemolish = {} |
||
716 | difference = {} |
||
717 | checkBuildQueues(data, client, db, systemID, prodPlanets) |
||
718 | # parse final plan to set buildings which need to be build and those that |
||
719 | # may be demolished |
||
720 | for planetID in finalSystemPlan: |
||
721 | difference[planetID] = Utils.dictSubtraction(finalSystemPlan[planetID], structStats.planets[planetID]) |
||
722 | for planetID in difference: |
||
723 | structsToBuild[planetID] = {} |
||
724 | structsToDemolish[planetID] = {} |
||
725 | for techID in difference[planetID]: |
||
726 | if difference[planetID][techID] > 0: |
||
727 | structsToBuild[planetID][techID] = difference[planetID][techID] |
||
728 | elif difference[planetID][techID] < 0: |
||
729 | structsToDemolish[planetID][techID] = difference[planetID][techID] |
||
730 | idlePlanets = copy.copy(prodPlanets) |
||
731 | for planetID in prodPlanets: |
||
732 | planet = db[planetID] |
||
733 | if getattr(planet, 'prodQueue', None): |
||
734 | # something in the build queue, skip the planet |
||
735 | idlePlanets.remove(planetID) |
||
736 | continue |
||
737 | # start the most effective project [CP-wise], which is still leaving |
||
738 | # sustainable system |
||
739 | toBuild = getStructBuildEffectivity(client, db, planetID, structsToBuild.keys(), structsToBuild, structsToDemolish) |
||
740 | for techID, targetPlanetID, targetTechID in toBuild: |
||
741 | targetPlanet = db[targetPlanetID] |
||
742 | if len(targetPlanet.slots) == targetPlanet.plSlots and targetTechID == Const.OID_NONE: |
||
743 | continue |
||
744 | deltaBio, deltaEn, deltaProd = getSystemStatsChange(client, db, techID, targetPlanetID, targetTechID) |
||
745 | if structStats.bio + deltaBio >= 0 and structStats.en + deltaEn >= 0: |
||
746 | planet.prodQueue, player.stratRes = client.cmdProxy.startConstruction(planetID, |
||
747 | techID, 1, targetPlanetID, techID < 1000, 0, targetTechID) |
||
748 | idlePlanets.remove(planetID) |
||
749 | # remove this struct from possibility list |
||
750 | structsToBuild[targetPlanetID][techID] -= 1 |
||
751 | if structsToBuild[targetPlanetID][techID] == 0: |
||
752 | del structsToBuild[targetPlanetID][techID] |
||
753 | break |
||
754 | return idlePlanets |
||
755 | |||
756 | def getStructBuildEffectivity(client, db, planetID, targetIDs, structsToBuild, structsToDemo): |
||
757 | """ Tries to sort all possible projects given the limits by parameters by |
||
758 | their CP-wise effectivity |
||
759 | |||
760 | targetIDs - iterable of all planets, on which are defined structsToBuild, |
||
761 | and structsToDemo. |
||
762 | structToBuild, structsToDemo - dictionaries, keys are planetIDs of players |
||
763 | or free planets, and values are dictionaries with keys |
||
764 | being techIDs and values being number of those structs. |
||
765 | |||
766 | Returns list of tuples (techID, targetPlanetID, targetTechID), sorted by |
||
767 | how long it takes to "pay back itself" from fastest to longest. Negative |
||
768 | values are valued differently, but order is made with same principle. |
||
769 | |||
770 | """ |
||
771 | planet = db[planetID] |
||
772 | player = client.getPlayer() |
||
773 | possibilitiesBuild = {} |
||
774 | for targetID in targetIDs: |
||
775 | target = db[targetID] |
||
776 | if planetID == targetID: |
||
777 | coeff = 1 |
||
778 | else: |
||
779 | coeff = 2 |
||
780 | # if build on empty slot |
||
781 | for techID in structsToBuild[targetID]: |
||
782 | tech = client.getFullTechInfo(techID) |
||
783 | techEff = Rules.techImprEff[player.techs.get(techID, 1)] |
||
784 | eff = float(tech.prodProd) * techEff / (tech.buildProd * coeff) * sum([x*y/100.0 for x, y in zip(tech.prodProdMod, [target.plBio, target.plMin, target.plEn, 100])]) |
||
785 | eff = round(eff, 3) |
||
786 | try: |
||
787 | possibilitiesBuild[eff].append((techID, targetID, Const.OID_NONE)) |
||
788 | except KeyError: |
||
789 | possibilitiesBuild[eff] = [(techID, targetID, Const.OID_NONE)] |
||
790 | # if build over the another structure |
||
791 | for targTechID in structsToDemo[targetID]: |
||
792 | targTech = client.getFullTechInfo(targTechID) |
||
793 | targTechEff = Rules.techImprEff[player.techs.get(targTechID, 1)] |
||
794 | prod = targTech.prodProd * targTechEff * sum([x*y/100.0 for x, y in zip(targTech.prodProdMod, [target.plBio, target.plMin, target.plEn, 100])]) |
||
795 | for techID in structsToBuild[targetID]: |
||
796 | tech = client.getFullTechInfo(techID) |
||
797 | techEff = Rules.techImprEff[player.techs.get(techID, 1)] |
||
798 | finalProd = float(tech.prodProd) * techEff - prod |
||
799 | if finalProd > 0: |
||
800 | eff = finalProd / (tech.buildProd * coeff) * sum([x*y/100.0 for x, y in zip(tech.prodProdMod, [target.plBio, target.plMin, target.plEn, 100])]) |
||
801 | # negative values are handled separately, as division creates illogical coefficient |
||
802 | else: |
||
803 | eff = finalProd * tech.buildProd * coeff * sum([x*y/100.0 for x, y in zip(tech.prodProdMod, [target.plBio, target.plMin, target.plEn, 100])]) |
||
804 | eff = round(eff, 3) |
||
805 | try: |
||
806 | possibilitiesBuild[eff].append((techID, targetID, targTechID)) |
||
807 | except KeyError: |
||
808 | possibilitiesBuild[eff] = [(techID, targetID, targTechID)] |
||
809 | toBuild = [] |
||
810 | toDemo = [] |
||
811 | for infoTuple in [possibilitiesBuild[x] for x in sorted(possibilitiesBuild, reverse=True)]: |
||
812 | toBuild += infoTuple |
||
813 | return toBuild |
||
814 | |||
815 | def compareBuildStructPlans(plan1, plan2): |
||
816 | """ Compare both dictionaries. Only difference from normal comparison |
||
817 | is that not having key is the same, as having key with value 0. |
||
818 | |||
819 | Returns Bool value |
||
820 | |||
821 | """ |
||
822 | plan1Keys = set(plan1.keys()) |
||
823 | plan2Keys = set(plan2.keys()) |
||
824 | for key in plan1Keys - plan2Keys: |
||
825 | if plan1[key]: |
||
826 | return False |
||
827 | for key in plan2Keys - plan1Keys: |
||
828 | if plan2[key]: |
||
829 | return False |
||
830 | for key in plan1Keys & plan2Keys: |
||
831 | if not plan1[key] == plan2[key]: |
||
832 | return False |
||
833 | return True |
||
834 | |||
835 |