Package horizons :: Package ai :: Package aiplayer :: Package building
[hide private]
[frames] | no frames]

Source Code for Package horizons.ai.aiplayer.building

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons 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  # This program 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 this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import logging 
 23  import math 
 24  import operator 
 25  from typing import Dict 
 26   
 27  from horizons.ai.aiplayer.constants import BUILD_RESULT 
 28  from horizons.constants import GAME_SPEED, RES 
 29  from horizons.entities import Entities 
 30  from horizons.world.production.producer import Producer 
 31  from horizons.world.production.productionline import ProductionLine 
32 33 34 -class AbstractBuilding:
35 """ 36 An object of this class tells the AI how to build a specific type of building. 37 38 Instances of the subclasses are used by production chains to discover the set of 39 buildings necessary to produce the right amount of resources. 40 """ 41 42 log = logging.getLogger("ai.aiplayer.building") 43
44 - def __init__(self, building_id, name, settler_level):
45 super().__init__() # TODO: check if this call is needed 46 self.id = building_id 47 self.name = name 48 self.settler_level = settler_level 49 self.width = Entities.buildings[building_id].size[0] 50 self.height = Entities.buildings[building_id].size[1] 51 self.size = (self.width, self.height) 52 self.radius = Entities.buildings[building_id].radius 53 self.terrain_type = Entities.buildings[building_id].terrain_type 54 self.lines = {} # output_resource_id: ProductionLine 55 if self.producer_building: 56 self.__init_production_lines()
57 58 __loaded = False 59 buildings = {} # type: Dict[int, AbstractBuilding] 60 _available_buildings = {} # type: Dict[int, AbstractBuilding] 61
62 - def __init_production_lines(self):
63 production_lines = self._get_producer_building().get_component_template(Producer)['productionlines'] 64 for key, value in production_lines.items(): 65 production_line = ProductionLine(key, value) 66 assert len(production_line.produced_res) == 1 67 self.lines[list(production_line.produced_res.keys())[0]] = production_line
68
69 - def _get_producer_building(self):
70 return Entities.buildings[self.id]
71 72 @classmethod
73 - def load_all(cls, db):
74 """Fill the cls.buildings dict so the registered buildings can be used.""" 75 if cls.__loaded: 76 return 77 78 for building_id, class_ref in cls._available_buildings.items(): 79 cls.buildings[building_id] = class_ref.load(db, building_id) 80 cls.__loaded = True
81 82 @classmethod
83 - def _load_name(cls, db, building_id):
84 return Entities.buildings[building_id].name
85 86 @classmethod
87 - def _load_settler_level(cls, building_id):
88 return Entities.buildings[building_id].settler_level
89 90 @classmethod
91 - def load(cls, db, building_id):
92 name = cls._load_name(db, building_id) 93 settler_level = cls._load_settler_level(building_id) 94 return cls(building_id, name, settler_level)
95 96 monthly_gold_cost = 50 97 resource_cost = {RES.GOLD: 1, RES.BOARDS: 20, RES.BRICKS: 45, RES.TOOLS: 50} 98
100 """Return a value representing the utility cost of building the building.""" 101 total = 0 102 for resource_id, amount in Entities.buildings[self.id].costs.items(): 103 total += self.resource_cost[resource_id] * amount 104 total += self.monthly_gold_cost * Entities.buildings[self.id].running_costs 105 return total
106
107 - def get_expected_cost(self, resource_id, production_needed, settlement_manager):
108 """Return a value representing the utility cost of building enough buildings to produced the given amount of resource per tick.""" 109 buildings_needed = math.ceil(max(0.0, production_needed / self.get_expected_production_level(resource_id))) 110 return buildings_needed * self.get_expected_building_cost()
111
112 - def get_expected_production_level(self, resource_id):
113 """Return the expected production capacity of a single building of this type producing the given resource.""" 114 if resource_id not in self.lines: 115 return None 116 line = self.lines[resource_id] 117 return line.produced_res[resource_id] / float(line.time) / GAME_SPEED.TICKS_PER_SECOND
118
119 - def get_production_level(self, building, resource_id):
120 """Return the actual production capacity of a single building of this type producing the given resource.""" 121 # most buildings can get away with reporting the expected production level 122 return self.get_expected_production_level(resource_id)
123
124 - def have_resources(self, settlement_manager):
125 """Return a boolean showing whether the given settlement has enough resources to build a building of this type.""" 126 return Entities.buildings[self.id].have_resources([settlement_manager.land_manager.settlement], settlement_manager.owner)
127 128 @classmethod
129 - def _get_buildability_intersection(cls, settlement_manager, size, terrain_type, need_collector_connection):
130 # Note that this is explicitly using the production_builder. This means that this 131 # code can never be used to construct anything outside the production area. 132 caches = (settlement_manager.production_builder.buildability_cache, settlement_manager.settlement.buildability_cache) 133 if need_collector_connection: 134 caches += (settlement_manager.production_builder.simple_collector_area_cache, ) 135 return settlement_manager.island.terrain_cache.get_buildability_intersection(terrain_type, size, *caches)
136
137 - def iter_potential_locations(self, settlement_manager):
138 """Iterate over possible locations of the building in the given settlement in the form of (x, y, orientation).""" 139 need_collector_connection = self.evaluator_class.need_collector_connection 140 for (x, y) in sorted(self._get_buildability_intersection(settlement_manager, self.size, self.terrain_type, need_collector_connection)): 141 yield (x, y, 0) 142 if self.width != self.height: 143 for (x, y) in sorted(self._get_buildability_intersection(settlement_manager, (self.height, self.width), self.terrain_type, need_collector_connection)): 144 yield (x, y, 1)
145 146 @property
147 - def evaluator_class(self):
148 """Return the relevant BuildingEvaluator subclass.""" 149 raise NotImplementedError('This function has to be overridden.')
150 151 @property
152 - def directly_buildable(self):
153 """Return a boolean showing whether the build function of this subclass can be used to build a building of this type.""" 154 return True
155 156 @property
157 - def coverage_building(self):
158 """Return a boolean showing whether buildings of this type may need to be built even when the production capacity has been reached.""" 159 return False
160 161 @property
162 - def ignore_production(self):
163 """Return a boolean showing whether instances of this building can be used to calculate the production capacity.""" 164 return False
165 166 @property
167 - def producer_building(self):
168 """Return a boolean showing whether this building is supposed to have usable production lines.""" 169 return True
170
171 - def get_evaluators(self, settlement_manager, resource_id):
172 """Return a list of every BuildingEvaluator for this building type in the given settlement.""" 173 options = [] # [BuildingEvaluator, ...] 174 for x, y, orientation in self.iter_potential_locations(settlement_manager): 175 evaluator = self.evaluator_class.create(settlement_manager.production_builder, x, y, orientation) 176 if evaluator is not None: 177 options.append(evaluator) 178 return options
179
180 - def build(self, settlement_manager, resource_id):
181 """Try to build the best possible instance of this building in the given settlement. Returns (BUILD_RESULT constant, building instance).""" 182 if not self.have_resources(settlement_manager): 183 return (BUILD_RESULT.NEED_RESOURCES, None) 184 185 for evaluator in sorted(self.get_evaluators(settlement_manager, resource_id), key=operator.attrgetter('value'), reverse=True): 186 result = evaluator.execute() 187 if result[0] != BUILD_RESULT.IMPOSSIBLE: 188 return result 189 self.log.debug('%s.build(%s, %d): no possible evaluators', self.__class__.__name__, settlement_manager, resource_id if resource_id else -1) 190 return (BUILD_RESULT.IMPOSSIBLE, None)
191
192 - def need_to_build_more_buildings(self, settlement_manager, resource_id):
193 """Return a boolean showing whether another instance of the building should be built right now regardless of the production capacity.""" 194 return False
195