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

Source Code for Module horizons.ai.aiplayer.resourcemanager

  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 math 
 23  from collections import defaultdict 
 24   
 25  from horizons.ai.aiplayer.building import AbstractBuilding 
 26  from horizons.command.uioptions import ClearTradeSlot, SetTradeSlot 
 27  from horizons.component.namedcomponent import NamedComponent 
 28  from horizons.component.storagecomponent import StorageComponent 
 29  from horizons.component.tradepostcomponent import TradePostComponent 
 30  from horizons.constants import BUILDINGS, RES, TRADER 
 31  from horizons.util.worldobject import WorldObject 
 32  from horizons.world.settlement import Settlement 
33 34 35 -class ResourceManager(WorldObject):
36 """ 37 An object of this class manages production capacity and keeps track of over/under stock. 38 39 The main task of this class is to keep track of the available and used production capacity. 40 That knowledge is used to figure out how much of the settlement's production 41 capacity is being exported and the relevant data is saved accordingly. 42 43 The other important task of this class is to keep track of how much resources the 44 settlement should have in inventory and how much it actually has. 45 That data is used by this class to make buy/sell decisions in this settlement, 46 by InternationalTradeManager to decide which resources to buy/sell at other players' 47 warehouses and by SpecialDomesticTradeManager to decide which resources to transfer 48 between the player's settlements in order to make best use of them. 49 50 Currently the quota priority system works by assigning local requests a high priority 51 and the export requests a low priority which should minimize the amount of resources 52 that have to be transferred. 53 54 The division of resources and production capacities is purely logical and does not 55 affect the way the actual game works. 56 """ 57
58 - def __init__(self, settlement_manager):
59 super().__init__() 60 self.__init(settlement_manager)
61
62 - def __init(self, settlement_manager):
63 self.settlement_manager = settlement_manager 64 self._data = {} # {(resource_id, building_id): SingleResourceManager, ...} 65 self._chain = {} # {resource_id: SimpleProductionChainSubtreeChoice, ...} (cache that doesn't have to be saved) 66 self._low_priority_requests = {} # {(quota_holder, resource_id): amount, ...} (only used during 1 tick, doesn't have to be saved) 67 self._settlement_manager_id = {} # {quota_holder: settlement_manager_id, ...} (cache that doesn't have to be saved) 68 self.trade_storage = defaultdict(lambda: defaultdict(int)) # {settlement_manager_id: {resource_id: float(amount)}, ...} shows how much of a resource is reserved for a particular settlement 69 self.resource_requirements = {} # {resource_id: int(amount), ...} the amount of resource the settlement would like to have in inventory (used to make buy/sell decisions) 70 self.personality = self.settlement_manager.owner.personality_manager.get('ResourceManager')
71
72 - def save(self, db):
73 super().save(db) 74 db("INSERT INTO ai_resource_manager(rowid, settlement_manager) VALUES(?, ?)", self.worldid, self.settlement_manager.worldid) 75 for resource_manager in self._data.values(): 76 resource_manager.save(db, self.worldid) 77 for settlement_manager_id, reserved_storage in self.trade_storage.items(): 78 for resource_id, amount in reserved_storage.items(): 79 if amount > 1e-9: 80 db("INSERT INTO ai_resource_manager_trade_storage(resource_manager, settlement_manager, resource, amount) VALUES(?, ?, ?, ?)", 81 self.worldid, settlement_manager_id, resource_id, amount) 82 for resource_id, amount in self.resource_requirements.items(): 83 db("INSERT INTO ai_resource_manager_requirement(resource_manager, resource, amount) VALUES(?, ?, ?)", self.worldid, resource_id, amount)
84
85 - def _load(self, db, settlement_manager):
86 worldid = db("SELECT rowid FROM ai_resource_manager WHERE settlement_manager = ?", settlement_manager.worldid)[0][0] 87 super().load(db, worldid) 88 self.__init(settlement_manager) 89 for db_row in db("SELECT rowid, resource_id, building_id FROM ai_single_resource_manager WHERE resource_manager = ?", worldid): 90 self._data[(db_row[1], db_row[2])] = SingleResourceManager.load(db, settlement_manager, db_row[0]) 91 for db_row in db("SELECT settlement_manager, resource, amount FROM ai_resource_manager_trade_storage WHERE resource_manager = ?", worldid): 92 self.trade_storage[db_row[0]][db_row[1]] = db_row[2] 93 for db_row in db("SELECT resource, amount FROM ai_resource_manager_requirement WHERE resource_manager = ?", worldid): 94 self.resource_requirements[db_row[0]] = db_row[1]
95 96 @classmethod
97 - def load(cls, db, settlement_manager):
98 self = cls.__new__(cls) 99 self._load(db, settlement_manager) 100 return self
101
102 - def _get_chain(self, resource_id, resource_producer, production_ratio):
103 """Return a SimpleProductionChainSubtreeChoice or None if it impossible to produce the resource.""" 104 options = [] 105 if resource_id in resource_producer: 106 for production_line, abstract_building in resource_producer[resource_id]: 107 possible = True 108 sources = [] 109 for consumed_resource, amount in production_line.consumed_res.items(): 110 next_production_ratio = abs(production_ratio * amount / production_line.produced_res[resource_id]) 111 subtree = self._get_chain(consumed_resource, resource_producer, next_production_ratio) 112 if not subtree: 113 possible = False 114 break 115 sources.append(subtree) 116 if possible: 117 options.append(SimpleProductionChainSubtree(self, resource_id, production_line, abstract_building, sources, production_ratio)) 118 if not options: 119 return None 120 return SimpleProductionChainSubtreeChoice(options)
121
122 - def _make_chain(self, resource_id):
123 """Return a SimpleProductionChainSubtreeChoice that knows how to produce the resource.""" 124 resource_producer = {} 125 for abstract_building in AbstractBuilding.buildings.values(): 126 for resource, production_line in abstract_building.lines.items(): 127 if resource not in resource_producer: 128 resource_producer[resource] = [] 129 resource_producer[resource].append((production_line, abstract_building)) 130 chain = self._get_chain(resource_id, resource_producer, 1.0) 131 chain.assign_identifier('') 132 return chain
133
134 - def refresh(self):
135 """Refresh the actual production capacity of the buildings and lower quotas if necessary.""" 136 for resource_manager in self._data.values(): 137 resource_manager.refresh()
138
139 - def request_quota_change(self, quota_holder, priority, resource_id, building_id, amount):
140 """ 141 Request that the quota of quota_holder be changed to the given amount for the specific resource/building pair. 142 143 @param quota_holder: a string identifying the quota holder (persistent over save/load cycles) 144 @param priority: boolean showing whether this quota has high priority (high priority means that low priority quotas can be lowered if necessary) 145 @param resource_id: the required resource 146 @param building_id: the type of building where this capacity should be gotten from 147 @param amount: the amount of resource per tick that is needed 148 """ 149 150 key = (resource_id, building_id) 151 if key not in self._data: 152 self._data[key] = SingleResourceManager(self.settlement_manager, resource_id, building_id) 153 self._data[key].request_quota_change(quota_holder, priority, amount)
154
155 - def get_quota(self, quota_holder, resource_id, building_id):
156 """Return the current quota given the resource and the type of building that should produce it.""" 157 key = (resource_id, building_id) 158 if key not in self._data: 159 self._data[key] = SingleResourceManager(self.settlement_manager, resource_id, building_id) 160 return self._data[key].get_quota(quota_holder)
161
162 - def request_deep_quota_change(self, quota_holder, priority, resource_id, amount):
163 """Request that the quota of quota_holder be changed to the given amount recursively.""" 164 if resource_id not in self._chain: 165 self._chain[resource_id] = self._make_chain(resource_id) 166 actual_amount = self._chain[resource_id].request_quota_change(quota_holder, amount, priority) 167 if not priority: 168 self._low_priority_requests[(quota_holder, resource_id)] = actual_amount 169 if actual_amount + 1e-9 < amount: 170 # release excess production that can't be used 171 self._chain[resource_id].request_quota_change(quota_holder, actual_amount, priority) 172 return actual_amount
173
174 - def get_deep_quota(self, quota_holder, resource_id):
175 """Return the current quota at the bottleneck.""" 176 if resource_id not in self._chain: 177 self._chain[resource_id] = self._make_chain(resource_id) 178 return self._chain[resource_id].get_quota(quota_holder)
179
181 """Retry adding low priority quota requests. This is required to make the feeder island mechanism work.""" 182 for (quota_holder, resource_id), amount in self._low_priority_requests.items(): 183 self.request_deep_quota_change(quota_holder, False, resource_id, amount)
184
186 """Record the amount of production that should be transferred to other islands.""" 187 for (quota_holder, resource_id), amount in self._low_priority_requests.items(): 188 if quota_holder not in self._settlement_manager_id: 189 self._settlement_manager_id[quota_holder] = WorldObject.get_object_by_id(int(quota_holder[1:].split(',')[0])).settlement_manager.worldid 190 self.trade_storage[self._settlement_manager_id[quota_holder]][resource_id] += ticks * amount
191
192 - def get_total_export(self, resource_id):
193 """Return the total amount of the given resource being (logically) exported per tick.""" 194 total = 0 195 for resource_manager in self._data.values(): 196 if resource_manager.resource_id == resource_id: 197 total += resource_manager.get_total_export() 198 return total
199
200 - def get_total_trade_storage(self, resource_id):
201 """Return the amount of the given resource that should be kept aside for other settlements.""" 202 total = 0 203 for settlement_storage in self.trade_storage.values(): 204 for stored_resource_id, amount in settlement_storage.items(): 205 if stored_resource_id == resource_id: 206 total += amount 207 return int(math.ceil(total))
208
209 - def get_default_resource_requirement(self, resource_id):
210 """Return the default amount of resource that should be in the settlement inventory.""" 211 if resource_id in [RES.TOOLS, RES.BOARDS]: 212 return self.personality.default_resource_requirement 213 elif resource_id == RES.CANNON and self.settlement_manager.settlement.count_buildings(BUILDINGS.BOAT_BUILDER) \ 214 and self.settlement_manager.settlement.owner.need_more_combat_ships: 215 return self.personality.default_cannon_requirement 216 elif self.settlement_manager.feeder_island and resource_id == RES.BRICKS: 217 return self.personality.default_feeder_island_brick_requirement if self.settlement_manager.owner.settler_level > 0 else 0 218 elif not self.settlement_manager.feeder_island and resource_id == RES.FOOD: 219 return self.personality.default_food_requirement 220 return 0
221
222 - def get_unit_building_costs(self, resource_id):
223 return 0 # TODO: take into account all the resources that are needed to build units
224
225 - def get_required_upgrade_resources(self, resource_id, upgrade_limit):
226 """Return the amount of resource still needed to upgrade at most upgrade_limit residences.""" 227 limit_left = upgrade_limit 228 needed = 0 229 for residence in self.settlement_manager.settlement.buildings_by_id.get(BUILDINGS.RESIDENTIAL, []): 230 if limit_left <= 0: 231 break 232 production = residence._upgrade_production 233 if production is None or production.is_paused(): 234 continue 235 for res, amount in production.get_consumed_resources().items(): 236 if res == resource_id and residence.get_component(StorageComponent).inventory[resource_id] < abs(amount): 237 # TODO: take into account the residence's collector 238 needed += abs(amount) - residence.get_component(StorageComponent).inventory[resource_id] 239 limit_left -= 1 240 return needed
241
242 - def get_required_building_resources(self, resource_id):
243 return 0 # TODO
244
245 - def get_current_resource_requirement(self, resource_id):
246 """Return the amount of resource that should be in the settlement inventory to provide for all needs.""" 247 currently_reserved = self.get_total_trade_storage(resource_id) 248 future_reserve = int(math.ceil(self.get_total_export(resource_id) * self.personality.reserve_time)) 249 current_usage = int(math.ceil(self.settlement_manager.get_resource_production_requirement(resource_id) * self.personality.reserve_time)) 250 unit_building_costs = self.get_unit_building_costs(resource_id) 251 upgrade_costs = self.get_required_upgrade_resources(resource_id, self.personality.max_upgraded_houses) 252 building_costs = self.get_required_building_resources(resource_id) 253 254 total_needed = currently_reserved + future_reserve + current_usage + unit_building_costs + upgrade_costs + building_costs 255 return max(total_needed, self.get_default_resource_requirement(resource_id))
256
257 - def manager_buysell(self):
258 """Calculate the required inventory levels and make buy/sell decisions based on that.""" 259 managed_resources = [RES.TOOLS, RES.BOARDS, RES.BRICKS, RES.FOOD, RES.TEXTILE, RES.LIQUOR, RES.TOBACCO_PRODUCTS, RES.SALT, RES.CANNON, RES.MEDICAL_HERBS] 260 settlement = self.settlement_manager.settlement 261 assert isinstance(settlement, Settlement) 262 inventory = settlement.get_component(StorageComponent).inventory 263 session = self.settlement_manager.session 264 gold = self.settlement_manager.owner.get_component(StorageComponent).inventory[RES.GOLD] 265 266 buy_sell_list = [] # [(importance (lower is better), resource_id, limit, sell), ...] 267 for resource_id in managed_resources: 268 current_requirement = self.get_current_resource_requirement(resource_id) 269 self.resource_requirements[resource_id] = current_requirement 270 max_buy = int(round(current_requirement * self.personality.buy_threshold)) # when to stop buying 271 if 0 < current_requirement <= self.personality.low_requirement_threshold: # avoid not buying resources when very little is needed in the first place 272 max_buy = current_requirement 273 min_sell = int(round(current_requirement * self.personality.sell_threshold)) # when to start selling 274 275 if inventory[resource_id] < max_buy: 276 # have 0, need 100, max_buy 67, importance -0.0434 277 # have 0, need 30, max_buy 20, importance -0.034 278 # have 10, need 30, max_buy 20, importance 0.288 279 # have 19, need 30, max_buy 20, importance 0.578 280 # have 66, need 100, max_buy 67, importance 0.610 281 importance = inventory[resource_id] / float(current_requirement + 1) - math.log(max_buy + 10) / 100 282 buy_sell_list.append((importance, resource_id, max_buy, False)) 283 elif inventory[resource_id] > min_sell: 284 price = int(session.db.get_res_value(resource_id) * TRADER.PRICE_MODIFIER_BUY) 285 # have 50, need 30, min_sell 40, gold 5000, price 15, importance 0.08625 286 # have 100, need 30, min_sell 40, gold 5000, price 15, importance 0.02464 287 # have 50, need 30, min_sell 40, gold 0, price 15, importance 0.05341 288 # have 50, need 20, min_sell 27, gold 5000, price 15, importance 0.07717 289 # have 28, need 20, min_sell 27, gold 5000, price 15, importance 0.23150 290 # have 28, need 20, min_sell 27, gold 0, price 15, importance 0.14335 291 # have 50, need 30, min_sell 40, gold 10000000, price 15, importance 0.16248 292 # have 40, need 30, min_sell 40, gold 5000, price 30, importance 0.04452 293 importance = 100.0 / (inventory[resource_id] - min_sell + 10) / (current_requirement + 1) * math.log(gold + 200) / (price + 1) 294 buy_sell_list.append((importance, resource_id, min_sell, True)) 295 if not buy_sell_list: 296 return # nothing to buy nor sell 297 298 trade_post = settlement.get_component(TradePostComponent) 299 trade_slots = trade_post.slots 300 num_slots = len(trade_slots) 301 sell_list = trade_post.sell_list 302 buy_list = trade_post.buy_list 303 304 # discard the less important buy/sell wishes 305 buy_sell_list = sorted(buy_sell_list)[:num_slots] 306 bought_sold_resources = list(zip(*buy_sell_list))[1] 307 308 # clear all slots we will no longer be needing 309 for resource_id in managed_resources: 310 if resource_id in bought_sold_resources: 311 sell = buy_sell_list[bought_sold_resources.index(resource_id)][3] 312 if sell and resource_id in buy_list: 313 ClearTradeSlot(trade_post, buy_list[resource_id]).execute(session) 314 elif not sell and resource_id in sell_list: 315 ClearTradeSlot(trade_post, sell_list[resource_id]).execute(session) 316 else: 317 if resource_id in buy_list: 318 ClearTradeSlot(trade_post, buy_list[resource_id]).execute(session) 319 elif resource_id in sell_list: 320 ClearTradeSlot(trade_post, sell_list[resource_id]).execute(session) 321 322 # add any new offers 323 for resource_id in managed_resources: 324 if resource_id in bought_sold_resources: 325 limit, sell = buy_sell_list[bought_sold_resources.index(resource_id)][2:] 326 if sell and (resource_id not in sell_list or trade_slots[sell_list[resource_id]].limit != limit): 327 SetTradeSlot(trade_post, trade_post.get_free_slot(resource_id), resource_id, True, limit).execute(session) 328 elif not sell and (resource_id not in buy_list or trade_slots[buy_list[resource_id]].limit != limit): 329 SetTradeSlot(trade_post, trade_post.get_free_slot(resource_id), resource_id, False, limit).execute(session)
330
331 - def finish_tick(self):
332 """Clear data used during a single tick.""" 333 self._low_priority_requests.clear()
334
335 - def __str__(self):
336 if not hasattr(self, "settlement_manager"): 337 return 'UninitializedResourceManager' 338 result = 'ResourceManager({}, {:d})'.format( 339 self.settlement_manager.settlement.get_component(NamedComponent).name, self.worldid) 340 for resource_manager in self._data.values(): 341 res = resource_manager.resource_id 342 if res not in [RES.FOOD, RES.TEXTILE, RES.BRICKS]: 343 continue 344 result += '\n' + resource_manager.__str__() 345 return result
346
347 348 -class SingleResourceManager(WorldObject):
349 """An object of this class keeps track of the production capacity of a single resource/building type pair of a settlement.""" 350 351 epsilon = 1e-7 # epsilon for avoiding problems with miniscule values 352 virtual_resources = {RES.FISH, RES.RAW_CLAY, RES.RAW_IRON} # resources that are not actually produced by player owned buildings 353 virtual_production = 9999 # pretend that virtual resources are always produced in this amount (should be larger than actually needed) 354
355 - def __init__(self, settlement_manager, resource_id, building_id):
356 super().__init__() 357 self.__init(settlement_manager, resource_id, building_id) 358 self.low_priority = 0.0 # used resource production per tick assigned to low priority holders 359 self.available = 0.0 # unused resource production per tick 360 self.total = 0.0 # total resource production per tick
361
362 - def __init(self, settlement_manager, resource_id, building_id):
363 self.settlement_manager = settlement_manager 364 self.resource_id = resource_id 365 self.building_id = building_id 366 self.quotas = {} # {quota_holder: (amount, priority), ...}
367
368 - def save(self, db, resource_manager_id):
369 super().save(db) 370 db("INSERT INTO ai_single_resource_manager(rowid, resource_manager, resource_id, building_id, low_priority, available, total) VALUES(?, ?, ?, ?, ?, ?, ?)", 371 self.worldid, resource_manager_id, self.resource_id, self.building_id, self.low_priority, self.available, self.total) 372 for identifier, (quota, priority) in self.quotas.items(): 373 db("INSERT INTO ai_single_resource_manager_quota(single_resource_manager, identifier, quota, priority) VALUES(?, ?, ?, ?)", self.worldid, identifier, quota, priority)
374
375 - def _load(self, db, settlement_manager, worldid):
376 super().load(db, worldid) 377 (resource_id, building_id, self.low_priority, self.available, self.total) = \ 378 db("SELECT resource_id, building_id, low_priority, available, total FROM ai_single_resource_manager WHERE rowid = ?", worldid)[0] 379 self.__init(settlement_manager, resource_id, building_id) 380 381 for (identifier, quota, priority) in db("SELECT identifier, quota, priority FROM ai_single_resource_manager_quota WHERE single_resource_manager = ?", worldid): 382 self.quotas[identifier] = (quota, priority)
383 384 @classmethod
385 - def load(cls, db, settlement_manager, worldid):
386 self = cls.__new__(cls) 387 self._load(db, settlement_manager, worldid) 388 return self
389
390 - def _get_current_production(self):
391 """Return the current amount of resource per tick being produced at buildings of this type.""" 392 if self.resource_id in self.virtual_resources: 393 return self.virtual_production 394 buildings = self.settlement_manager.settlement.buildings_by_id.get(self.building_id, []) 395 return sum(AbstractBuilding.buildings[building.id].get_production_level(building, self.resource_id) for building in buildings)
396
397 - def refresh(self):
398 """Adjust the quotas to take into account the current production levels.""" 399 currently_used = sum(list(zip(*iter(self.quotas.values())))[0]) 400 self.total = self._get_current_production() 401 if self.total + self.epsilon >= currently_used: 402 self.available = self.total - currently_used 403 else: 404 # unable to honor current quota assignments 405 self.available = 0.0 406 if currently_used - self.total <= self.low_priority and self.low_priority > self.epsilon: 407 # the problem can be solved by reducing low priority quotas 408 new_low_priority = max(0.0, self.low_priority - (currently_used - self.total)) 409 multiplier = 0.0 if new_low_priority < self.epsilon else new_low_priority / self.low_priority 410 assert 0.0 <= multiplier < 1.0 411 for quota_holder, (quota, priority) in self.quotas.items(): 412 if quota > self.epsilon and not priority: 413 self.quotas[quota_holder] = (quota * multiplier, priority) 414 elif not priority: 415 self.quotas[quota_holder] = (0.0, priority) 416 self.low_priority = new_low_priority 417 elif currently_used > self.total + self.epsilon: 418 # decreasing all high priority quotas equally, removing low priority quotas completely 419 multiplier = 0.0 if self.total < self.epsilon else self.total / (currently_used - self.low_priority) 420 assert 0.0 <= multiplier < 1.0 421 for quota_holder, (quota, priority) in self.quotas.items(): 422 if quota > self.epsilon and priority: 423 self.quotas[quota_holder] = (quota * multiplier, priority) 424 else: 425 self.quotas[quota_holder] = (0.0, priority) 426 self.low_priority = 0.0
427
428 - def get_quota(self, quota_holder):
429 """Return the current quota of the given quota holder.""" 430 if quota_holder not in self.quotas: 431 self.quotas[quota_holder] = (0.0, False) 432 return self.quotas[quota_holder][0]
433
434 - def request_quota_change(self, quota_holder, priority, amount):
435 """ 436 Request that the quota of quota_holder be changed to the given amount. 437 438 The algorithm: 439 * if the new amount is less than before: set the new quota to the requested value 440 * else if there is enough spare capacity to raise the quota: do it 441 * else assign all the spare capacity 442 * if this is a high priority request: 443 * reduce the low priority quotas to get the maximum possible amount for this quota holder 444 445 @param quota_holder: a string identifying the quota holder (persistent over save/load cycles) 446 @param priority: boolean showing whether this quota has high priority (high priority means that low priority quotas can be lowered if necessary) 447 @param amount: the amount of resource per tick that is needed 448 """ 449 450 if quota_holder not in self.quotas: 451 self.quotas[quota_holder] = (0.0, priority) 452 amount = max(amount, 0.0) 453 454 if abs(amount - self.quotas[quota_holder][0]) < self.epsilon: 455 pass # ignore miniscule change requests 456 elif amount < self.quotas[quota_holder][0]: 457 # lower the amount of reserved production 458 change = self.quotas[quota_holder][0] - amount 459 self.available += change 460 self.quotas[quota_holder] = (self.quotas[quota_holder][0] - change, priority) 461 if not priority: 462 self.low_priority -= change 463 else: 464 if priority and self.available < (amount - self.quotas[quota_holder][0]) and self.low_priority > self.epsilon: 465 # can't get the full requested amount but can get more by reusing some of the low priority quotas 466 new_low_priority = max(0.0, self.low_priority - (amount - self.quotas[quota_holder][0] - self.available)) 467 multiplier = 0.0 if new_low_priority < self.epsilon else new_low_priority / self.low_priority 468 assert 0.0 <= multiplier < 1.0 469 for other_quota_holder, (quota, other_priority) in self.quotas.items(): 470 if quota > self.epsilon and not other_priority: 471 self.quotas[other_quota_holder] = (quota * multiplier, other_priority) 472 elif not other_priority: 473 self.quotas[other_quota_holder] = (0.0, other_priority) 474 self.available += self.low_priority - new_low_priority 475 self.low_priority = new_low_priority 476 477 # raise the amount of reserved production as much as possible 478 change = min(amount - self.quotas[quota_holder][0], self.available) 479 self.available -= change 480 self.quotas[quota_holder] = (self.quotas[quota_holder][0] + change, priority) 481 if not priority: 482 self.low_priority += change
483
484 - def get_total_export(self):
485 """Return the total amount of capacity that is reserved by quota holders in other settlements.""" 486 # this is accurate for now because all trade is set to low priority and nothing else is 487 return self.low_priority
488
489 - def __str__(self):
490 if not hasattr(self, "resource_id"): 491 return 'UninitializedSingleResourceManager' 492 result = 'Resource {:d} production {:.5f}/{:.5f} ({:.5f} low priority)'.format( 493 self.resource_id, self.available, self.total, self.low_priority) 494 for quota_holder, (quota, priority) in self.quotas.items(): 495 result += '\n {}quota assignment {:.5f} to {}'.format( 496 'priority ' if priority else '', quota, quota_holder) 497 return result
498
499 500 -class SimpleProductionChainSubtreeChoice:
501 """This is a simple version of ProductionChainSubtreeChoice used to make recursive quotas possible.""" 502
503 - def __init__(self, options):
504 super().__init__() # TODO: check if this call is needed 505 self.options = options # [SimpleProductionChainSubtree, ...] 506 self.resource_id = options[0].resource_id
507
508 - def assign_identifier(self, prefix):
509 """Recursively assign an identifier to this subtree to know which subtree owns which resource quota.""" 510 self.identifier = prefix + ('/choice' if len(self.options) > 1 else '') 511 for option in self.options: 512 option.assign_identifier(self.identifier)
513
514 - def request_quota_change(self, quota_holder, amount, priority):
515 """Try to reserve currently available production. Return the total amount that can be reserved.""" 516 total_reserved = 0.0 517 for option in self.options: 518 total_reserved += option.request_quota_change(quota_holder, max(0.0, amount - total_reserved), priority) 519 return total_reserved
520
521 - def get_quota(self, quota_holder):
522 return sum(option.get_quota(quota_holder) for option in self.options)
523
524 525 -class SimpleProductionChainSubtree:
526 """This is a simple version of ProductionChainSubtree used to make recursive quotas possible.""" 527
528 - def __init__(self, resource_manager, resource_id, production_line, abstract_building, children, production_ratio):
529 super().__init__() # TODO: check if this call is needed 530 self.resource_manager = resource_manager 531 self.resource_id = resource_id 532 self.production_line = production_line 533 self.abstract_building = abstract_building 534 self.children = children # [SimpleProductionChainSubtreeChoice, ...] 535 self.production_ratio = production_ratio
536
537 - def assign_identifier(self, prefix):
538 """Recursively assign an identifier to this subtree to know which subtree owns which resource quota.""" 539 self.identifier = '{}/{:d},{:d}'.format(prefix, self.resource_id, self.abstract_building.id) 540 for child in self.children: 541 child.assign_identifier(self.identifier)
542
543 - def request_quota_change(self, quota_holder, amount, priority):
544 """Try to reserve currently available production. Return the total amount that can be reserved.""" 545 total_reserved = amount 546 for child in self.children: 547 total_reserved = min(total_reserved, child.request_quota_change(quota_holder, amount, priority)) 548 549 self.resource_manager.request_quota_change(quota_holder + self.identifier, priority, self.resource_id, self.abstract_building.id, amount * self.production_ratio) 550 return min(total_reserved, self.resource_manager.get_quota(quota_holder + self.identifier, self.resource_id, self.abstract_building.id) / self.production_ratio)
551
552 - def get_quota(self, quota_holder):
553 """Return the current quota at the bottleneck.""" 554 root_quota = self.resource_manager.get_quota(quota_holder + self.identifier, self.resource_id, self.abstract_building.id) / self.production_ratio 555 if self.children: 556 return min(root_quota, min(child.get_quota(quota_holder) for child in self.children)) 557 return root_quota
558