Completed
Push — master ( 79c35a...3e3ee6 )
by Marek
16s queued 14s
created

ai_tools.doRelevance()   C

Complexity

Conditions 9

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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