Issues (229)

client-ai/ai_tools.py (1 issue)

Severity
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
The variable targetID does not seem to be defined for all execution paths.
Loading history...
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