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

Source Code for Module horizons.ai.aiplayer.trademanager

  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  from collections import defaultdict 
 25   
 26  from horizons.component.namedcomponent import NamedComponent 
 27  from horizons.component.storagecomponent import StorageComponent 
 28  from horizons.constants import RES, STORAGE 
 29  from horizons.util.worldobject import WorldObject, WorldObjectNotFound 
 30   
 31  from .building import AbstractBuilding 
 32  from .mission.domestictrade import DomesticTrade 
33 34 35 -class TradeManager(WorldObject):
36 """ 37 An object of this class manages the continuous domestic resource import process of one settlement. 38 39 This class keeps track of how much of each resource it is importing, what the purpose 40 of each import request is, and organizes the missions to transport the resources 41 from the producing settlements to the one it is managing. 42 43 The process for determining how much can be imported: 44 * find out how much of each resource every other settlement can export, reserve all of it 45 * run the settlement's production capacity reserve process which tries to use the local 46 capacity as much as possible and if that isn't enough then ask this object for 47 more: these requests get approved if we can import the required amount 48 * finalize the amount and source of the imported resources, release the remaining 49 amount to let the trade managers of other settlements do their work 50 51 The process for actually getting the resources 52 For this example settlement A imports from settlement B 53 * TradeManager of A reserves production at the ResourceManager of B as described above 54 * ResourceManager of B keeps track of how much resources it is producing for A 55 * TradeManager of A sends a ship to B to pick up some resources (a DomesticTrade mission) 56 * the ship arrives at the warehouse of B and calls A's TradeManager.load_resources 57 which loads the ship and adjusts the data of B's ResourceManager 58 * the ship arrives at the warehouse of A and unloads the resources 59 """ 60 61 log = logging.getLogger("ai.aiplayer.trademanager") 62 63 # resources that can be produced on another island and transported to where they are needed 64 legal_resources = [RES.FOOD, RES.TEXTILE, RES.LIQUOR, RES.BRICKS, RES.TOBACCO_PRODUCTS, RES.SALT, RES.MEDICAL_HERBS] 65
66 - def __init__(self, settlement_manager):
67 super().__init__() 68 self.__init(settlement_manager)
69
70 - def __init(self, settlement_manager):
71 self.settlement_manager = settlement_manager 72 self.owner = settlement_manager.owner 73 self.data = {} # resource_id: SingleResourceTradeManager 74 self.ships_sent = defaultdict(int) # {settlement_manager_id: num_sent, ...}
75
76 - def save(self, db):
77 super().save(db) 78 db("INSERT INTO ai_trade_manager(rowid, settlement_manager) VALUES(?, ?)", self.worldid, self.settlement_manager.worldid) 79 for resource_manager in self.data.values(): 80 resource_manager.save(db, self.worldid)
81
82 - def _load(self, db, settlement_manager):
83 worldid = db("SELECT rowid FROM ai_trade_manager WHERE settlement_manager = ?", settlement_manager.worldid)[0][0] 84 self.__init(settlement_manager) 85 for db_row in db("SELECT rowid, resource_id FROM ai_single_resource_trade_manager WHERE trade_manager = ?", worldid): 86 self.data[db_row[1]] = SingleResourceTradeManager.load(db, settlement_manager, db_row[0]) 87 super().load(db, worldid)
88 89 @classmethod
90 - def load(cls, db, settlement_manager):
91 self = cls.__new__(cls) 92 self._load(db, settlement_manager) 93 return self
94
95 - def refresh(self):
96 """Reserve the total remaining production in every other settlement and adjust quotas if necessary.""" 97 for resource_manager in self.data.values(): 98 resource_manager.refresh()
99
100 - def finalize_requests(self):
101 """Release the unnecessarily reserved production capacity and decide which settlements will be providing the resources.""" 102 for resource_manager in self.data.values(): 103 resource_manager.finalize_requests()
104
105 - def request_quota_change(self, quota_holder, resource_id, amount):
106 """Request that the quota of quota_holder be changed to the given amount.""" 107 if resource_id not in self.legal_resources: 108 return 109 if resource_id not in self.data: 110 self.data[resource_id] = SingleResourceTradeManager(self.settlement_manager, resource_id) 111 self.data[resource_id].request_quota_change(quota_holder, amount)
112
113 - def get_quota(self, quota_holder, resource_id):
114 """Return the current quota in units per tick.""" 115 if resource_id not in self.legal_resources: 116 return 0.0 117 if resource_id not in self.data: 118 self.data[resource_id] = SingleResourceTradeManager(self.settlement_manager, resource_id) 119 return self.data[resource_id].get_quota(quota_holder)
120
121 - def get_total_import(self, resource_id):
122 """Return the total amount of the given resource imported per tick.""" 123 if resource_id not in self.legal_resources: 124 return 0.0 125 if resource_id not in self.data: 126 self.data[resource_id] = SingleResourceTradeManager(self.settlement_manager, resource_id) 127 return self.data[resource_id].get_total_import()
128
129 - def load_resources(self, mission):
130 """A ship we sent out to retrieve our resources has reached the source settlement so load the resources.""" 131 destination_settlement_manager = mission.destination_settlement_manager 132 ship = mission.ship 133 134 total_amount = defaultdict(int) 135 resource_manager = self.settlement_manager.resource_manager 136 for resource_id, amount in resource_manager.trade_storage[destination_settlement_manager.worldid].items(): 137 available_amount = int(min(math.floor(amount), self.settlement_manager.settlement.get_component(StorageComponent).inventory[resource_id])) 138 if available_amount > 0: 139 total_amount[resource_id] += available_amount 140 141 destination_inventory = destination_settlement_manager.settlement.get_component(StorageComponent).inventory 142 any_transferred = False 143 for resource_id, amount in total_amount.items(): 144 actual_amount = amount - ship.get_component(StorageComponent).inventory[resource_id] 145 actual_amount = min(actual_amount, destination_inventory.get_limit(resource_id) - destination_inventory[resource_id]) 146 if actual_amount <= 0: 147 continue # TODO: consider unloading the resources if there is more than needed 148 any_transferred = True 149 self.log.info('Transfer %d of %d to %s for a journey from %s to %s, total amount %d', actual_amount, 150 resource_id, ship, self.settlement_manager.settlement.get_component(NamedComponent).name, destination_settlement_manager.settlement.get_component(NamedComponent).name, amount) 151 old_amount = self.settlement_manager.settlement.get_component(StorageComponent).inventory[resource_id] 152 mission.move_resource(ship, self.settlement_manager.settlement, resource_id, -actual_amount) 153 actually_transferred = old_amount - self.settlement_manager.settlement.get_component(StorageComponent).inventory[resource_id] 154 resource_manager.trade_storage[destination_settlement_manager.worldid][resource_id] -= actually_transferred 155 156 destination_settlement_manager.trade_manager.ships_sent[self.settlement_manager.worldid] -= 1 157 return any_transferred
158
160 """Return the settlement manager of the settlement from which we should pick up resources next or None if none are needed.""" 161 # TODO: find a better way of getting the following constants 162 ship_capacity = STORAGE.SHIP_TOTAL_STORAGE 163 ship_resource_slots = STORAGE.SHIP_TOTAL_SLOTS_NUMBER 164 165 options = [] # [(available resource amount, available number of resources, settlement_manager_id), ...] 166 for settlement_manager in self.owner.settlement_managers: 167 if settlement_manager is self.settlement_manager: 168 continue 169 resource_manager = settlement_manager.resource_manager 170 num_resources = 0 171 total_amount = 0 172 for resource_id, amount in resource_manager.trade_storage[self.settlement_manager.worldid].items(): 173 available_amount = int(min(math.floor(amount), settlement_manager.settlement.get_component(StorageComponent).inventory[resource_id])) 174 if available_amount > 0: 175 num_resources += 1 176 total_amount += available_amount 177 ships_needed = int(max(math.ceil(num_resources / float(ship_resource_slots)), math.ceil(total_amount / float(ship_capacity)))) 178 if ships_needed > self.ships_sent[settlement_manager.worldid]: 179 self.log.info('have %d ships, need %d ships, %d resource types, %d total amount', 180 self.ships_sent[settlement_manager.worldid], ships_needed, num_resources, total_amount) 181 options.append((total_amount - ship_capacity * self.ships_sent[settlement_manager.worldid], 182 num_resources - ship_resource_slots * self.ships_sent[settlement_manager.worldid], settlement_manager.worldid)) 183 return None if not options else WorldObject.get_object_by_id(max(options)[2])
184
185 - def organize_shipping(self):
186 """Try to send another ship to retrieve resources from one of the settlements we import from.""" 187 source_settlement_manager = self._get_source_settlement_manager() 188 if source_settlement_manager is None: 189 return # no trade ships needed 190 191 # need to get a ship 192 chosen_ship = None 193 for ship, ship_state in sorted(self.owner.ships.items()): 194 if ship_state is self.owner.shipStates.idle: 195 chosen_ship = ship 196 if chosen_ship is None: 197 self.owner.request_ship() 198 return # no available ships 199 200 self.owner.start_mission(DomesticTrade(source_settlement_manager, self.settlement_manager, chosen_ship, self.owner.report_success, self.owner.report_failure)) 201 self.ships_sent[source_settlement_manager.worldid] += 1
202
203 - def __str__(self):
204 result = 'TradeManager({}, {})'.format( 205 self.settlement_manager.settlement.get_component(NamedComponent).name if hasattr( 206 self.settlement_manager, 'settlement') else 'unknown', 207 self.worldid if hasattr(self, 'worldid') else 'none') 208 for resource_manager in self.data.values(): 209 result += '\n' + resource_manager.__str__() 210 return result
211
212 213 -class SingleResourceTradeManager(WorldObject):
214 """An object of this class keeps track of both parties of the resource import/export deal for one resource.""" 215
216 - def __init__(self, settlement_manager, resource_id):
217 super().__init__() 218 self.__init(settlement_manager, resource_id) 219 self.available = 0.0 # unused resource production available per tick 220 self.total = 0.0 # total resource production imported per tick
221
222 - def __init(self, settlement_manager, resource_id):
223 self.settlement_manager = settlement_manager 224 self.resource_id = resource_id 225 self.quotas = {} # {quota_holder: amount, ...} 226 self.partners = {} # {settlement_manager_id: amount, ...} 227 self.identifier = '/{:d},{:d}/trade'.format(self.worldid, self.resource_id) 228 self.building_ids = [] 229 for abstract_building in AbstractBuilding.buildings.values(): 230 if self.resource_id in abstract_building.lines: 231 self.building_ids.append(abstract_building.id)
232
233 - def save(self, db, trade_manager_id):
234 super().save(db) 235 db("INSERT INTO ai_single_resource_trade_manager(rowid, trade_manager, resource_id, available, total) VALUES(?, ?, ?, ?, ?)", 236 self.worldid, trade_manager_id, self.resource_id, self.available, self.total) 237 for identifier, quota in self.quotas.items(): 238 db("INSERT INTO ai_single_resource_trade_manager_quota(single_resource_trade_manager, identifier, quota) VALUES(?, ?, ?)", 239 self.worldid, identifier, quota) 240 for settlement_manager_id, amount in self.partners.items(): 241 db("INSERT INTO ai_single_resource_trade_manager_partner(single_resource_trade_manager, settlement_manager, amount) VALUES(?, ?, ?)", 242 self.worldid, settlement_manager_id, amount)
243
244 - def _load(self, db, settlement_manager, worldid):
245 super().load(db, worldid) 246 resource_id, self.available, self.total = \ 247 db("SELECT resource_id, available, total FROM ai_single_resource_trade_manager WHERE rowid = ?", worldid)[0] 248 self.__init(settlement_manager, resource_id) 249 250 for identifier, quota in db("SELECT identifier, quota FROM ai_single_resource_trade_manager_quota WHERE single_resource_trade_manager = ?", worldid): 251 self.quotas[identifier] = quota 252 253 db_result = db("SELECT settlement_manager, amount FROM ai_single_resource_trade_manager_partner WHERE single_resource_trade_manager = ?", worldid) 254 for settlement_manager_id, amount in db_result: 255 self.partners[settlement_manager_id] = amount
256 257 @classmethod
258 - def load(cls, db, settlement_manager, worldid):
259 self = cls.__new__(cls) 260 self._load(db, settlement_manager, worldid) 261 return self
262
264 """Return the total spare production including the import rate of this settlement (also reserves that amount).""" 265 total = 0.0 266 for settlement_manager in self.settlement_manager.owner.settlement_managers: 267 if self.settlement_manager is not settlement_manager: 268 resource_manager = settlement_manager.resource_manager 269 resource_manager.request_deep_quota_change(self.identifier, False, self.resource_id, 100) 270 total += resource_manager.get_deep_quota(self.identifier, self.resource_id) 271 return total
272
273 - def refresh(self):
274 """Reserve the total remaining production in every other settlement and adjust quotas if necessary.""" 275 currently_used = sum(self.quotas.values()) 276 self.total = self._get_current_spare_production() 277 if self.total >= currently_used: 278 self.available = self.total - currently_used 279 else: 280 self.available = 0.0 281 # unable to honor current quota assignments, decreasing all equally 282 multiplier = 0.0 if abs(self.total) < 1e-7 else self.total / currently_used 283 for quota_holder in self.quotas: 284 if self.quotas[quota_holder] > 1e-7: 285 self.quotas[quota_holder] *= multiplier 286 else: 287 self.quotas[quota_holder] = 0
288
289 - def finalize_requests(self):
290 """Release the unnecessarily reserved production capacity and decide which settlements will be providing the resources.""" 291 options = [] 292 for settlement_manager in self.settlement_manager.owner.settlement_managers: 293 if self.settlement_manager != settlement_manager: 294 resource_manager = settlement_manager.resource_manager 295 amount = resource_manager.get_deep_quota(self.identifier, self.resource_id) 296 options.append((amount, resource_manager.worldid, resource_manager, settlement_manager)) 297 options.sort(reverse=True) 298 299 self.partners = defaultdict(float) 300 needed_amount = self.total - self.available 301 for amount, _, resource_manager, settlement_manager in options: 302 if needed_amount < 1e-9: 303 break 304 if amount > needed_amount: 305 resource_manager.request_deep_quota_change(self.identifier, False, self.resource_id, needed_amount) 306 self.partners[settlement_manager.worldid] += needed_amount 307 needed_amount = 0 308 else: 309 self.partners[settlement_manager.worldid] += amount 310 needed_amount -= amount 311 self.total -= self.available 312 self.available = 0.0
313
314 - def get_quota(self, quota_holder):
315 """Return the current quota in units per tick.""" 316 if quota_holder not in self.quotas: 317 self.quotas[quota_holder] = 0.0 318 return self.quotas[quota_holder]
319
320 - def get_total_import(self):
321 """Return the total amount of resource imported per tick.""" 322 return self.total - self.available
323
324 - def request_quota_change(self, quota_holder, amount):
325 """Request that the quota of quota_holder be changed to the given amount.""" 326 if quota_holder not in self.quotas: 327 self.quotas[quota_holder] = 0.0 328 amount = max(amount, 0.0) 329 330 if abs(amount - self.quotas[quota_holder]) < 1e-7: 331 pass 332 elif amount < self.quotas[quota_holder]: 333 # lower the amount of reserved import 334 change = self.quotas[quota_holder] - amount 335 self.available += change 336 self.quotas[quota_holder] -= change 337 else: 338 # raise the amount of reserved import 339 change = min(amount - self.quotas[quota_holder], self.available) 340 self.available -= change 341 self.quotas[quota_holder] += change
342
343 - def __str__(self):
344 if not hasattr(self, "resource_id"): 345 return "UninitializedSingleResourceTradeManager" 346 result = 'Resource {:d} import {:.5f}/{:.5f}'.format( 347 self.resource_id, self.available, self.total) 348 for quota_holder, quota in self.quotas.items(): 349 result += '\n quota assignment {:.5f} to {}'.format(quota, quota_holder) 350 for settlement_manager_id, amount in self.partners.items(): 351 try: 352 settlement = WorldObject.get_object_by_id(settlement_manager_id).settlement 353 settlement_name = settlement.get_component(NamedComponent).name 354 except WorldObjectNotFound: 355 settlement_name = 'unknown' 356 result += '\n import {:.5f} from {}'.format(amount, settlement_name) 357 return result
358