Package horizons :: Package component :: Module tradepostcomponent
[hide private]
[frames] | no frames]

Source Code for Module horizons.component.tradepostcomponent

  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  from horizons.component import Component 
 23  from horizons.component.storagecomponent import StorageComponent 
 24  from horizons.constants import RES, TRADER 
 25  from horizons.i18n import gettext as T 
 26  from horizons.scheduler import Scheduler 
 27  from horizons.util.changelistener import ChangeListener 
 28  from horizons.util.worldobject import WorldObject 
29 30 31 -class TRADE_ERROR_TYPE:
32 """Machine controlled entities need to know the difference. On this basis, they decide 33 whether to retry the trade in a few seconds. 34 """ 35 NO_ERROR, TEMPORARY, PERMANENT = range(3)
36
37 38 -class TradeSlotInfo:
39 - def __init__(self, resource_id, selling, limit):
40 self.resource_id = resource_id 41 self.selling = selling 42 self.limit = limit
43
44 45 -class TradePostComponent(ChangeListener, Component):
46 """This Class has to be inherited by every class that wishes to use BuySellTab and trade with 47 the free trader. 48 """ 49 NAME = 'tradepostcomponent' 50 yaml_tag = '!TradePostComponent' 51
52 - def __init__(self):
53 super().__init__()
54
55 - def initialize(self):
56 self.slots = [None, None, None] # [TradeSlotInfo, ...] 57 self.buy_list = {} # dict of resources that are to be bought. {resource_id: slot_id, ...} 58 self.sell_list = {} # dict of resources that are to be sold. {resource_id: slot_id, ...} 59 self.trade_history = [] # [(tick, player_id, resource_id, amount, gold), ...] ordered by tick, player_id 60 self.buy_history = {} # {tick_id: (res, amount, price), ...} 61 self.sell_history = {} # {tick_id: (res, amount, price), ...} 62 self.total_income = 0 63 self.total_expenses = 0
64
65 - def set_slot(self, slot_id, resource_id, selling, limit):
66 self.clear_slot(slot_id, False) 67 self.slots[slot_id] = TradeSlotInfo(resource_id, selling, limit) 68 if selling: 69 self.sell_list[resource_id] = slot_id 70 else: 71 self.buy_list[resource_id] = slot_id 72 self._changed()
73
74 - def clear_slot(self, slot_id, trigger_changed):
75 if self.slots[slot_id] is not None: 76 old_resource_id = self.slots[slot_id].resource_id 77 if self.slots[slot_id].selling: 78 del self.sell_list[old_resource_id] 79 else: 80 del self.buy_list[old_resource_id] 81 82 self.slots[slot_id] = None 83 if trigger_changed: 84 self._changed()
85
86 - def get_free_slot(self, resource_id):
87 if resource_id in self.buy_list: 88 return self.buy_list[resource_id] 89 if resource_id in self.sell_list: 90 return self.sell_list[resource_id] 91 for i in range(len(self.slots)): 92 if self.slots[i] is None: 93 return i 94 return None
95
96 - def save(self, db):
97 super().save(db) 98 99 for slot_id in range(len(self.slots)): 100 if self.slots[slot_id] is not None: 101 db("INSERT INTO trade_slots(trade_post, slot_id, resource_id, selling, trade_limit) VALUES(?, ?, ?, ?, ?)", 102 self.instance.worldid, slot_id, self.slots[slot_id].resource_id, self.slots[slot_id].selling, self.slots[slot_id].limit) 103 104 db("INSERT INTO trade_values(object, total_income, total_expenses) VALUES (?, ?, ?)", 105 self.instance.worldid, self.total_income, self.total_expenses) 106 107 for row in self.trade_history: 108 translated_tick = row[0] - Scheduler().cur_tick # pre-translate for the loading process 109 db("INSERT INTO trade_history(settlement, tick, player, resource_id, amount, gold) VALUES(?, ?, ?, ?, ?, ?)", 110 self.instance.worldid, translated_tick, row[1], row[2], row[3], row[4])
111
112 - def load(self, db, worldid):
113 super().load(db, worldid) 114 self.initialize() 115 116 for (slot_id, resource_id, selling, limit) in db("SELECT slot_id, resource_id, selling, trade_limit FROM trade_slots WHERE trade_post = ?", worldid): 117 self.set_slot(slot_id, resource_id, selling, limit) 118 119 self.total_income, self.total_expenses = db("SELECT total_income, total_expenses FROM trade_values WHERE object = ?", worldid)[0] 120 121 for row in db("SELECT tick, player, resource_id, amount, gold FROM trade_history WHERE settlement = ? ORDER BY tick, player", worldid): 122 self.trade_history.append(row)
123
124 - def get_owner_inventory(self):
125 return self.instance.owner.get_component(StorageComponent).inventory
126
127 - def get_inventory(self):
128 return self.instance.get_component(StorageComponent).inventory
129
130 - def buy(self, res, amount, price, player_id):
131 """Buy from the free trader. 132 @param res: 133 @param amount: 134 @param price: cumulative price for whole amount of res 135 @param player_id: the worldid of the trade partner 136 @return bool, whether we did buy it""" 137 assert price >= 0, "the price must be POSITIVE" 138 assert amount >= 0, "the amount must be POSITIVE" 139 if res not in self.buy_list or \ 140 self.get_owner_inventory()[RES.GOLD] < price or \ 141 self.get_inventory().get_free_space_for(res) < amount or \ 142 amount + self.get_inventory()[res] > self.slots[self.buy_list[res]].limit: 143 self._changed() 144 return False 145 146 else: 147 remnant = self.get_owner_inventory().alter(RES.GOLD, -price) 148 assert remnant == 0 149 remnant = self.get_inventory().alter(res, amount) 150 assert remnant == 0 151 self.trade_history.append((Scheduler().cur_tick, player_id, res, amount, -price)) 152 self.buy_history[Scheduler().cur_tick] = (res, amount, price) 153 self.total_expenses += amount * price 154 self._changed() 155 return True 156 assert False
157
158 - def sell(self, res, amount, price, player_id):
159 """Sell to the free trader. 160 @param res: 161 @param amount: 162 @param price: cumulative price for whole amount of res 163 @param player_id: the worldid of the trade partner 164 @return bool, whether we did sell it""" 165 assert price >= 0, "the price must be POSITIVE" 166 assert amount >= 0, "the amount must be POSITIVE" 167 if res not in self.sell_list or \ 168 self.get_inventory()[res] < amount or \ 169 self.get_inventory()[res] - amount < self.slots[self.sell_list[res]].limit: 170 self._changed() 171 return False 172 173 else: 174 remnant = self.get_owner_inventory().alter(RES.GOLD, price) 175 assert remnant == 0 176 remnant = self.get_inventory().alter(res, -amount) 177 assert remnant == 0 178 self.trade_history.append((Scheduler().cur_tick, player_id, res, -amount, price)) 179 self.sell_history[Scheduler().cur_tick] = (res, amount, price) 180 self.total_income += amount * price 181 self._changed() 182 return True 183 assert False
184
185 - def sell_resource(self, ship_worldid, resource_id, amount, add_error_type=False, suppress_messages=False):
186 """ Attempt to sell the given amount of resource to the ship, returns the amount sold. 187 @param add_error_type: if True, return tuple where second item is ERROR_TYPE""" 188 ship = WorldObject.get_object_by_id(ship_worldid) 189 190 def err(string, err_type): 191 if not suppress_messages and ship.owner.is_local_player: 192 self.session.ingame_gui.message_widget.add_custom(string, point=ship.position) 193 return 0 if not add_error_type else (0, err_type)
194 195 if resource_id not in self.sell_list: 196 return err(T("The trade partner does not sell this."), TRADE_ERROR_TYPE.PERMANENT) 197 198 price = int(self.session.db.get_res_value(resource_id) * TRADER.PRICE_MODIFIER_BUY) # price per ton of resource 199 assert price > 0 200 201 # can't sell more than the ship can fit in its inventory 202 amount = min(amount, ship.get_component(StorageComponent).inventory.get_free_space_for(resource_id)) 203 if amount <= 0: 204 return err(T("You can not store this."), TRADE_ERROR_TYPE.PERMANENT) 205 # can't sell more than the ship's owner can afford 206 amount = min(amount, ship.owner.get_component(StorageComponent).inventory[RES.GOLD] // price) 207 if amount <= 0: 208 return err(T("You can not afford to buy this."), TRADE_ERROR_TYPE.TEMPORARY) 209 # can't sell more than what we have 210 amount = min(amount, self.get_inventory()[resource_id]) 211 # can't sell more than we are trying to sell according to the settings 212 amount = min(amount, self.get_inventory()[resource_id] - self.slots[self.sell_list[resource_id]].limit) 213 if amount <= 0: 214 return err(T("The trade partner does not sell more of this."), TRADE_ERROR_TYPE.TEMPORARY) 215 216 total_price = price * amount 217 assert self.get_owner_inventory().alter(RES.GOLD, total_price) == 0 218 assert ship.owner.get_component(StorageComponent).inventory.alter(RES.GOLD, -total_price) == 0 219 assert self.get_inventory().alter(resource_id, -amount) == 0 220 assert ship.get_component(StorageComponent).inventory.alter(resource_id, amount) == 0 221 self.trade_history.append((Scheduler().cur_tick, ship.owner.worldid, resource_id, -amount, total_price)) 222 self.sell_history[Scheduler().cur_tick] = (resource_id, amount, total_price) 223 self.total_income += total_price 224 self._changed() 225 return amount if not add_error_type else amount, TRADE_ERROR_TYPE.NO_ERROR
226
227 - def buy_resource(self, ship_worldid, resource_id, amount, add_error_type=False, suppress_messages=False):
228 """ Attempt to buy the given amount of resource from the ship, return the amount bought 229 @param add_error_type: if True, return tuple where second item is ERROR_TYPE""" 230 ship = WorldObject.get_object_by_id(ship_worldid) 231 232 def err(string, err_type): 233 if not suppress_messages and ship.owner.is_local_player: 234 self.session.ingame_gui.message_widget.add_custom(string, point=ship.position) 235 return 0 if not add_error_type else 0, err_type
236 237 if resource_id not in self.buy_list: 238 return err(T("The trade partner does not buy this."), TRADE_ERROR_TYPE.PERMANENT) 239 240 price = int(self.session.db.get_res_value(resource_id) * TRADER.PRICE_MODIFIER_SELL) # price per ton of resource 241 assert price > 0 242 243 # can't buy more than the ship has 244 amount = min(amount, ship.get_component(StorageComponent).inventory[resource_id]) 245 if amount <= 0: 246 return err(T("You do not possess this."), TRADE_ERROR_TYPE.PERMANENT) 247 # can't buy more than we can fit in the inventory 248 amount = min(amount, self.get_inventory().get_free_space_for(resource_id)) 249 if amount <= 0: 250 return err(T("The trade partner can not store more of this."), TRADE_ERROR_TYPE.TEMPORARY) 251 # can't buy more than we can afford 252 amount = min(amount, self.get_owner_inventory()[RES.GOLD] // price) 253 if amount <= 0: 254 return err(T("The trade partner can not afford to buy this."), TRADE_ERROR_TYPE.TEMPORARY) 255 256 # can't buy more than we are trying to buy according to the settings 257 amount = min(amount, self.slots[self.buy_list[resource_id]].limit - self.get_inventory()[resource_id]) 258 if amount <= 0: 259 return err(T("The trade partner does not buy more of this."), TRADE_ERROR_TYPE.TEMPORARY) 260 261 total_price = price * amount 262 assert self.get_owner_inventory().alter(RES.GOLD, -total_price) == 0 263 assert ship.owner.get_component(StorageComponent).inventory.alter(RES.GOLD, total_price) == 0 264 assert self.get_inventory().alter(resource_id, amount) == 0 265 assert ship.get_component(StorageComponent).inventory.alter(resource_id, -amount) == 0 266 self.trade_history.append((Scheduler().cur_tick, ship.owner.worldid, resource_id, amount, -total_price)) 267 self.buy_history[Scheduler().cur_tick] = (resource_id, amount, total_price) 268 self.total_expenses += total_price 269 self._changed() 270 return amount if not add_error_type else amount, TRADE_ERROR_TYPE.TEMPORARY 271 272 @property
273 - def sell_income(self):
274 """Returns sell income of last month. 275 Deletes older entries of the sell list.""" 276 income = 0 277 last_month_start = Scheduler().cur_tick - Scheduler().get_ticks_of_month() 278 keys_to_delete = [] 279 for key, values in self.sell_history.items(): 280 if key < last_month_start: 281 keys_to_delete.append(key) 282 else: 283 income += values[2] 284 # remove old keys 285 for key in keys_to_delete: 286 del self.sell_history[key] 287 return income
288 289 @property
290 - def buy_expenses(self):
291 """Returns last months buy expenses. 292 Deletes older entries of the buy list.""" 293 expenses = 0 294 last_month_start = Scheduler().cur_tick - Scheduler().get_ticks_of_month() 295 keys_to_delete = [] 296 for key, values in self.buy_history.items(): 297 if key < last_month_start: 298 keys_to_delete.append(key) 299 else: 300 expenses += values[2] 301 # remove old keys 302 for key in keys_to_delete: 303 del self.buy_history[key] 304 return expenses
305 306 @property
307 - def total_earnings(self):
308 """Returns the entire earning of this settlement 309 total_earnings = sell_income - buy_expenses""" 310 return self.total_income - self.total_expenses
311