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

Source Code for Module horizons.ai.aiplayer.productionchain

  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   
 24  from horizons.ai.aiplayer.building import AbstractBuilding 
 25  from horizons.ai.aiplayer.constants import BUILD_RESULT 
 26  from horizons.constants import RES 
27 28 29 -class ProductionChain:
30 """ 31 A production chain handles the building of buildings required to produce a resource. 32 33 Production chains use the list of production lines and the available AbstractBuilding 34 subclasses to figure out all ways of producing a certain resource and finding the 35 right ratio of them to produce just enough of the resource. The result is a tree 36 that can be used to produce the required resource. 37 38 Each subtree reserves a portion of the total capacity of the buildings of the relevant 39 type. This is a logical classification and doesn't affect the actual buildings in any way. 40 Some subtrees can import the required resource from other islands using a mechanism 41 similar to the previous one but in that case it acts as if the resource was produced 42 without any subtrees. The imported amounts are added up over time and saved as an owed 43 resource in the exporting settlement's resource manager (these restrictions are again 44 just logical without affecting the way the settlements work in any way). That storage 45 is realized by organizing DomesticTrade missions that transfer the resources to the 46 right settlements. 47 """ 48 49 log = logging.getLogger("ai.aiplayer.productionchain") 50
51 - def __init__(self, settlement_manager, resource_id, resource_producer):
52 super().__init__() # TODO: check if this call needed 53 self.settlement_manager = settlement_manager 54 self.resource_id = resource_id 55 self.chain = self._get_chain(resource_id, resource_producer, 1.0) 56 self.chain.assign_identifier('/{:d},{:d}'.format( 57 self.settlement_manager.worldid, self.resource_id))
58
59 - def _get_chain(self, resource_id, resource_producer, production_ratio):
60 """Return a ProductionChainSubtreeChoice if it is possible to produce the resource, None otherwise.""" 61 options = [] 62 if resource_id in resource_producer: 63 for production_line, abstract_building in resource_producer[resource_id]: 64 possible = True 65 sources = [] 66 for consumed_resource, amount in production_line.consumed_res.items(): 67 next_production_ratio = abs(production_ratio * amount / production_line.produced_res[resource_id]) 68 subtree = self._get_chain(consumed_resource, resource_producer, next_production_ratio) 69 if not subtree: 70 possible = False 71 break 72 sources.append(subtree) 73 if possible: 74 options.append(ProductionChainSubtree(self.settlement_manager, resource_id, production_line, abstract_building, sources, production_ratio)) 75 if not options: 76 return None 77 return ProductionChainSubtreeChoice(options)
78 79 @classmethod
80 - def create(cls, settlement_manager, resource_id):
81 """Create a production chain that can produce the given resource.""" 82 resource_producer = {} 83 for abstract_building in AbstractBuilding.buildings.values(): 84 for resource, production_line in abstract_building.lines.items(): 85 if resource not in resource_producer: 86 resource_producer[resource] = [] 87 resource_producer[resource].append((production_line, abstract_building)) 88 return ProductionChain(settlement_manager, resource_id, resource_producer)
89
90 - def __str__(self):
91 return 'ProductionChain({:d}): {:.5f}\n{}'.format( 92 self.resource_id, self.get_final_production_level(), self.chain)
93
94 - def build(self, amount):
95 """Build a building that gets it closer to producing at least the given amount of resource per tick.""" 96 return self.chain.build(amount)
97
98 - def reserve(self, amount, may_import):
99 """Reserve currently available production capacity and import from other islands if allowed.""" 100 return self.chain.reserve(amount, may_import)
101
102 - def need_to_build_more_buildings(self, amount):
103 """Return a boolean showing whether more buildings need to be built in order to produce at least amount of resource per tick.""" 104 return self.chain.need_to_build_more_buildings(amount)
105
107 """Return the production level per tick at the bottleneck.""" 108 return self.chain.get_final_production_level()
109
110 - def get_ratio(self, resource_id):
111 """Return the ratio of the given resource needed given that 1 unit of the root resource is required.""" 112 return self.chain.get_ratio(resource_id)
113
114 115 -class ProductionChainSubtreeChoice:
116 """An object of this class represents a choice between N >= 1 ways of producing the required resource.""" 117 118 log = logging.getLogger("ai.aiplayer.productionchain") 119 coverage_resources = {RES.COMMUNITY, RES.FAITH, RES.EDUCATION, RES.GET_TOGETHER} 120
121 - def __init__(self, options):
122 super().__init__() # TODO: check if this call is needed 123 self.options = options # [ProductionChainSubtree, ...] 124 self.resource_id = options[0].resource_id # the required resource 125 self.production_ratio = options[0].production_ratio # given that 1 unit has to be produced at the root of the tree, how much has to be produced here? 126 self.ignore_production = options[0].ignore_production # whether to try to build more buildings even when the required production capacity has been reached 127 self.trade_manager = options[0].trade_manager # TradeManager instance 128 self.settlement_manager = options[0].settlement_manager # SettlementManager instance
129
130 - def assign_identifier(self, prefix):
131 """Recursively assign an identifier to this subtree to know which subtree owns which resource quota.""" 132 self.identifier = prefix + ('/choice' if len(self.options) > 1 else '') 133 for option in self.options: 134 option.assign_identifier(self.identifier)
135
136 - def __str__(self, level=0):
137 result = '{}Choice between {:d} options: {:.5f}\n'.format( 138 ' ' * level, len(self.options), self.get_final_production_level()) 139 for option in self.options: 140 result += option.__str__(level + 1) 141 if self.get_root_import_level() > 1e-9: 142 result += '\n{}Import {:.5f}'.format( 143 ' ' * (level + 1), self.get_root_import_level()) 144 return result
145
146 - def get_root_import_level(self):
147 """Return the amount of the resource imported per tick.""" 148 return self.trade_manager.get_quota(self.identifier, self.resource_id) / self.production_ratio
149
151 """Return the total reserved production capacity of the resource per tick (includes import).""" 152 return sum(option.get_final_production_level() for option in self.options) + self.get_root_import_level()
153
154 - def get_expected_cost(self, amount):
155 """Return the expected utility cost of building enough buildings to produce a total of the given amount of the resource per tick.""" 156 return min(option.get_expected_cost(amount) for option in self.options)
157
158 - def _get_available_options(self):
159 """Return a list of the currently available options to produce the resource.""" 160 available_options = [] 161 for option in self.options: 162 if option.available: 163 available_options.append(option) 164 return available_options
165
166 - def build(self, amount):
167 """Try to build a building in the subtree that is currently the cheapest. Return a BUILD_RESULT constant.""" 168 current_production = self.get_final_production_level() 169 if amount < current_production + 1e-7 and self.resource_id not in self.coverage_resources: 170 # we are already producing enough 171 return BUILD_RESULT.ALL_BUILT 172 173 available_options = self._get_available_options() 174 if not available_options: 175 self.log.debug('%s: no available options', self) 176 return BUILD_RESULT.IMPOSSIBLE 177 elif len(available_options) == 1: 178 return available_options[0].build(amount) 179 180 # need to increase production: build the cheapest subtree 181 expected_costs = [] 182 for i, option in enumerate(available_options): 183 cost = option.get_expected_cost(amount - current_production + option.get_final_production_level()) 184 if cost is not None: 185 expected_costs.append((cost, i, option)) 186 187 if not expected_costs: 188 self.log.debug('%s: no possible options', self) 189 return BUILD_RESULT.IMPOSSIBLE 190 else: 191 for option in list(zip(*sorted(expected_costs)))[2]: 192 result = option.build(amount) # TODO: this amount should not include the part provided by the other options 193 if result != BUILD_RESULT.IMPOSSIBLE: 194 return result 195 return BUILD_RESULT.IMPOSSIBLE
196
197 - def reserve(self, amount, may_import):
198 """Reserve currently available production capacity and import from other islands if allowed. Returns the total amount it can reserve or import.""" 199 total_reserved = 0.0 200 for option in self._get_available_options(): 201 total_reserved += option.reserve(max(0.0, amount - total_reserved), may_import) 202 203 # check how much we can import 204 if may_import: 205 required_amount = max(0.0, amount - total_reserved) 206 self.trade_manager.request_quota_change(self.identifier, self.resource_id, required_amount * self.production_ratio) 207 total_reserved += self.get_root_import_level() 208 209 return total_reserved
210
211 - def need_to_build_more_buildings(self, amount):
212 """Return a boolean showing whether more buildings need to be built in order to produce at least the given amount of resource per tick.""" 213 current_production = self.get_final_production_level() 214 if self.resource_id not in self.coverage_resources: 215 return current_production + 1e-7 <= amount 216 for option in self._get_available_options(): 217 if option.need_to_build_more_buildings(amount): 218 return True 219 return False
220
221 - def get_ratio(self, resource_id):
222 """Return the ratio of the given resource needed given that 1 unit of the root resource is required.""" 223 return sum(option.get_ratio(resource_id) for option in self.options)
224
225 226 -class ProductionChainSubtree:
227 """An object of this type represents a subtree of buildings that need to be built in order to produce the given resource.""" 228
229 - def __init__(self, settlement_manager, resource_id, production_line, abstract_building, children, production_ratio):
230 super().__init__() # TODO: check if this call is needed 231 self.settlement_manager = settlement_manager # SettlementManager instance 232 self.resource_manager = settlement_manager.resource_manager # ResourceManager instance 233 self.trade_manager = settlement_manager.trade_manager # TradeManager instance 234 self.resource_id = resource_id # the required resource 235 self.production_line = production_line # ProductionLine instance 236 self.abstract_building = abstract_building # AbstractBuilding instance 237 self.children = children # [ProductionChainSubtreeChoice, ...] 238 self.production_ratio = production_ratio # given that 1 unit has to be produced at the root of the tree, how much has to be produced here? 239 self.ignore_production = abstract_building.ignore_production # whether to try to build more buildings even when the required production capacity has been reached
240
241 - def assign_identifier(self, prefix):
242 """Recursively assign an identifier to this subtree to know which subtree owns which resource quota.""" 243 self.identifier = '{}/{:d},{:d}'.format( 244 prefix, self.resource_id, self.abstract_building.id) 245 for child in self.children: 246 child.assign_identifier(self.identifier)
247 248 @property
249 - def available(self):
250 """Return a boolean showing whether this subtree is currently available.""" 251 return self.settlement_manager.owner.settler_level >= self.abstract_building.settler_level
252
253 - def __str__(self, level=0):
254 result = '{}Produce {:d} (ratio {:.2f}) in {} ({:.5f}, {:.5f})\n'.format( 255 ' ' * level, self.resource_id, 256 self.production_ratio, self.abstract_building.name, 257 self.get_root_production_level(), self.get_final_production_level()) 258 for child in self.children: 259 result += child.__str__(level + 1) 260 return result
261
262 - def get_expected_children_cost(self, amount):
263 """Return the expected utility cost of building enough buildings in the subtrees to produce a total of the given amount of the resource per tick.""" 264 total = 0 265 for child in self.children: 266 cost = child.get_expected_cost(amount) 267 if cost is None: 268 return None 269 total += cost 270 return total
271
272 - def get_expected_cost(self, amount):
273 """Return the expected utility cost of building enough buildings to produce a total of the given amount of the resource per tick.""" 274 children_cost = self.get_expected_children_cost(amount) 275 if children_cost is None: 276 return None 277 278 production_needed = (amount - self.get_root_production_level()) * self.production_ratio 279 root_cost = self.abstract_building.get_expected_cost(self.resource_id, production_needed, self.settlement_manager) 280 if root_cost is None: 281 return None 282 return children_cost + root_cost
283
285 """Return the currently reserved production capacity of this subtree at the root.""" 286 return self.resource_manager.get_quota(self.identifier, self.resource_id, self.abstract_building.id) / self.production_ratio
287
289 """Return the currently reserved production capacity at the bottleneck.""" 290 min_child_production = None 291 for child in self.children: 292 if child.ignore_production: 293 continue 294 production_level = child.get_final_production_level() 295 if min_child_production is None: 296 min_child_production = production_level 297 else: 298 min_child_production = min(min_child_production, production_level) 299 if min_child_production is None: 300 return self.get_root_production_level() 301 else: 302 return min(min_child_production, self.get_root_production_level())
303
304 - def need_more_buildings(self, amount):
305 """Return a boolean showing whether more buildings of this specific type need to be built in order to produce at least the given amount of resource per tick.""" 306 if not self.abstract_building.directly_buildable: 307 return False # building must be triggered by children instead 308 if self.abstract_building.coverage_building: 309 return True 310 return amount > self.get_root_production_level() + 1e-7
311
312 - def build(self, amount):
313 """Build a building in order to get closer to the goal of producing at least the given amount of resource per tick at the bottleneck.""" 314 # try to build one of the lower level buildings (results in a depth first order) 315 result = None 316 for child in self.children: 317 result = child.build(amount) 318 if result == BUILD_RESULT.ALL_BUILT: 319 continue # build another child or build this building 320 elif result == BUILD_RESULT.NEED_PARENT_FIRST: 321 break # parent building has to be built before child (example: farm before field) 322 else: 323 return result # an error or successful building 324 325 if result == BUILD_RESULT.NEED_PARENT_FIRST or self.need_more_buildings(amount): 326 # build a building 327 (result, building) = self.abstract_building.build(self.settlement_manager, self.resource_id) 328 if result == BUILD_RESULT.OUT_OF_SETTLEMENT: 329 return self.settlement_manager.production_builder.extend_settlement(building) 330 return result 331 return BUILD_RESULT.ALL_BUILT
332
333 - def reserve(self, amount, may_import):
334 """Reserve currently available production capacity and import from other islands if allowed. Returns the total amount it can reserve or import.""" 335 total_reserved = amount 336 for child in self.children: 337 total_reserved = min(total_reserved, child.reserve(amount, may_import)) 338 339 self.resource_manager.request_quota_change(self.identifier, True, self.resource_id, self.abstract_building.id, amount * self.production_ratio) 340 total_reserved = min(total_reserved, self.resource_manager.get_quota(self.identifier, self.resource_id, self.abstract_building.id) / self.production_ratio) 341 return total_reserved
342
343 - def need_to_build_more_buildings(self, amount):
344 """Return a boolean showing whether more buildings in this subtree need to be built in order to produce at least the given amount of resource per tick.""" 345 for child in self.children: 346 if child.need_to_build_more_buildings(amount): 347 return True 348 if not self.need_more_buildings(amount): 349 return False 350 return self.abstract_building.need_to_build_more_buildings(self.settlement_manager, self.resource_id)
351
352 - def get_ratio(self, resource_id):
353 """Return the ratio of the given resource needed given that 1 unit of the root resource is required.""" 354 result = self.production_ratio if self.resource_id == resource_id else 0 355 return result + sum(child.get_ratio(resource_id) for child in self.children)
356