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

Source Code for Module horizons.ai.trader

  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.generic import GenericAI 
 25  from horizons.command.unit import CreateUnit 
 26  from horizons.component.tradepostcomponent import TradePostComponent 
 27  from horizons.constants import BUILDINGS, TRADER, UNITS 
 28  from horizons.ext.enum import Enum 
 29  from horizons.messaging import NewSettlement 
 30  from horizons.scheduler import Scheduler 
 31  from horizons.util.python.callback import Callback 
 32  from horizons.util.shapes import Circle 
 33  from horizons.util.worldobject import WorldObject 
 34  from horizons.world.disaster.blackdeathdisaster import BlackDeathDisaster 
 35  from horizons.world.units.unitexeptions import MoveNotPossible 
 36   
 37   
38 -class Trader(GenericAI):
39 """A trader represents the free trader that travels around the map with its trading ship(s) and 40 sells resources to players and buys resources from them. This is a very simple form of AI, as it 41 doesn't do any more then drive to a place on water or a warehouse randomly and then buys and 42 sells resources. A game should not have more then one free trader (it could though) 43 @param id: int - player id, every Player needs a unique id, as the free trader is a Player instance, it also does. 44 @param name: Traders name, also needed for the Player class. 45 @param color: util.Color instance with the traders banner color, also needed for the Player class""" 46 47 shipStates = Enum.get_extended(GenericAI.shipStates, 'moving_to_warehouse', 'reached_warehouse') 48 49 log = logging.getLogger("ai.trader") 50 regular_player = False 51
52 - def __init__(self, session, id, name, color, **kwargs):
53 super().__init__(session, id, name, color, **kwargs) 54 self.__init() 55 map_size = self.session.world.map_dimensions.width 56 while map_size > 0: 57 self.create_ship() 58 map_size -= TRADER.TILES_PER_TRADER
59
60 - def create_ship(self):
61 """Create a ship and place it randomly""" 62 self.log.debug("Trader %s: creating new ship", self.worldid) 63 point = self.session.world.get_random_possible_ship_position() 64 ship = CreateUnit(self.worldid, UNITS.TRADER_SHIP, point.x, point.y)(issuer=self) 65 self.ships[ship] = self.shipStates.reached_warehouse 66 Scheduler().add_new_object(Callback(self.ship_idle, ship), self, run_in=0)
67
68 - def __init(self):
69 self.warehouse = {} # { ship.worldid : warehouse }. stores the warehouse the ship is currently heading to 70 self.allured_by_signal_fire = {} # bool, used to get away from a signal fire (and not be allured again immediately) 71 72 NewSettlement.subscribe(self._on_new_settlement)
73
74 - def _on_new_settlement(self, msg):
75 # make sure there's a trader ship for SETTLEMENTS_PER_SHIP settlements 76 if len(self.session.world.settlements) > self.get_ship_count() * TRADER.SETTLEMENTS_PER_SHIP: 77 self.create_ship()
78
79 - def save(self, db):
80 super().save(db) 81 82 # mark self as a trader 83 db("UPDATE player SET is_trader = 1 WHERE rowid = ?", self.worldid) 84 85 for ship in self.ships: 86 # prepare values 87 ship_state = self.ships[ship] 88 89 remaining_ticks = None 90 # get current callback in scheduler, according to ship state, to retrieve 91 # the number of ticks, when the call will actually be done 92 current_callback = None 93 if ship_state == self.shipStates.reached_warehouse: 94 current_callback = Callback(self.ship_idle, ship) 95 if current_callback is not None: 96 # current state has a callback 97 calls = Scheduler().get_classinst_calls(self, current_callback) 98 assert len(calls) == 1, "got {} calls for saving {}: {}".format( 99 len(calls), current_callback, calls) 100 remaining_ticks = max(list(calls.values())[0], 1) 101 102 targeted_warehouse = None if ship.worldid not in self.warehouse else self.warehouse[ship.worldid].worldid 103 104 # put them in the database 105 db("INSERT INTO trader_ships(rowid, state, remaining_ticks, targeted_warehouse) \ 106 VALUES(?, ?, ?, ?)", ship.worldid, ship_state.index, remaining_ticks, targeted_warehouse)
107
108 - def _load(self, db, worldid):
109 super()._load(db, worldid) 110 self.__init()
111
112 - def load_ship_states(self, db):
113 # load ships one by one from db (ship instances themselves are loaded already, but 114 # we have to use them here) 115 for ship_id, state_id, remaining_ticks, targeted_warehouse in \ 116 db("SELECT rowid, state, remaining_ticks, targeted_warehouse FROM trader_ships"): 117 state = self.shipStates[state_id] 118 ship = WorldObject.get_object_by_id(ship_id) 119 120 self.ships[ship] = state 121 122 if state == self.shipStates.moving_random: 123 ship.add_move_callback(Callback(self.ship_idle, ship)) 124 elif state == self.shipStates.moving_to_warehouse: 125 ship.add_move_callback(Callback(self.reached_warehouse, ship)) 126 assert targeted_warehouse is not None 127 self.warehouse[ship.worldid] = WorldObject.get_object_by_id(targeted_warehouse) 128 elif state == self.shipStates.reached_warehouse: 129 assert remaining_ticks is not None 130 Scheduler().add_new_object( 131 Callback(self.ship_idle, ship), self, remaining_ticks)
132
133 - def get_ship_count(self):
134 """Returns number of ships""" 135 return len(self.ships)
136
137 - def send_ship_random(self, ship):
138 """Sends a ship to a random position on the map. 139 @param ship: Ship instance that is to be used""" 140 super().send_ship_random(ship) 141 ship.add_conditional_callback(Callback(self._check_for_signal_fire_in_ship_range, ship), 142 callback=Callback(self._ship_found_signal_fire, ship))
143
145 """Returns the signal fire instance, if there is one in the ships range, else False""" 146 if ship in self.allured_by_signal_fire and self.allured_by_signal_fire[ship]: 147 return False # don't visit signal fire again 148 for tile in self.session.world.get_tiles_in_radius(ship.position, ship.radius): 149 try: 150 if tile.object.id == BUILDINGS.SIGNAL_FIRE: 151 return tile.object 152 except AttributeError: 153 pass # tile has no object or object has no id 154 return False
155
156 - def _ship_found_signal_fire(self, ship):
157 signal_fire = self._check_for_signal_fire_in_ship_range(ship) 158 self.log.debug("Trader %s ship %s found signal fire %s", self.worldid, ship.worldid, signal_fire) 159 # search a warehouse in the range of the signal fire and move to it 160 warehouses = self.session.world.get_warehouses() 161 for house in warehouses: 162 if house.position.distance(signal_fire.position) <= signal_fire.radius and \ 163 house.owner == signal_fire.owner: 164 self.log.debug("Trader %s moving to house %s", self.worldid, house) 165 self.allured_by_signal_fire[ship] = True 166 167 # HACK: remove allured flag in a few ticks 168 def rem_allured(self, ship): 169 self.allured_by_signal_fire[ship] = False
170 Scheduler().add_new_object(Callback(rem_allured, self, ship), self, Scheduler().get_ticks(60)) 171 self.send_ship_random_warehouse(ship, house) 172 return 173 self.log.debug("Trader can't find warehouse in range of signal fire")
174
175 - def send_ship_random_warehouse(self, ship, warehouse=None):
176 """Sends a ship to a random warehouse on the map 177 @param ship: Ship instance that is to be used 178 @param warehouse: warehouse instance to move to. Random one is selected on None.""" 179 self.log.debug("Trader %s ship %s moving to warehouse (random=%s)", self.worldid, ship.worldid, 180 (warehouse is None)) 181 #TODO maybe this kind of list should be saved somewhere, as this is pretty performance intense 182 warehouses = self.session.world.get_warehouses() 183 # Remove all warehouses that are not safe to visit 184 warehouses = list(filter(self.is_warehouse_safe, warehouses)) 185 if not warehouses: # there aren't any warehouses, move randomly 186 self.send_ship_random(ship) 187 else: # select a warehouse 188 if warehouse is None: 189 self.warehouse[ship.worldid] = self.session.random.choice(warehouses) 190 else: 191 self.warehouse[ship.worldid] = warehouse 192 try: # try to find a possible position near the warehouse 193 ship.move(Circle(self.warehouse[ship.worldid].position.center, ship.radius), Callback(self.reached_warehouse, ship)) 194 self.ships[ship] = self.shipStates.moving_to_warehouse 195 except MoveNotPossible: 196 self.send_ship_random(ship)
197
198 - def is_warehouse_safe(self, warehouse):
199 """Checkes whether a warehouse is safe to visit""" 200 return not isinstance(self.session.world.disaster_manager.get_disaster(warehouse.settlement), BlackDeathDisaster)
201
202 - def reached_warehouse(self, ship):
203 """Actions that need to be taken when reaching a warehouse: 204 Sell demanded res, buy offered res, simulate load/unload, continue route. 205 @param ship: ship instance""" 206 self.log.debug("Trader %s ship %s: reached warehouse", self.worldid, ship.worldid) 207 settlement = self.warehouse[ship.worldid].settlement 208 # NOTE: must be sorted for mp games (same order everywhere) 209 trade_comp = settlement.get_component(TradePostComponent) 210 for res in sorted(trade_comp.buy_list.keys()): # check for resources that the settlement wants to buy 211 # select a random amount to sell 212 amount = self.session.random.randint(TRADER.SELL_AMOUNT_MIN, TRADER.SELL_AMOUNT_MAX) 213 # try to sell all, else try smaller pieces 214 for try_amount in range(amount, 0, -1): 215 price = int(self.session.db.get_res_value(res) * TRADER.PRICE_MODIFIER_SELL * try_amount) 216 trade_successful = trade_comp.buy(res, try_amount, price, self.worldid) 217 self.log.debug("Trader %s: offered sell %s tons of res %s, success: %s", self.worldid, try_amount, res, trade_successful) 218 if trade_successful: 219 break 220 221 # NOTE: must be sorted for mp games (same order everywhere) 222 for res in sorted(trade_comp.sell_list.keys()): 223 # select a random amount to buy from the settlement 224 amount = self.session.random.randint(TRADER.BUY_AMOUNT_MIN, TRADER.BUY_AMOUNT_MAX) 225 # try to buy all, else try smaller pieces 226 for try_amount in range(amount, 0, -1): 227 price = int(self.session.db.get_res_value(res) * TRADER.PRICE_MODIFIER_BUY * try_amount) 228 trade_successful = trade_comp.sell(res, try_amount, price, self.worldid) 229 self.log.debug("Trader %s: offered buy %s tons of res %s, success: %s", self.worldid, try_amount, res, trade_successful) 230 if trade_successful: 231 break 232 233 del self.warehouse[ship.worldid] 234 # wait a few seconds before going on to simulate loading/unloading process 235 Scheduler().add_new_object(Callback(self.ship_idle, ship), self, 236 Scheduler().get_ticks(TRADER.TRADING_DURATION)) 237 self.ships[ship] = self.shipStates.reached_warehouse
238
239 - def ship_idle(self, ship):
240 """Called if a ship is idle. Sends ship to either a random place or warehouse. 241 Probability for 'random warehouse' in percent: TRADER.BUSINESS_SENSE. 242 @param ship: ship instance""" 243 if self.session.random.randint(0, 100) < TRADER.BUSINESS_SENSE: 244 # delay one tick, to allow old movement calls to completely finish 245 self.log.debug("Trader %s ship %s: idle, moving to random warehouse", self.worldid, ship.worldid) 246 Scheduler().add_new_object(Callback(self.send_ship_random_warehouse, ship), self, run_in=0) 247 else: 248 self.log.debug("Trader %s ship %s: idle, moving to random location", self.worldid, ship.worldid) 249 Scheduler().add_new_object(Callback(self.send_ship_random, ship), self, run_in=0)
250
251 - def end(self):
252 super().end() 253 NewSettlement.unsubscribe(self._on_new_settlement)
254