Passed
Push — master ( 3da424...a0c806 )
by Marek
01:50
created

AIs.ais_mutant.Mutant._system_manager()   F

Complexity

Conditions 19

Size

Total Lines 61
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 50
nop 1
dl 0
loc 61
rs 0.5999
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like AIs.ais_mutant.Mutant._system_manager() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
import random, copy, math
21
22
from ige import log
23
from ige.ospace import Const
24
from ige.ospace import Rules
25
from ige.ospace import Utils
26
27
import ai_tools as tool
28
from ai import AI
29
30
class Mutant(AI):
31
    def __init__(self, client):
32
        super(Mutant, self).__init__(client)
33
        tool.doRelevance(self.data, self.client, self.db, 10)
34
35
    def _system_worthiness(self, system, weights):
36
        """ Scans system, and based on planetary composition and weights returns constant.
37
        Weights are expected to be quadruplet of numbers, for [gaia, terrestial, marginal, rest]
38
        """
39
        worth = 0
40
        for planet_id in self.data.myPlanets & set(system.planets):
41
            planet = self.db[planet_id]
42
            if planet.plType == u"I":  # gaia
43
                worth += weights[0]
44
            elif planet.plType == u"E":  # terrestial
45
                worth += weights[1]
46
            elif planet.plType == u"M":  # marginal
47
                worth += weights[2]
48
            else:  # junk
49
                worth += weights[3]
50
        return worth
51
52
    def _create_gaia_blueprint(self, space):
53
        # preserve minefield position, and in case there is no
54
        # minefield in the system, try to place it on the first planet
55
        # available
56
        power_plants = math.ceil(max(space - 1, 0) / 6.0)
57
        factories = space - power_plants
58
        return {Rules.Tech.MUTANTBASE4:1,
59
                Rules.Tech.MUTANTPP2:power_plants,
60
                Rules.Tech.MUTANTFACT2:factories}
61
62
    def _create_terrestial_blueprint(self, space):
63
        # preserve minefield position, and in case there is no
64
        # minefield in the system, try to place it on the first planet
65
        # available
66
        power_plants = math.ceil(max(space - 1, 0) / 5.0)
67
        factories = space - power_plants
68
        return {Rules.Tech.MUTANTBASE3:1,
69
                Rules.Tech.MUTANTPP2:power_plants,
70
                Rules.Tech.MUTANTFACT2:factories}
71
72
    def _create_marginal_blueprint(self, space):
73
        # preserve minefield position, and in case there is no
74
        # minefield in the system, try to place it on the first planet
75
        # available
76
        power_plants = math.ceil(max(space - 1, 0) / 7.0)
77
        factories = space - power_plants
78
        return {Rules.Tech.MUTANTBASE2:1,
79
                Rules.Tech.MUTANTPP2:power_plants,
80
                Rules.Tech.MUTANTFACT1:factories}
81
82
    def _create_submarginal_blueprint(self, space):
83
        # preserve minefield position, and in case there is no
84
        # minefield in the system, try to place it on the first planet
85
        # available
86
        power_plants = math.ceil(max(space - 1, 0) / 5.0)
87
        factories = space - power_plants
88
        return {Rules.Tech.MUTANTBASE:1,
89
                Rules.Tech.MUTANTPP1:power_plants,
90
                Rules.Tech.MUTANTFACT1:factories}
91
92
    def _insert_minefield(self, system, system_blueprint):
93
        # pick worst possible planet to put minefield on, don't waste
94
        # precious gaia space if possible
95
        # also ignore actual state - don't be afraid to rebuild if planet is
96
        # promoted
97
        for avoided_types in [(u"I", u"E", u"M"), (u"I", u"E"), (u"I",), ()]:
98
            # sorting, to avoid rebuilds between equivalent planets
99
            for planet_id in sorted(system_blueprint):
100
                planet = self.db[planet_id]
101
                if planet.plType in avoided_types or planet.plSlots == 1:
102
                    continue
103
                if Rules.Tech.MUTANTFACT1 in system_blueprint[planet_id]:
104
                    assert system_blueprint[planet_id][Rules.Tech.MUTANTFACT1] > 0
105
                    system_blueprint[planet_id][Rules.Tech.MUTANTFACT1] -= 1
106
                elif Rules.Tech.MUTANTFACT2 in system_blueprint[planet_id]:
107
                    assert system_blueprint[planet_id][Rules.Tech.MUTANTFACT2] > 0
108
                    system_blueprint[planet_id][Rules.Tech.MUTANTFACT2] -= 1
109
                else:
110
                    continue
111
                system_blueprint[planet_id][Rules.Tech.MUTANTMINES] = 1
112
                return
113
114
    def _cleanup_dict(self, dict_):
115
        for key in dict_.keys():
116
            if not dict_[key]:
117
                del dict_[key]
118
119
    def _create_system_blueprint(self, system):
120
        # create appropriate build plans
121
        system_blueprint = {}
122
        for planet_id in self.data.freePlanets & set(system.planets):
123
            system_blueprint[planet_id] = {Rules.Tech.MUTANTBASE:1}
124
        for planet_id in self.data.myPlanets & set(system.planets):
125
            planet = self.db[planet_id]
126
            space = planet.plSlots - 1 # the main building is there every time
127
            if planet.plType == u"I":  # gaia
128
                system_blueprint[planet_id] = self._create_gaia_blueprint(space)
129
                continue
130
            elif planet.plType == u"E":  # terrestial
131
                system_blueprint[planet_id] = self._create_terrestial_blueprint(space)
132
                continue
133
            elif planet.plType == u"M":  # marginal
134
                system_blueprint[planet_id] = self._create_marginal_blueprint(space)
135
                continue
136
            else: # all sub-marginal types
137
                system_blueprint[planet_id] = self._create_submarginal_blueprint(space)
138
                continue
139
        self._cleanup_dict(system_blueprint)
140
        self._insert_minefield(system, system_blueprint)
141
        return system_blueprint
142
143
    def _system_manager(self):
144
        for planet_id in self.data.myPlanets:
145
            tool.sortStructures(self.client, self.db, planet_id)
146
        for system_id in self.data.mySystems:
147
            system = self.db[system_id]
148
            # creation of final system plans
149
            system_blueprint = self._create_system_blueprint(system)
150
            idle_planets = tool.buildSystem(self.data, self.client, self.db, system_id, self.data.myProdPlanets & set(system.planets), system_blueprint)
151
            # rest of the planets build ships
152
            # first get all our ships in the system
153
            system_fleet = {}
154
            for fleet_id in getattr(system, 'fleets', []):
155
                fleet = self.db[fleet_id]
156
                if getattr(fleet, 'owner', Const.OID_NONE) == self.player.oid:
157
                    system_fleet = Utils.dictAddition(system_fleet, tool.getFleetSheet(fleet))
158
            hasSeeders = False
159
            hasSeekers = False
160
            try:
161
                if system_fleet[2] >= 2: hasSeeders = True
162
            except KeyError:
163
                pass
164
            try:
165
                if system_fleet[3] >= 2: hasSeekers = True
166
            except KeyError:
167
                pass
168
            # this variable will gather how valuable system is in regards of fighter defense
169
            # in general, mutant has quite significant planetary defense, so our target is
170
            # to have only about 10 % production spend on support
171
            fighters_to_defend = self._system_worthiness(system, [15,8,5,3])
172
173
            for planet_id in idle_planets:
174
                planet = self.db[planet_id]
175
                shipDraw = random.randint(1, 10)
176
                if (not hasSeeders or not hasSeekers) and shipDraw < 9:
177
                    # there is 20% chance it won't build civilian ships, but military one
178
                    if not hasSeeders:
179
                        planet.prodQueue, self.player.stratRes = self.client.cmdProxy.startConstruction(planet_id, 2, 1, planet_id, True, False, Const.OID_NONE)
180
                        continue
181
                    elif not hasSeekers:
182
                        planet.prodQueue, self.player.stratRes = self.client.cmdProxy.startConstruction(planet_id, 3, 1, planet_id, True, False, Const.OID_NONE)
183
                        continue
184
                # rest is creation of ships based on current state + expected guard fighters
185
                try:
186
                    fighters = system_fleet[1]
187
                except KeyError:
188
                    fighters = 0
189
                try:
190
                    bombers = system_fleet[4]
191
                except KeyError:
192
                    bombers = 0
193
                expected_fighters = bombers * 1.5 + fighters_to_defend
194
                weight_fighter = 3
195
                weight_bomber = 2
196
                if expected_fighters > fighters:
197
                    # we have to build more fighters
198
                    weight_fighter += 1
199
                elif expected_fighters < fighters:
200
                    # we have too many fighters - let's prefer bombers for now
201
                    weight_bomber += 1
202
                choice = Utils.weightedRandom([1,4], [weight_fighter, weight_bomber])
203
                planet.prodQueue, self.player.stratRes = self.client.cmdProxy.startConstruction(planet_id, choice, 2, planet_id, True, False, Const.OID_NONE)
204
205
    def _colonize_occupied_systems(self, seeder_id):
206
        seeder_fleets = self.data.myFleetsWithDesign.get(seeder_id, set())
207
        should_repeat = False
208
        for fleet_id in copy.copy(seeder_fleets & self.data.idleFleets):
209
            fleet = self.db[fleet_id]
210
            orbit_id = fleet.orbiting
211
            if orbit_id != Const.OID_NONE:
212
                orbit = self.db[orbit_id]
213
                if set(orbit.planets) & self.data.freePlanets and orbit_id in self.data.otherSystems:
214
                    target_id = self._find_best_planet(set(orbit.planets) & self.data.freePlanets)
215
                    fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db,
216
                        {seeder_id:1}, True, fleet_id, Const.FLACTION_DEPLOY, target_id, seeder_id)
217
                    self.data.myFleetSheets[fleet_id][seeder_id] -= 1
218
                    if self.data.myFleetSheets[fleet_id][seeder_id] == 0:
219
                        del self.data.myFleetSheets[fleet_id][seeder_id]
220
                        seeder_fleets.remove(fleet_id)
221
        return should_repeat
222
223
    def _expansion_manager(self):
224
        should_repeat = True
225
        seeker_id = 3
226
        seeder_id = 2
227
        while should_repeat:
228
            should_repeat = False
229
            should_repeat |= self._explore(seeker_id)
230
            should_repeat |= self._colonize_free_systems(copy.copy(self.data.freeSystems), seeder_id)
231
            should_repeat |= self._colonize_occupied_systems(seeder_id)
232
233
    def _ship_design_manager(self):
234
        # there are 4 basic designs    created by the server
235
        # 1: Swarmer [Small hull, Cockpit, 2x EMCannon, 2xFTL]
236
        # 2: Seeder [Medium hull, Cockpit, Mutant Colony Pod, 4xFTL]
237
        # 3: Seeker [Small hull, Cockpit, 1x ActiveScan, 2xFTL]
238
        # 4: Sower [Small hull, Cockpit, 1x Conv.Bomb, 2xFTL]
239
        pass
240
241
    def _logistics_manager(self):
242
        for system_id in self.data.mySystems - self.data.myRelevantSystems:
243
            system = self.db[system_id]
244
            for fleet_id in set(system.fleets) & self.data.idleFleets:
245
                fleet = self.db[fleet_id]
246
                subfleet = tool.getSubfleet(fleet, {1:0, 4:0}, False)
247
                if len(subfleet):
248
                    fleet_range = tool.subfleetMaxRange(self.client, self.db, {1:0, 4:0}, fleet_id)
249
                    relevant_sys_id = tool.findNearest(self.db, system, self.data.myRelevantSystems, fleet_range)
250
                    if relevant_sys_id:
251
                        relevant_sys_id = relevant_sys_id[0]
252
                        fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db,
253
                            {1:0, 4:0}, True, fleet_id, Const.FLACTION_MOVE, relevant_sys_id, None)
254
                        self.data.idleFleets -= set([fleet_id])
255
                    else:
256
                        min_dist = fleet_range
257
                        min_dist_sys_id = None
258
                        min_dist_rel = self.data.distanceToRelevance[system_id]
259
                        for temp_id, dist in self.data.distanceToRelevance.items():
260
                            temp = self.db[temp_id]
261
                            distance = math.hypot(temp.x - system.x, temp.y - system.y)
262
                            if distance < min_dist and dist < min_dist_rel:
263
                                min_dist = distance
264
                                min_dist_sys_id = temp_id
265
                                min_dist_rel = dist
266
                        if min_dist_sys_id:
267
                            fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db,
268
                                {1:0, 4:0}, True, fleet_id, Const.FLACTION_MOVE, min_dist_sys_id, None)
269
                            self.data.idleFleets -= set([fleet_id])
270
271
    def _get_attack_fleets(self):
272
        attack_fleets = set()
273
        for fleet_id in copy.copy(self.data.myFleets):
274
            fleet = self.db.get(fleet_id, None)
275
            # minimal size of attack fleet is determined by size of originating system - larger
276
            # more developed systems will stage stronger attack fleets
277
            try:
278
                system = self.db[fleet.orbiting]
279
            except KeyError:
280
                # this fleet is not on orbit, set legacy value
281
                minimum = 12
282
            else:
283
                minimum = self._system_worthiness(system, [8,5,3,2]) + 10
284
            if getattr(fleet, 'target', Const.OID_NONE) == Const.OID_NONE and getattr(fleet, 'ships', []):
285
                # this also covers fleets fighting over enemy systems - in that case, there
286
                # is slight chance the fleet will continue to the next system without conquering
287
                # the system first
288
                if fleet.orbiting in self.data.otherSystems and Utils.weightedRandom([True, False], [9,1]):
289
                    continue
290
                if tool.fleetContains(fleet, {1:minimum, 4:minimum}):
291
                    attack_fleets.add(fleet_id)
292
        return attack_fleets
293
294
    def _attack_manager(self):
295
        for fleet_id in self._get_attack_fleets():
296
            fleet = self.db[fleet_id]
297
            # send the attack fleet, if in range
298
            sheet = tool.getFleetSheet(fleet)
299
            sowers = sheet[4]
300
            swarmers = min(sheet[1], math.ceil(sowers * 1.5))
301
            max_range = 0.8 * tool.subfleetMaxRange(self.client, self.db, {1:swarmers, 4:sowers}, fleet_id)
302
            # four nearest systems are considered, with probability to be chosen based on order
303
            nearest = tool.findNearest(self.db, fleet, self.data.otherSystems, max_range, 4)
304
            if len(nearest):
305
                # range is adjusted to flatten probabilities a bit
306
                probability_map = map(lambda x: x ** 2, range(6, 2, -1))
307
                target = Utils.weightedRandom(nearest, probability_map)
308
309
                fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db,
310
                    {1:swarmers, 4:sowers}, True,
311
                    fleet_id, Const.FLACTION_MOVE, target, None)
312
313
    def economy_manager(self):
314
        self._expansion_manager()
315
        self._system_manager()
316
317
    def offense_manager(self):
318
        self._ship_design_manager()
319
        self._logistics_manager()
320
        self._attack_manager()
321
322
    def run(self):
323
        self.economy_manager()
324
        self.offense_manager()
325
326
def run(aclient):
327
    ai = Mutant(aclient)
328
    ai.run()
329
    aclient.saveDB()
330
331