{"action":"created","comment":{"url":"https://api.github.com/repos/freeorion/freeorion/pulls/comments/29383011","id":29383011,"diff_hunk":"@@ -0,0 +1,1573 @@\n+\"\"\"\n+This module deals with the autonomous shipdesign from the AI. The design process is class-based.\n+The basic functionality is defined in the class AIShipDesign. The more specialised classes mostly \n+implement the rating function and some additional information for the optimizing algorithms to improve performance. \n+\n+Example usage of this module:\n+import ShipDesignAI\n+myDesign = ShipDesignAI.AIShipDesign_Military()\n+myDesign.additional_specifications.enemy_mine_dmg = 10\n+best_military_designs = myDesign.optimize_design()  # best designs per planet: (rating,planetID,design_id,cost) tuples\n+\n+\n+Available ship classes:\n+- AIShipDesign_Military: basic military ship\n+- AIShipDesign_Trooper_Orbital: Troop ships for invasion in the same system\n+- AIShipDesign_Trooper_Ship: Troop ships for invasion of other systems\n+- AIShipDesign_Colonisation_Orbital: Ships for colonization in the same system\n+- AIShipDesign_Colonisation_Ship: Ships for colonization of other systems\n+- AIShipDesign_Outposter_Orbital: Ships for outposting in the same system\n+- AIShipDesign_Outposter_Ship: Troop ships for outposting in other systems\n+- AIShipDesign_OrbitalDefense: Ships for stationary defense\n+\n+Internal use only:\n+Classes:\n+- ShipDesignCache: caches information used in this module. Only use the defined instance (variable name \"Cache\")\n+- AIShipDesign: base class for all designs. Provides basic and general functionalities\n+- AIShipDesign_Colonisation: base class for all (specialised) colonisation ships which provides common functionalities\n+- AIShipDesign_Outposter: same but for outposter ships\n+- AIShipDesign_Trooper: same but for troop ships\n+- AdditionalSpecifications:  Defines all requirements we have for our designs such as minimum fuel or minimum starlane speed.\n+\n+global variables:\n+- Cache: Instance of the ShipDesignCache class - all cached information is stored in here.\n+\"\"\"\n+\n+# ToDo: Implement a nicer naming system (based on rating? Hull?)\n+# ToDo: add hull.detection to interface, then add scout class\n+\n+import freeOrionAIInterface as fo\n+import FreeOrionAI as foAI\n+import ColonisationAI\n+import copy\n+import traceback\n+import math\n+from collections import Counter\n+from collections import defaultdict\n+\n+# Define meta classes for the ship parts\n+ARMOUR = frozenset({fo.shipPartClass.armour})\n+SHIELDS = frozenset({fo.shipPartClass.shields})\n+DETECTION = frozenset({fo.shipPartClass.detection})\n+STEALTH = frozenset({fo.shipPartClass.stealth})\n+FUEL = frozenset({fo.shipPartClass.fuel})\n+COLONISATION = frozenset({fo.shipPartClass.colony})\n+ENGINES = frozenset({fo.shipPartClass.starlaneSpeed})  # TODO: Update to \"speed\" after new test build is supplied\n+TROOPS = frozenset({fo.shipPartClass.troops})\n+WEAPONS = frozenset({fo.shipPartClass.shortRange, fo.shipPartClass.missiles,\n+                     fo.shipPartClass.fighters, fo.shipPartClass.pointDefense})\n+ALL_META_CLASSES = frozenset({WEAPONS, ARMOUR, DETECTION, FUEL, STEALTH, SHIELDS, COLONISATION, ENGINES, TROOPS})\n+\n+# Prefixes for the test ship designs\n+TESTDESIGN_NAME_BASE = \"AI_TESTDESIGN\"\n+TESTDESIGN_NAME_HULL = TESTDESIGN_NAME_BASE+\"_HULL\"\n+TESTDESIGN_NAME_PART = TESTDESIGN_NAME_BASE+\"_PART\"\n+\n+# Hardcoded prefered hullname for testdesigns - should be a hull without conditions but with maximum different slottypes\n+TESTDESIGN_PREFERED_HULL = \"SH_BASIC_MEDIUM\"\n+\n+\n+class ShipDesignCache(object):\n+    \"\"\"This class handles the caching of information used to assess and build shipdesigns in this module.\n+\n+    Important methods:\n+    update_for_new_turn(self): Updates the cache for the current turn, to be called once at the beginning of each turn.\n+\n+    Important members:\n+    testhulls:                 # set of all hullnames used for testdesigns\n+    design_id_by_name          # {\"designname\": designid}\n+    part_by_partname           # {\"partname\": part object}\n+    map_reference_design_name  # {\"reference_designname\": \"ingame_designname\"}, cf. _build_reference_name()\n+    strictly_worse_parts       # strictly worse parts: {\"part\": [\"worsePart1\",\"worsePart2\"]}\n+    hulls_for_planets          # buildable hulls per planet {planetID: [\"buildableHull1\",\"buildableHull2\",...]}\n+    parts_for_planets          # buildable parts per planet and slot: {planetID: {slottype1: [\"part1\",\"part2\"]}}\n+    best_designs               # {shipclass:{reqTup:{species:{available_parts:{hull:(rating,best_parts)}}}}}\n+    production_cost            # {planetID: {\"partname1\": local_production_cost, \"hullname1\": local_production_cost}}\n+    production_time            # {planetID: {\"partname1\": local_production_time, \"hullname1\": local_production_time}}\n+\n+    Debug methods:\n+    print_CACHENAME(self), e.g. print_testhulls: prints content of the cache in some nicer format\n+    print_all(self): calls all the printing functions\n+    \"\"\"\n+\n+    def __init__(self):\n+        \"\"\"Cache is empty on creation\"\"\"\n+        self.testhulls = set()\n+        self.design_id_by_name = {}\n+        self.part_by_partname = {}\n+        self.map_reference_design_name = {}\n+        self.strictly_worse_parts = {}\n+        self.hulls_for_planets = {}\n+        self.parts_for_planets = {}\n+        self.best_designs = {}\n+        self.production_cost = {}\n+        self.production_time = {}\n+\n+    def update_for_new_turn(self):\n+        \"\"\" Update the cache for the current turn.\n+\n+        Make sure this function is called once at the beginning of the turn,\n+        i.e. before any other function of this module is used.\n+        \"\"\"\n+        print\n+        print \"-----   Updating ShipDesign cache for new turn   -----\"\n+        if not self.map_reference_design_name:\n+            self._build_cache_after_load()\n+        self._check_cache_for_consistency()\n+        self._update_cost_cache()\n+        self._update_buildable_items_this_turn(verbose=False)\n+\n+    def print_testhulls(self):\n+        \"\"\"Print the testhulls cache.\"\"\"\n+        print \"Testhull cache:\", self.testhulls\n+\n+    def print_design_id_by_name(self):\n+        \"\"\"Print the design_id_by_name cache.\"\"\"\n+        print \"DesignID cache:\", self.design_id_by_name\n+\n+    def print_part_by_partname(self):\n+        \"\"\"Print the part_by_partname cache.\"\"\"\n+        print \"Parts cached by name:\", self.part_by_partname\n+\n+    def print_strictly_worse_parts(self):\n+        \"\"\"Print the strictly_worse_parts cache.\"\"\"\n+        print \"List of strictly worse parts (ignoring slots):\"\n+        for part in self.strictly_worse_parts:\n+            print \"  %s:\" % part, self.strictly_worse_parts[part]\n+\n+    def print_map_reference_design_name(self):\n+        \"\"\"Print the ingame, reference name map of shipdesigns.\"\"\"\n+        print \"Design name map:\", self.map_reference_design_name\n+\n+    def print_hulls_for_planets(self, pid=None):\n+        \"\"\"Print the hulls buildable on each planet.\n+\n+        :param pid: None, int or list of ints\n+        \"\"\"\n+        if pid is None:\n+            planets = [pid for pid in self.hulls_for_planets]\n+        elif isinstance(pid, int):\n+            planets = [pid]\n+        elif isinstance(pid, list):\n+            planets = pid\n+        else:\n+            print \"ERROR: Invalid parameter 'pid' for 'print_hulls_for_planets'. Expected int, list or None.\"\n+            return\n+        print \"Hull-cache:\"\n+        get_planet = fo.getUniverse().getPlanet\n+        for pid in planets:\n+            print \"%s:\" % get_planet(pid).name, self.hulls_for_planets[pid]\n+\n+    def print_parts_for_planets(self, pid=None):\n+        \"\"\"Print the parts buildable on each planet.\n+\n+        :param pid: int or list of ints\n+        \"\"\"\n+        if pid is None:\n+            planets = [pid for pid in self.parts_for_planets]\n+        elif isinstance(pid, int):\n+            planets = [pid]\n+        elif isinstance(pid, list):\n+            planets = pid\n+        else:\n+            print \"FAILURE: Invalid parameter 'pid' for 'print_parts_for_planets'. Expected int, list or None.\"\n+            return\n+        print \"Available parts per planet:\"\n+        get_planet = fo.getUniverse().getPlanet\n+        for pid in planets:\n+            print \"  %s:\" % get_planet(pid).name,\n+            for slot in self.parts_for_planets[pid]:\n+                print slot, \":\", self.parts_for_planets[pid][slot]\n+\n+    def print_best_designs(self):\n+        \"\"\"Print the best designs that were previously found.\"\"\"\n+        print \"Currently cached best designs:\"\n+        for classname in self.best_designs:\n+            print classname\n+            for req_tuple in self.best_designs[classname]:\n+                print \"    \", req_tuple\n+                for species_tuple in self.best_designs[classname][req_tuple]:\n+                    print \"        \", species_tuple, \" # relevant species stats\"\n+                    for avParts in self.best_designs[classname][req_tuple][species_tuple]:\n+                        print \"            \", avParts\n+                        for hullname in sorted(self.best_designs[classname][req_tuple][species_tuple][avParts].keys(),\n+                                               reverse=True, key=lambda x: self.best_designs[classname][req_tuple]\n+                                                                           [species_tuple][avParts][x][0]):\n+                            print \"                \", hullname, \":\",\n+                            print self.best_designs[classname][req_tuple][species_tuple][avParts][hullname]\n+\n+    def print_production_cost(self):\n+        \"\"\"Print production_cost cache.\"\"\"\n+        universe = fo.getUniverse()\n+        print \"Cached production cost per planet:\"\n+        for pid in self.production_cost:\n+            print \"  %s:\" % universe.getPlanet(pid).name, self.production_cost[pid]\n+\n+    def print_production_time(self):\n+        \"\"\"Print production_time cache.\"\"\"\n+        universe = fo.getUniverse()\n+        print \"Cached production cost per planet:\"\n+        for pid in self.production_time:\n+            print \"  %s:\" % universe.getPlanet(pid).name, self.production_time[pid]\n+\n+    def print_all(self):\n+        \"\"\"Print the entire ship design cache.\"\"\"\n+        print\n+        print \"Printing the ShipDesignAI cache...\"\n+        self.print_testhulls()\n+        self.print_design_id_by_name()\n+        self.print_part_by_partname()\n+        self.print_strictly_worse_parts()\n+        self.print_map_reference_design_name()\n+        self.print_hulls_for_planets()\n+        self.print_parts_for_planets()\n+        self.print_best_designs()\n+        self.print_production_cost()\n+        self.print_production_time()\n+        print \"-----\"\n+        print\n+\n+    def _update_cost_cache(self):\n+        \"\"\"Cache the production cost and time for each part and hull for each planet (with shipyard) for this turn.\"\"\"\n+        self.production_cost.clear()\n+        self.production_time.clear()\n+        empire = fo.getEmpire()\n+        empire_id = empire.empireID\n+        for pid in _get_planets_with_shipyard():\n+            self.production_time[pid] = {}\n+            self.production_cost[pid] = {}\n+            for partname in empire.availableShipParts:\n+                part = _get_part_type(partname)\n+                self.production_cost[pid][partname] = part.productionCost(empire_id, pid)\n+                self.production_time[pid][partname] = part.productionTime(empire_id, pid)\n+            for hullname in empire.availableShipHulls:\n+                hull = fo.getHullType(hullname)\n+                self.production_cost[pid][hullname] = hull.productionCost(empire_id, pid)\n+                self.production_time[pid][hullname] = hull.productionTime(empire_id, pid)\n+\n+    def _build_cache_after_load(self):\n+        \"\"\"Build cache after loading or starting a game.\n+\n+        This function is supposed to be called after a reload or at the first turn.\n+        It reads out all the existing ship designs and then updates the following cache:\n+        - map_reference_design_name\n+        - design_id_by_name\n+        \"\"\"\n+        if self.map_reference_design_name or self.design_id_by_name:\n+            print \"WARNING: In ShipDesignAI.py: Cache._build_cache_after_load() called but cache is not empty.\"\n+        for design_id in fo.getEmpire().allShipDesigns:\n+            design = fo.getShipDesign(design_id)\n+            reference_name = _build_reference_name(design.hull, design.parts)\n+            self.map_reference_design_name[reference_name] = design.name(False)\n+            self.design_id_by_name[design.name(False)] = design_id\n+\n+    def _check_cache_for_consistency(self):\n+        \"\"\"Check if the persistent cache is consistent with the gamestate and fix it if not.\n+\n+        This function should be called once at the beginning of the turn (before update_shipdesign_cache()).\n+        Especially (only?) in multiplayer games, the shipDesignIDs may sometimes change across turns.\n+        \"\"\"\n+        print \"Checking persistent cache for consistency...\"\n+        try:\n+            for partname in self.part_by_partname:\n+                cached_name = self.part_by_partname[partname].name\n+                if cached_name != partname:\n+                    self.part_by_partname[partname] = fo.getPartType(partname)\n+                    print \"WARNING: Part cache corrupted.\"\n+                    print \"Expected: %s, got: %s. Cache was repaired.\" % (partname, cached_name)\n+        except Exception:\n+            self.part_by_partname.clear()\n+            traceback.print_exc()\n+\n+        corrupted = []\n+        for designname in self.design_id_by_name:\n+            try:\n+                cached_name = fo.getShipDesign(self.design_id_by_name[designname]).name(False)\n+                if cached_name != designname:\n+                    print \"WARNING: ShipID cache corrupted.\"\n+                    print \"Expected: %s, got: %s. Repairing cache.\" % (designname, cached_name)\n+                    design_id = next(iter([shipDesignID for shipDesignID in fo.getEmpire().allShipDesigns\n+                                          if designname == fo.getShipDesign(shipDesignID).name(False)]), None)\n+                    if design_id is not None:\n+                        self.design_id_by_name[designname] = design_id\n+                    else:\n+                        corrupted.append(designname)\n+            except AttributeError:\n+                print \"WARNING: ShipID cache corrupted. Could not get cached shipdesign. Repairing Cache.\"\n+                print traceback.format_exc()  # do not print to stderr as this is an \"expected\" exception.\n+                design_id = next(iter([shipDesignID for shipDesignID in fo.getEmpire().allShipDesigns\n+                                      if designname == fo.getShipDesign(shipDesignID).name(False)]), None)\n+                if design_id is not None:\n+                    self.design_id_by_name[designname] = design_id\n+                else:\n+                    corrupted.append(designname)\n+        for corrupted_entry in corrupted:\n+            del self.design_id_by_name[corrupted_entry]\n+\n+    def _update_buildable_items_this_turn(self, verbose=False):\n+        \"\"\"Calculate which parts and hulls can be built on each planet this turn.\n+\n+        :param verbose: bool - toggles detailed debugging output.\n+        \"\"\"\n+        # The AI currently has no way of checking building requirements of individual parts and hulls directly.\n+        # It can only check if we can build a design. Therefore, we use specific testdesigns to check if we can\n+        # build a hull or part.\n+        # The building requirements are constant so calculate this only once at the beginning of each turn.\n+        #\n+        # Code structure:\n+        #   1. Update hull test designs\n+        #   2. Get a list of buildable ship hulls for each planet\n+        #   3. Update ship part test designs\n+        #   4. Cache the list of buildable ship parts for each planet\n+        #\n+        self.hulls_for_planets.clear()\n+        self.parts_for_planets.clear()\n+        get_shipdesign = fo.getShipDesign\n+        get_hulltype = fo.getHullType\n+        empire = fo.getEmpire()\n+        empire_id = empire.empireID\n+        universe = fo.getUniverse()\n+        planets_with_shipyards = _get_planets_with_shipyard()\n+        available_hulls = list(empire.availableShipHulls)   # copy so we can sort it locally\n+        # Later on in the code, we need to find suitable testhulls, i.e. buildable hulls for all slottypes.\n+        # To reduce the number of lookups, move the hardcoded TESTDESIGN_PREFERED_HULL to the front of the list.\n+        # This hull should be buiildable on each planet and also cover the most common slottypes.\n+        try:\n+            idx = available_hulls.index(TESTDESIGN_PREFERED_HULL)\n+            available_hulls[0], available_hulls[idx] = available_hulls[idx], available_hulls[0]\n+        except ValueError:\n+            print \"WARNING: Tried to use '%s' as testhull but it is not in available_hulls.\" % TESTDESIGN_PREFERED_HULL,\n+            print \"Please update ShipDesignAI.py according to the new content.\"\n+            traceback.print_exc()\n+        testdesign_names = [get_shipdesign(design_id).name(False) for design_id in empire.allShipDesigns\n+                            if get_shipdesign(design_id).name(False).startswith(TESTDESIGN_NAME_BASE)]\n+        testdesign_names_hull = [name for name in testdesign_names if name.startswith(TESTDESIGN_NAME_HULL)]\n+        testdesign_names_part = [name for name in testdesign_names if name.startswith(TESTDESIGN_NAME_PART)]\n+        available_slot_types = {slottype for slotlist in [get_hulltype(hull).slots for hull in available_hulls]\n+                                for slottype in slotlist}\n+        new_parts = [_get_part_type(part) for part in empire.availableShipParts\n+                     if part not in self.strictly_worse_parts]\n+        pid = self.production_cost.keys()[0]  # TODO: Check if location invariant. If not, implement different handling\n+        for new_part in new_parts:\n+            self.strictly_worse_parts[new_part.name] = []\n+            for part_class in ALL_META_CLASSES:\n+                if new_part.partClass in part_class:\n+                    for old_part in [_get_part_type(part) for part in self.strictly_worse_parts\n+                                     if part != new_part.name]:\n+                        if old_part.partClass in part_class:\n+                            if new_part.capacity >= old_part.capacity:\n+                                a = new_part\n+                                b = old_part\n+                            else:\n+                                a = old_part\n+                                b = new_part\n+                            if (self.production_cost[pid][a.name] <= self.production_cost[pid][b.name]\n+                                    and {x for x in a.mountableSlotTypes} >= {x for x in b.mountableSlotTypes}):\n+                                # ToDo: add production_time as additional condition?\n+                                self.strictly_worse_parts[a.name].append(b.name)\n+                    break\n+        available_parts = sorted(self.strictly_worse_parts.keys(),\n+                                 key=lambda item: _get_part_type(item).capacity, reverse=True)\n+\n+        # in case of a load, we need to rebuild our Cache.\n+        if not self.testhulls:\n+            print \"Testhull cache not found. This may happen only at first turn after game start or load.\"\n+            for hullname in available_hulls:\n+                des = [des for des in testdesign_names_part if des.endswith(hullname)]\n+                if des:\n+                    self.testhulls.add(hullname)\n+            if verbose:\n+                print \"Rebuilt Cache. The following hulls are used in testdesigns for parts: \", self.testhulls\n+\n+        # 1. Update hull test designs\n+        print \"Updating Testdesigns for hulls...\"\n+        if verbose:\n+            print \"Available Hulls: \", available_hulls\n+            print \"Existing Designs (prefix: %s): \" % TESTDESIGN_NAME_HULL,\n+            print [x.replace(TESTDESIGN_NAME_HULL, \"\") for x in testdesign_names_hull]\n+        for hull in [get_hulltype(hullname) for hullname in available_hulls\n+                     if \"%s_%s\" % (TESTDESIGN_NAME_HULL, hullname) not in testdesign_names_hull]:\n+            partlist = len(hull.slots) * [\"\"]\n+            testdesign_name = \"%s_%s\" % (TESTDESIGN_NAME_HULL, hull.name)\n+            res = fo.issueCreateShipDesignOrder(testdesign_name, \"TESTPURPOSE ONLY\", hull.name,\n+                                                partlist, \"\", \"fighter\", False)\n+            if res:\n+                print \"Success: Added Test Design %s, with result %d\" % (testdesign_name, res)\n+            else:\n+                print \"Error: When adding test design %s - got result %d but expected 1\" % (testdesign_name, res)\n+                continue\n+\n+        # 2. Cache the list of buildable ship hulls for each planet\n+        print \"Caching buildable hulls per planet...\"\n+        testname = \"%s_%s\" % (TESTDESIGN_NAME_HULL, \"%s\")\n+        for pid in planets_with_shipyards:\n+            self.hulls_for_planets[pid] = []\n+        for hullname in available_hulls:\n+            testdesign = _get_design_by_name(testname % hullname)\n+            try:\n+                for pid in planets_with_shipyards:\n+                    if _can_build(testdesign, empire_id, pid):\n+                        self.hulls_for_planets[pid].append(hullname)\n+            except AttributeError:\n+                print \"Missing testdesign for hull %s!\" % hullname\n+                continue\n+\n+        # 3. Update ship part test designs\n+        #     Because there are different slottypes, we need to find a hull that can host said slot.\n+        #     However, not every planet can buld every hull. Thus, for each planet with a shipyard:\n+        #       I. Check which parts do not have a testdesign yet with a hull we can build on this planet\n+        #       II. If there are parts, find out which slots we need\n+        #       III. For each slot type, try to find a hull we can build on this planet\n+        #            and use this hull for all the parts hostable in this type.\n+        print \"Updating test designs for ship parts...\"\n+        if verbose:\n+            print \"Available parts: \", available_parts\n+            print \"Existing Designs (prefix: %s): \" % TESTDESIGN_NAME_PART,\n+            print [x.replace(TESTDESIGN_NAME_PART, \"\") for x in testdesign_names_part]\n+        for pid in planets_with_shipyards:\n+            planetname = universe.getPlanet(pid).name\n+            local_hulls = self.hulls_for_planets[pid]\n+            needs_update = [_get_part_type(partname) for partname in available_parts\n+                            if not any([\"%s_%s_%s\" % (TESTDESIGN_NAME_PART, partname, hullname) in testdesign_names_part\n+                                       for hullname in local_hulls])]\n+            if not needs_update:\n+                if verbose:\n+                    print \"Planet %s: Test designs are up to date\" % planetname\n+                continue\n+            if verbose:\n+                print \"Planet %s: The following parts appear to need a new design: \" % planetname,\n+                print [part.name for part in needs_update]\n+            for slot in available_slot_types:\n+                testhull = next((hullname for hullname in local_hulls if slot in get_hulltype(hullname).slots), None)\n+                if testhull is None:\n+                    if verbose:\n+                        print \"Failure: Could not find a hull with slots of type '%s' for this planet\" % slot.name\n+                    continue\n+                else:\n+                    if verbose:\n+                        print \"Using hull %s for slots of type '%s'\" % (testhull, slot.name)\n+                    self.testhulls.add(testhull)\n+                slotlist = [s for s in get_hulltype(testhull).slots]\n+                slot_index = slotlist.index(slot)\n+                num_slots = len(slotlist)\n+                for part in [part for part in needs_update if slot in part.mountableSlotTypes]:\n+                    partlist = num_slots * [\"\"]\n+                    partlist[slot_index] = part.name\n+                    testdesign_name = \"%s_%s_%s\" % (TESTDESIGN_NAME_PART, part.name, testhull)\n+                    res = fo.issueCreateShipDesignOrder(testdesign_name, \"TESTPURPOSE ONLY\", testhull,\n+                                                        partlist, \"\", \"fighter\", False)\n+                    if res:\n+                        print \"Success: Added Test Design %s, with result %d\" % (testdesign_name, res)\n+                        testdesign_names_part.append(testdesign_name)\n+                    else:\n+                        print \"Failure: Unknown error when adding test design %s\" % testdesign_name,\n+                        print \"got result %d but expected 1\" % res\n+                        continue\n+                    needs_update.remove(part)  # We only need one design per part, not for every possible slot\n+\n+        #  later on in the code, we will have to check multiple times if the test hulls are in\n+        #  the list of buildable hulls for the planet. As the ordering is preserved, move the\n+        #  testhulls to the front of the availableHull list to save some time in the checks.\n+        for i, s in enumerate(self.testhulls):\n+            try:\n+                idx = available_hulls.index(s)\n+                if i != idx:\n+                    available_hulls[i], available_hulls[idx] = available_hulls[idx], available_hulls[i]\n+            except ValueError:\n+                print \"ERROR: hull in testhull cache not in available_hulls\",\n+                print \"eventhough it is supposed to be a proper subset.\"\n+                traceback.print_exc()\n+        number_of_testhulls = len(self.testhulls)\n+\n+        # 4. Cache the list of buildable ship parts for each planet\n+        print \"Caching buildable ship parts per planet...\"\n+        for pid in planets_with_shipyards:\n+            local_testhulls = [hull for hull in self.testhulls\n+                               if hull in self.hulls_for_planets[pid][:number_of_testhulls]]\n+            self.parts_for_planets[pid] = {}\n+            local_ignore = set()\n+            local_cache = self.parts_for_planets[pid]\n+            for slot in available_slot_types:\n+                local_cache[slot] = []\n+            for partname in available_parts:\n+                if partname in local_ignore:\n+                    continue\n+                ship_design = None\n+                for hullname in local_testhulls:\n+                    ship_design = _get_design_by_name(\"%s_%s_%s\" % (TESTDESIGN_NAME_PART, partname, hullname))\n+                    if ship_design:\n+                        if _can_build(ship_design, empire_id, pid):\n+                            for slot in _get_part_type(partname).mountableSlotTypes:\n+                                local_cache[slot].append(partname)\n+                                local_ignore.update(self.strictly_worse_parts[partname])\n+                        break\n+                if verbose and not ship_design:\n+                    planetname = universe.getPlanet(pid).name\n+                    print \"Failure: Couldn't find a testdesign for part %s on planet %s.\" % (partname, planetname)\n+            # make sure we do not edit the list later on this turn => tuple: immutable\n+            # This also allows to shallowcopy the cache.\n+            for slot in local_cache:\n+                local_cache[slot] = tuple(local_cache[slot])\n+\n+            if verbose:\n+                print \"%s: \" % universe.getPlanet(pid).name, self.parts_for_planets[pid]\n+\n+\n+Cache = ShipDesignCache()\n+\n+\n+class AdditionalSpecifications(object):\n+    \"\"\"This class is a container for all kind of additional information\n+    and requirements we may want to use when assessing ship designs.\n+\n+    methods for external use:\n+    convert_to_tuple(): Builds and returns a tuple of the class attributes\n+    update_enemy(enemy): updates enemy stats\n+    \"\"\"\n+\n+    def __init__(self):\n+        # TODO: Extend this framework according to needs of future implementations\n+        self.minimum_fuel = 0\n+        self.minimum_speed = 0\n+        self.minimum_structure = 1\n+        self.enemy_shields = 0\n+        self.enemy_weapon_strength = 0\n+        self.enemy_mine_dmg = 0  # TODO: Implement the detection of enemy mine damage\n+        self.update_enemy(foAI.foAIstate.empire_standard_enemy)\n+\n+    def update_enemy(self, enemy):\n+        \"\"\"Read out the enemies stats and save them.\n+\n+        :param enemy: enemy as defined in AIstate\n+        \"\"\"\n+        self.enemy_shields = enemy[2]\n+        enemy_attack_stats = enemy[1]\n+        self.enemy_weapon_strength = 0\n+        for stat in enemy_attack_stats:\n+            if stat[0] > self.enemy_weapon_strength:\n+                self.enemy_weapon_strength = stat[0]\n+\n+    def convert_to_tuple(self):\n+        \"\"\"Create a tuple of this class' attributes (e.g. to use as key in dict).\n+\n+        :returns: tuple (minFuel,minSpeed,enemyDmg,enemyShield,enemyMineDmg)\n+        \"\"\"\n+        tup = (\"minFuel: %s\" % self.minimum_fuel, \"minSpeed: %s\" % self.minimum_speed,\n+               \"enemyDmg: %s\" % self.enemy_weapon_strength, \"enemyShields: %s\" % self.enemy_shields,\n+               \"enemyMineDmg: %s\" % self.enemy_mine_dmg)\n+        return tup\n+\n+\n+class AIShipDesign(object):\n+    \"\"\"This class and its subclasses implement the building of a ship design and its rating.\n+     Specialised Designs with their own rating system or optimizing algorithms should inherit from this class.\n+\n+    Member functions intended for external use:\n+    optimize_design(): Returns the estimated optimum design according to the rating function\n+    evaluate(): Returns a rating for the design as of the current state\n+    update_hull(hullname): sets the hull used in the design\n+    update_parts(partname_list): sets the parts used in the design\n+    update_species(speciesname): sets the piloting species\n+    update_stats(): calculates the stats of the design based on hull+parts+species\n+    add_design(): Adds the shipdesign in the C++ part of the game\n+\n+    Functions which are to be overridden in inherited classes:\n+    _rating_function()\n+    _class_specific_filter()\n+    _starting_guess()\n+\n+    For improved performance, maybe override _filling_algorithm() with a more specialised algorithm as well.\n+    \"\"\"\n+    basename = \"Default - Do not build\"     # base design name\n+    description = \"Base class Ship type\"    # design description\n+    useful_part_classes = ALL_META_CLASSES  # only these parts are considered in the design process\n+\n+    filter_useful_parts = True              # removes any part not belonging to self.useful_part_classes\n+    filter_inefficient_parts = False        # removes cost-inefficient parts (less capacity and less capacity/cost)\n+\n+    running_index = 1                       # running index used in the design name\n+    running_index_needs_update = True       # To check if we need to update the running_index after a load.\n+\n+    def __init__(self):\n+        \"\"\"Make sure to call this constructor in each subclass.\"\"\"\n+        self.species = None         # name of the piloting species (string)\n+        self.hull = None            # hull object (not hullname!)\n+        self.partnames = []         # list of partnames (string)\n+        self.parts = []             # list of actual part objects\n+        self.attacks = {}           # {damage:count}\n+        self.structure = 0\n+        self.shields = 0\n+        self.fuel = 0\n+        self.speed = 0\n+        self.stealth = 0\n+        self.detection = 0\n+        self.troops = 0\n+        self.colonisation = -1      # -1 since 0 indicates an outpost (capacity = 0)","path":"default/AI/ShipDesignAI.py","position":605,"original_position":605,"commit_id":"cae69d33b21a657f1b11271e6a8a87191def98b6","original_commit_id":"cae69d33b21a657f1b11271e6a8a87191def98b6","user":{"login":"Cjkjvfnby","id":171597,"avatar_url":"https://avatars.githubusercontent.com/u/171597?v=3","gravatar_id":"","url":"https://api.github.com/users/Cjkjvfnby","html_url":"https://github.com/Cjkjvfnby","followers_url":"https://api.github.com/users/Cjkjvfnby/followers","following_url":"https://api.github.com/users/Cjkjvfnby/following{/other_user}","gists_url":"https://api.github.com/users/Cjkjvfnby/gists{/gist_id}","starred_url":"https://api.github.com/users/Cjkjvfnby/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Cjkjvfnby/subscriptions","organizations_url":"https://api.github.com/users/Cjkjvfnby/orgs","repos_url":"https://api.github.com/users/Cjkjvfnby/repos","events_url":"https://api.github.com/users/Cjkjvfnby/events{/privacy}","received_events_url":"https://api.github.com/users/Cjkjvfnby/received_events","type":"User","site_admin":false},"body":"IMHO splitting it to `colonization` and `outpost` will be more easy to understand.","created_at":"2015-04-29T21:29:11Z","updated_at":"2015-04-29T21:29:11Z","html_url":"https://github.com/freeorion/freeorion/pull/49#discussion_r29383011","pull_request_url":"https://api.github.com/repos/freeorion/freeorion/pulls/49","_links":{"self":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/comments/29383011"},"html":{"href":"https://github.com/freeorion/freeorion/pull/49#discussion_r29383011"},"pull_request":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/49"}}},"pull_request":{"url":"https://api.github.com/repos/freeorion/freeorion/pulls/49","id":34303002,"html_url":"https://github.com/freeorion/freeorion/pull/49","diff_url":"https://github.com/freeorion/freeorion/pull/49.diff","patch_url":"https://github.com/freeorion/freeorion/pull/49.patch","issue_url":"https://api.github.com/repos/freeorion/freeorion/issues/49","number":49,"state":"open","locked":false,"title":"AI: autonomous ship design","user":{"login":"Morlic-fo","id":11706951,"avatar_url":"https://avatars.githubusercontent.com/u/11706951?v=3","gravatar_id":"","url":"https://api.github.com/users/Morlic-fo","html_url":"https://github.com/Morlic-fo","followers_url":"https://api.github.com/users/Morlic-fo/followers","following_url":"https://api.github.com/users/Morlic-fo/following{/other_user}","gists_url":"https://api.github.com/users/Morlic-fo/gists{/gist_id}","starred_url":"https://api.github.com/users/Morlic-fo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Morlic-fo/subscriptions","organizations_url":"https://api.github.com/users/Morlic-fo/orgs","repos_url":"https://api.github.com/users/Morlic-fo/repos","events_url":"https://api.github.com/users/Morlic-fo/events{/privacy}","received_events_url":"https://api.github.com/users/Morlic-fo/received_events","type":"User","site_admin":false},"body":"There is still some work to be done but the current version should be stable.\r\n\r\nAs the changes are already somewhat extensive and non-trivial, I guess it makes sense to discuss this basic version now and then polish it further from here.","created_at":"2015-04-28T19:36:56Z","updated_at":"2015-04-29T21:29:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2809cec73606a223ab9a76a96ffba7115a28b7f9","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/freeorion/freeorion/pulls/49/commits","review_comments_url":"https://api.github.com/repos/freeorion/freeorion/pulls/49/comments","review_comment_url":"https://api.github.com/repos/freeorion/freeorion/pulls/comments{/number}","comments_url":"https://api.github.com/repos/freeorion/freeorion/issues/49/comments","statuses_url":"https://api.github.com/repos/freeorion/freeorion/statuses/cae69d33b21a657f1b11271e6a8a87191def98b6","head":{"label":"Morlic-fo:AI-Shipdesign","ref":"AI-Shipdesign","sha":"cae69d33b21a657f1b11271e6a8a87191def98b6","user":{"login":"Morlic-fo","id":11706951,"avatar_url":"https://avatars.githubusercontent.com/u/11706951?v=3","gravatar_id":"","url":"https://api.github.com/users/Morlic-fo","html_url":"https://github.com/Morlic-fo","followers_url":"https://api.github.com/users/Morlic-fo/followers","following_url":"https://api.github.com/users/Morlic-fo/following{/other_user}","gists_url":"https://api.github.com/users/Morlic-fo/gists{/gist_id}","starred_url":"https://api.github.com/users/Morlic-fo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Morlic-fo/subscriptions","organizations_url":"https://api.github.com/users/Morlic-fo/orgs","repos_url":"https://api.github.com/users/Morlic-fo/repos","events_url":"https://api.github.com/users/Morlic-fo/events{/privacy}","received_events_url":"https://api.github.com/users/Morlic-fo/received_events","type":"User","site_admin":false},"repo":{"id":33083305,"name":"freeorion","full_name":"Morlic-fo/freeorion","owner":{"login":"Morlic-fo","id":11706951,"avatar_url":"https://avatars.githubusercontent.com/u/11706951?v=3","gravatar_id":"","url":"https://api.github.com/users/Morlic-fo","html_url":"https://github.com/Morlic-fo","followers_url":"https://api.github.com/users/Morlic-fo/followers","following_url":"https://api.github.com/users/Morlic-fo/following{/other_user}","gists_url":"https://api.github.com/users/Morlic-fo/gists{/gist_id}","starred_url":"https://api.github.com/users/Morlic-fo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Morlic-fo/subscriptions","organizations_url":"https://api.github.com/users/Morlic-fo/orgs","repos_url":"https://api.github.com/users/Morlic-fo/repos","events_url":"https://api.github.com/users/Morlic-fo/events{/privacy}","received_events_url":"https://api.github.com/users/Morlic-fo/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/Morlic-fo/freeorion","description":"Source code repository of the FreeOrion project.","fork":true,"url":"https://api.github.com/repos/Morlic-fo/freeorion","forks_url":"https://api.github.com/repos/Morlic-fo/freeorion/forks","keys_url":"https://api.github.com/repos/Morlic-fo/freeorion/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Morlic-fo/freeorion/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Morlic-fo/freeorion/teams","hooks_url":"https://api.github.com/repos/Morlic-fo/freeorion/hooks","issue_events_url":"https://api.github.com/repos/Morlic-fo/freeorion/issues/events{/number}","events_url":"https://api.github.com/repos/Morlic-fo/freeorion/events","assignees_url":"https://api.github.com/repos/Morlic-fo/freeorion/assignees{/user}","branches_url":"https://api.github.com/repos/Morlic-fo/freeorion/branches{/branch}","tags_url":"https://api.github.com/repos/Morlic-fo/freeorion/tags","blobs_url":"https://api.github.com/repos/Morlic-fo/freeorion/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Morlic-fo/freeorion/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Morlic-fo/freeorion/git/refs{/sha}","trees_url":"https://api.github.com/repos/Morlic-fo/freeorion/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Morlic-fo/freeorion/statuses/{sha}","languages_url":"https://api.github.com/repos/Morlic-fo/freeorion/languages","stargazers_url":"https://api.github.com/repos/Morlic-fo/freeorion/stargazers","contributors_url":"https://api.github.com/repos/Morlic-fo/freeorion/contributors","subscribers_url":"https://api.github.com/repos/Morlic-fo/freeorion/subscribers","subscription_url":"https://api.github.com/repos/Morlic-fo/freeorion/subscription","commits_url":"https://api.github.com/repos/Morlic-fo/freeorion/commits{/sha}","git_commits_url":"https://api.github.com/repos/Morlic-fo/freeorion/git/commits{/sha}","comments_url":"https://api.github.com/repos/Morlic-fo/freeorion/comments{/number}","issue_comment_url":"https://api.github.com/repos/Morlic-fo/freeorion/issues/comments{/number}","contents_url":"https://api.github.com/repos/Morlic-fo/freeorion/contents/{+path}","compare_url":"https://api.github.com/repos/Morlic-fo/freeorion/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Morlic-fo/freeorion/merges","archive_url":"https://api.github.com/repos/Morlic-fo/freeorion/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Morlic-fo/freeorion/downloads","issues_url":"https://api.github.com/repos/Morlic-fo/freeorion/issues{/number}","pulls_url":"https://api.github.com/repos/Morlic-fo/freeorion/pulls{/number}","milestones_url":"https://api.github.com/repos/Morlic-fo/freeorion/milestones{/number}","notifications_url":"https://api.github.com/repos/Morlic-fo/freeorion/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Morlic-fo/freeorion/labels{/name}","releases_url":"https://api.github.com/repos/Morlic-fo/freeorion/releases{/id}","created_at":"2015-03-29T18:09:38Z","updated_at":"2015-04-28T19:03:01Z","pushed_at":"2015-04-29T18:32:02Z","git_url":"git://github.com/Morlic-fo/freeorion.git","ssh_url":"git@github.com:Morlic-fo/freeorion.git","clone_url":"https://github.com/Morlic-fo/freeorion.git","svn_url":"https://github.com/Morlic-fo/freeorion","homepage":"http://www.freeorion.org/","size":477734,"stargazers_count":0,"watchers_count":0,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"freeorion:master","ref":"master","sha":"d5ec689a836ff8c1be92d947097d02d5ec1ae332","user":{"login":"freeorion","id":1025550,"avatar_url":"https://avatars.githubusercontent.com/u/1025550?v=3","gravatar_id":"","url":"https://api.github.com/users/freeorion","html_url":"https://github.com/freeorion","followers_url":"https://api.github.com/users/freeorion/followers","following_url":"https://api.github.com/users/freeorion/following{/other_user}","gists_url":"https://api.github.com/users/freeorion/gists{/gist_id}","starred_url":"https://api.github.com/users/freeorion/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freeorion/subscriptions","organizations_url":"https://api.github.com/users/freeorion/orgs","repos_url":"https://api.github.com/users/freeorion/repos","events_url":"https://api.github.com/users/freeorion/events{/privacy}","received_events_url":"https://api.github.com/users/freeorion/received_events","type":"Organization","site_admin":false},"repo":{"id":33066062,"name":"freeorion","full_name":"freeorion/freeorion","owner":{"login":"freeorion","id":1025550,"avatar_url":"https://avatars.githubusercontent.com/u/1025550?v=3","gravatar_id":"","url":"https://api.github.com/users/freeorion","html_url":"https://github.com/freeorion","followers_url":"https://api.github.com/users/freeorion/followers","following_url":"https://api.github.com/users/freeorion/following{/other_user}","gists_url":"https://api.github.com/users/freeorion/gists{/gist_id}","starred_url":"https://api.github.com/users/freeorion/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freeorion/subscriptions","organizations_url":"https://api.github.com/users/freeorion/orgs","repos_url":"https://api.github.com/users/freeorion/repos","events_url":"https://api.github.com/users/freeorion/events{/privacy}","received_events_url":"https://api.github.com/users/freeorion/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/freeorion/freeorion","description":"Source code repository of the FreeOrion project.","fork":false,"url":"https://api.github.com/repos/freeorion/freeorion","forks_url":"https://api.github.com/repos/freeorion/freeorion/forks","keys_url":"https://api.github.com/repos/freeorion/freeorion/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freeorion/freeorion/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freeorion/freeorion/teams","hooks_url":"https://api.github.com/repos/freeorion/freeorion/hooks","issue_events_url":"https://api.github.com/repos/freeorion/freeorion/issues/events{/number}","events_url":"https://api.github.com/repos/freeorion/freeorion/events","assignees_url":"https://api.github.com/repos/freeorion/freeorion/assignees{/user}","branches_url":"https://api.github.com/repos/freeorion/freeorion/branches{/branch}","tags_url":"https://api.github.com/repos/freeorion/freeorion/tags","blobs_url":"https://api.github.com/repos/freeorion/freeorion/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freeorion/freeorion/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freeorion/freeorion/git/refs{/sha}","trees_url":"https://api.github.com/repos/freeorion/freeorion/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freeorion/freeorion/statuses/{sha}","languages_url":"https://api.github.com/repos/freeorion/freeorion/languages","stargazers_url":"https://api.github.com/repos/freeorion/freeorion/stargazers","contributors_url":"https://api.github.com/repos/freeorion/freeorion/contributors","subscribers_url":"https://api.github.com/repos/freeorion/freeorion/subscribers","subscription_url":"https://api.github.com/repos/freeorion/freeorion/subscription","commits_url":"https://api.github.com/repos/freeorion/freeorion/commits{/sha}","git_commits_url":"https://api.github.com/repos/freeorion/freeorion/git/commits{/sha}","comments_url":"https://api.github.com/repos/freeorion/freeorion/comments{/number}","issue_comment_url":"https://api.github.com/repos/freeorion/freeorion/issues/comments{/number}","contents_url":"https://api.github.com/repos/freeorion/freeorion/contents/{+path}","compare_url":"https://api.github.com/repos/freeorion/freeorion/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freeorion/freeorion/merges","archive_url":"https://api.github.com/repos/freeorion/freeorion/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freeorion/freeorion/downloads","issues_url":"https://api.github.com/repos/freeorion/freeorion/issues{/number}","pulls_url":"https://api.github.com/repos/freeorion/freeorion/pulls{/number}","milestones_url":"https://api.github.com/repos/freeorion/freeorion/milestones{/number}","notifications_url":"https://api.github.com/repos/freeorion/freeorion/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freeorion/freeorion/labels{/name}","releases_url":"https://api.github.com/repos/freeorion/freeorion/releases{/id}","created_at":"2015-03-29T08:22:20Z","updated_at":"2015-04-29T20:56:39Z","pushed_at":"2015-04-29T20:56:38Z","git_url":"git://github.com/freeorion/freeorion.git","ssh_url":"git@github.com:freeorion/freeorion.git","clone_url":"https://github.com/freeorion/freeorion.git","svn_url":"https://github.com/freeorion/freeorion","homepage":"http://www.freeorion.org/","size":487096,"stargazers_count":28,"watchers_count":28,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":13,"mirror_url":null,"open_issues_count":5,"forks":13,"open_issues":5,"watchers":28,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/49"},"html":{"href":"https://github.com/freeorion/freeorion/pull/49"},"issue":{"href":"https://api.github.com/repos/freeorion/freeorion/issues/49"},"comments":{"href":"https://api.github.com/repos/freeorion/freeorion/issues/49/comments"},"review_comments":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/49/comments"},"review_comment":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/freeorion/freeorion/pulls/49/commits"},"statuses":{"href":"https://api.github.com/repos/freeorion/freeorion/statuses/cae69d33b21a657f1b11271e6a8a87191def98b6"}}}}