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