Total Complexity | 94 |
Total Lines | 385 |
Duplicated Lines | 13.51 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AIs.ais_mutant 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 ais_base 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 _explore(self, seeker_fleets): |
||
206 | should_repeat = False |
||
207 | for fleet_id in copy.copy(seeker_fleets & self.data.idleFleets): |
||
208 | max_range = tool.subfleetMaxRange(self.client, self.db, {3:1}, fleet_id) |
||
209 | nearest = tool.findNearest(self.db, self.db[fleet_id], self.data.unknownSystems, max_range) |
||
210 | if len(nearest) > 0: |
||
211 | system_id = nearest[0] |
||
212 | # send the fleet |
||
213 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
214 | {3:1}, True, fleet_id, Const.FLACTION_MOVE, system_id, None) |
||
215 | self.data.myFleetSheets[fleet_id][3] -= 1 |
||
216 | if self.data.myFleetSheets[fleet_id][3] == 0: |
||
217 | del self.data.myFleetSheets[fleet_id][3] |
||
218 | seeker_fleets.remove(fleet_id) |
||
219 | else: |
||
220 | should_repeat = True |
||
221 | self.data.unknownSystems.remove(system_id) |
||
222 | return should_repeat |
||
223 | |||
224 | View Code Duplication | def _colonize_free_systems(self, seeder_fleets): |
|
|
|||
225 | should_repeat = False |
||
226 | for fleet_id in copy.copy(seeder_fleets & self.data.idleFleets): |
||
227 | max_range = tool.subfleetMaxRange(self.client, self.db, {2:1}, fleet_id) |
||
228 | nearest = tool.findNearest(self.db, self.db[fleet_id], self.data.freeSystems, max_range) |
||
229 | if len(nearest) > 0: |
||
230 | system_id = nearest[0] |
||
231 | # finding best planet for deployment |
||
232 | system = self.db[system_id] |
||
233 | max_slots = 0 |
||
234 | largest_planet_id = None |
||
235 | for planet_id in system.planets: |
||
236 | planet = self.db[planet_id] |
||
237 | if max_slots < planet.plSlots: |
||
238 | max_slots = planet.plSlots |
||
239 | largest_planet_id = planet_id |
||
240 | # send the fleet |
||
241 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
242 | {2:1}, True, fleet_id, Const.FLACTION_DEPLOY, largest_planet_id, 2) |
||
243 | self.data.myFleetSheets[fleet_id][2] -= 1 |
||
244 | if self.data.myFleetSheets[fleet_id][2] == 0: |
||
245 | del self.data.myFleetSheets[fleet_id][2] |
||
246 | seeder_fleets.remove(fleet_id) |
||
247 | else: |
||
248 | should_repeat = True |
||
249 | self.data.freeSystems.remove(system_id) |
||
250 | return should_repeat |
||
251 | |||
252 | View Code Duplication | def _colonize_occupied_systems(self, seeder_fleets): |
|
253 | should_repeat = False |
||
254 | for fleet_id in copy.copy(seeder_fleets & self.data.idleFleets): |
||
255 | max_range = tool.subfleetMaxRange(self.client, self.db, {2:1}, fleet_id) |
||
256 | nearest = tool.findNearest(self.db, self.db[fleet_id], self.data.freeSystems, max_range) |
||
257 | fleet = self.db[fleet_id] |
||
258 | orbit_id = fleet.orbiting |
||
259 | if not orbit_id == Const.OID_NONE: |
||
260 | orbit = self.db[orbit_id] |
||
261 | if set(orbit.planets) & self.data.freePlanets and orbit_id in self.data.otherSystems: |
||
262 | max_slots = 0 |
||
263 | largest_planet_id = None |
||
264 | for planet_id in set(orbit.planets) & self.data.freePlanets: |
||
265 | planet = self.db[planet_id] |
||
266 | if max_slots < planet.plSlots: |
||
267 | max_slots = planet.plSlots |
||
268 | largest_planet_id = planet_id |
||
269 | # issue the deploy order |
||
270 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
271 | {2:1}, True, fleet_id, Const.FLACTION_DEPLOY, largest_planet_id, 2) |
||
272 | self.data.myFleetSheets[fleet_id][2] -= 1 |
||
273 | if self.data.myFleetSheets[fleet_id][2] == 0: |
||
274 | del self.data.myFleetSheets[fleet_id][2] |
||
275 | seeder_fleets.remove(fleet_id) |
||
276 | return should_repeat |
||
277 | |||
278 | def _expansion_manager(self): |
||
279 | should_repeat = True |
||
280 | seeker_fleets = self.data.myFleetsWithDesign.get(3, set()) |
||
281 | seeder_fleets = self.data.myFleetsWithDesign.get(2, set()) |
||
282 | while should_repeat: |
||
283 | should_repeat = False |
||
284 | should_repeat |= self._explore(seeker_fleets) |
||
285 | should_repeat |= self._colonize_free_systems(seeder_fleets) |
||
286 | should_repeat |= self._colonize_occupied_systems(seeder_fleets) |
||
287 | |||
288 | def _ship_design_manager(self): |
||
289 | # there are 4 basic designs created by the server |
||
290 | # 1: Swarmer [Small hull, Cockpit, 2x EMCannon, 2xFTL] |
||
291 | # 2: Seeder [Medium hull, Cockpit, Mutant Colony Pod, 4xFTL] |
||
292 | # 3: Seeker [Small hull, Cockpit, 1x ActiveScan, 2xFTL] |
||
293 | # 4: Sower [Small hull, Cockpit, 1x Conv.Bomb, 2xFTL] |
||
294 | pass |
||
295 | |||
296 | def _logistics_manager(self): |
||
297 | for system_id in self.data.mySystems - self.data.myRelevantSystems: |
||
298 | system = self.db[system_id] |
||
299 | for fleet_id in set(system.fleets) & self.data.idleFleets: |
||
300 | fleet = self.db[fleet_id] |
||
301 | subfleet = tool.getSubfleet(fleet, {1:0, 4:0}, False) |
||
302 | if len(subfleet): |
||
303 | fleet_range = tool.subfleetMaxRange(self.client, self.db, {1:0, 4:0}, fleet_id) |
||
304 | relevant_sys_id = tool.findNearest(self.db, system, self.data.myRelevantSystems, fleet_range) |
||
305 | if relevant_sys_id: |
||
306 | relevant_sys_id = relevant_sys_id[0] |
||
307 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
308 | {1:0, 4:0}, True, fleet_id, Const.FLACTION_MOVE, relevant_sys_id, None) |
||
309 | self.data.idleFleets -= set([fleet_id]) |
||
310 | else: |
||
311 | min_dist = fleet_range |
||
312 | min_dist_sys_id = None |
||
313 | min_dist_rel = self.data.distanceToRelevance[system_id] |
||
314 | for temp_id, dist in self.data.distanceToRelevance.items(): |
||
315 | temp = self.db[temp_id] |
||
316 | distance = math.hypot(temp.x - system.x, temp.y - system.y) |
||
317 | if distance < min_dist and dist < min_dist_rel: |
||
318 | min_dist = distance |
||
319 | min_dist_sys_id = temp_id |
||
320 | min_dist_rel = dist |
||
321 | if min_dist_sys_id: |
||
322 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
323 | {1:0, 4:0}, True, fleet_id, Const.FLACTION_MOVE, min_dist_sys_id, None) |
||
324 | self.data.idleFleets -= set([fleet_id]) |
||
325 | |||
326 | def _get_attack_fleets(self): |
||
327 | attack_fleets = set() |
||
328 | for fleet_id in copy.copy(self.data.myFleets): |
||
329 | fleet = self.db.get(fleet_id, None) |
||
330 | # minimal size of attack fleet is determined by size of originating system - larger |
||
331 | # more developed systems will stage stronger attack fleets |
||
332 | try: |
||
333 | system = self.db[fleet.orbiting] |
||
334 | except KeyError: |
||
335 | # this fleet is not on orbit, set legacy value |
||
336 | minimum = 12 |
||
337 | else: |
||
338 | minimum = self._system_worthiness(system, [8,5,3,2]) + 10 |
||
339 | if getattr(fleet, 'target', Const.OID_NONE) == Const.OID_NONE and getattr(fleet, 'ships', []): |
||
340 | # this also covers fleets fighting over enemy systems - in that case, there |
||
341 | # is slight chance the fleet will continue to the next system without conquering |
||
342 | # the system first |
||
343 | if fleet.orbiting in self.data.otherSystems and Utils.weightedRandom([True, False], [9,1]): |
||
344 | continue |
||
345 | if tool.fleetContains(fleet, {1:minimum, 4:minimum}): |
||
346 | attack_fleets.add(fleet_id) |
||
347 | return attack_fleets |
||
348 | |||
349 | def _attack_manager(self): |
||
350 | for fleet_id in self._get_attack_fleets(): |
||
351 | fleet = self.db[fleet_id] |
||
352 | # send the attack fleet, if in range |
||
353 | sheet = tool.getFleetSheet(fleet) |
||
354 | sowers = sheet[4] |
||
355 | swarmers = min(sheet[1], math.ceil(sowers * 1.5)) |
||
356 | max_range = 0.8 * tool.subfleetMaxRange(self.client, self.db, {1:swarmers, 4:sowers}, fleet_id) |
||
357 | # four nearest systems are considered, with probability to be chosen based on order |
||
358 | nearest = tool.findNearest(self.db, fleet, self.data.otherSystems, max_range, 4) |
||
359 | if len(nearest): |
||
360 | # range is adjusted to flatten probabilities a bit |
||
361 | probability_map = map(lambda x: x ** 2, range(6, 2, -1)) |
||
362 | target = Utils.weightedRandom(nearest, probability_map) |
||
363 | |||
364 | fleet, new_fleet, my_fleets = tool.orderPartFleet(self.client, self.db, |
||
365 | {1:swarmers, 4:sowers}, True, |
||
366 | fleet_id, Const.FLACTION_MOVE, target, None) |
||
367 | |||
368 | def economy_manager(self): |
||
369 | self._expansion_manager() |
||
370 | self._system_manager() |
||
371 | |||
372 | def offense_manager(self): |
||
373 | self._ship_design_manager() |
||
374 | self._logistics_manager() |
||
375 | self._attack_manager() |
||
376 | |||
377 | def run(self): |
||
378 | self.economy_manager() |
||
379 | self.offense_manager() |
||
380 | |||
381 | def run(aclient): |
||
382 | ai = Mutant(aclient) |
||
383 | ai.run() |
||
384 | aclient.saveDB() |
||
385 | |||
386 |