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: |
|
|
|
|
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
|
|
|
|