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

Source Code for Module horizons.ai.aiplayer.combat.fleet

  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  from collections import defaultdict 
 24  from weakref import WeakKeyDictionary 
 25   
 26  from horizons.component.namedcomponent import NamedComponent 
 27  from horizons.ext.enum import Enum 
 28  from horizons.scheduler import Scheduler 
 29  from horizons.util.python.callback import Callback 
 30  from horizons.util.shapes import Circle, Point 
 31  from horizons.util.worldobject import WorldObject 
 32  from horizons.world.units.unitexeptions import MoveNotPossible 
33 34 35 -class Fleet(WorldObject):
36 """ 37 Fleet object is responsible for moving a group of ship around the map in an ordered manner, that is: 38 1. provide a single move callback for a fleet as a whole, 39 2. resolve self-blocks in a group of ships 40 3. resolve MoveNotPossible exceptions. 41 """ 42 43 log = logging.getLogger("ai.aiplayer.fleet") 44 45 # ship states inside a fleet, fleet doesn't care about AIPlayer.shipStates since it doesn't do any reasoning. 46 # all fleet cares about is to move ships from A to B. 47 shipStates = Enum('idle', 'moving', 'blocked', 'reached') 48 49 RETRY_BLOCKED_TICKS = 16 50 51 # state for a fleet as a whole 52 fleetStates = Enum('idle', 'moving') 53
54 - def __init__(self, ships, destroy_callback=None):
55 super().__init__() 56 57 assert ships, "request to create a fleet from {} ships".format(len(ships)) 58 self.__init(ships, destroy_callback)
59
60 - def __init(self, ships, destroy_callback=None):
61 self.owner = ships[0].owner 62 63 # dictionary of ship => state 64 self._ships = WeakKeyDictionary() 65 for ship in ships: 66 self._ships[ship] = self.shipStates.idle 67 #TODO: @below, this caused errors on one occasion but I was not able to reproduce it. 68 ship.add_remove_listener(Callback(self._lost_ship, ship)) 69 self.state = self.fleetStates.idle 70 self.destroy_callback = destroy_callback
71
72 - def save(self, db):
73 super().save(db) 74 # save the fleet 75 # save destination if fleet is moving somewhere 76 db("INSERT INTO fleet (fleet_id, owner_id, state_id) VALUES(?, ?, ?)", self.worldid, self.owner.worldid, self.state.index) 77 78 if self.state == self.fleetStates.moving and hasattr(self, 'destination'): 79 if isinstance(self.destination, Point): 80 x, y = self.destination.x, self.destination.y 81 db("UPDATE fleet SET dest_x = ?, dest_y = ? WHERE fleet_id = ?", x, y, self.worldid) 82 elif isinstance(self.destination, Circle): 83 x, y, radius = self.destination.center.x, self.destination.center.y, self.destination.radius 84 db("UPDATE fleet SET dest_x = ?, dest_y = ?, radius = ? WHERE fleet_id = ?", x, y, radius, self.worldid) 85 else: 86 assert False, "destination is neither a Circle nor a Point: {}".format( 87 self.destination.__class__.__name__) 88 89 if hasattr(self, "ratio"): 90 db("UPDATE fleet SET ratio = ? WHERE fleet_id = ?", self.ratio, self.worldid) 91 92 # save ships 93 for ship in self.get_ships(): 94 db("INSERT INTO fleet_ship (ship_id, fleet_id, state_id) VALUES(?, ?, ?)", 95 ship.worldid, self.worldid, self._ships[ship].index)
96
97 - def _load(self, worldid, owner, db, destroy_callback):
98 super().load(db, worldid) 99 self.owner = owner 100 state_id, dest_x, dest_y, radius, ratio = db("SELECT state_id, dest_x, dest_y, radius, ratio FROM fleet WHERE fleet_id = ?", worldid)[0] 101 102 if radius: # Circle 103 self.destination = Circle(Point(dest_x, dest_y), radius) 104 elif dest_x and dest_y: # Point 105 self.destination = Point(dest_x, dest_y) 106 else: # No destination 107 pass 108 109 if ratio: 110 self.ratio = ratio 111 112 ships_states = [(WorldObject.get_object_by_id(ship_id), self.shipStates[ship_state_id]) 113 for ship_id, ship_state_id 114 in db("SELECT ship_id, state_id FROM fleet_ship WHERE fleet_id = ?", worldid)] 115 ships = [item[0] for item in ships_states] 116 117 self.__init(ships, destroy_callback) 118 self.state = self.fleetStates[state_id] 119 120 for ship, state in ships_states: 121 self._ships[ship] = state 122 123 if self.state == self.fleetStates.moving: 124 for ship in self.get_ships(): 125 if self._ships[ship] == self.shipStates.moving: 126 ship.add_move_callback(Callback(self._ship_reached, ship)) 127 128 if destroy_callback: 129 self.destroy_callback = destroy_callback
130 131 @classmethod
132 - def load(cls, worldid, owner, db, destroy_callback=None):
133 self = cls.__new__(cls) 134 self._load(worldid, owner, db, destroy_callback) 135 return self
136
137 - def get_ships(self):
138 return self._ships.keys()
139
140 - def destroy(self):
141 for ship in self._ships.keys(): 142 ship.remove_remove_listener(self._lost_ship) 143 if self.destroy_callback: 144 self.destroy_callback()
145
146 - def _lost_ship(self, ship):
147 """ 148 Used when fleet was on the move and one of the ships was killed during that. 149 This way fleet has to check whether the target point was reached. 150 """ 151 if ship in self._ships: 152 del self._ships[ship] 153 if self.size() == 0: 154 self.destroy() 155 elif self._was_target_reached(): 156 self._fleet_reached()
157
158 - def _get_ship_states_count(self):
159 """ 160 Returns Counter about how many ships are in state idle, moving, reached. 161 """ 162 counter = defaultdict(int) 163 for value in self._ships.values(): 164 counter[value] += 1 165 return counter
166
167 - def _was_target_reached(self):
168 """ 169 Checks whether required ratio of ships reached the target. 170 """ 171 state_counts = self._get_ship_states_count() 172 173 # below: include blocked ships as "reached" as well since there's not much more left to do, 174 # and it's better than freezing the whole fleet 175 reached = state_counts[self.shipStates.reached] + state_counts[self.shipStates.blocked] 176 total = len(self._ships) 177 return self.ratio <= float(reached) / total
178
179 - def _ship_reached(self, ship):
180 """ 181 Called when a single ship reaches destination. 182 """ 183 self.log.debug("Fleet %s, Ship %s reached the destination", self.worldid, ship.get_component(NamedComponent).name) 184 self._ships[ship] = self.shipStates.reached 185 if self._was_target_reached(): 186 self._fleet_reached()
187
188 - def _fleet_reached(self):
189 """ 190 Called when whole fleet reaches destination. 191 """ 192 self.log.debug("Fleet %s reached the destination", self.worldid) 193 self.state = self.fleetStates.idle 194 for ship in self._ships.keys(): 195 self._ships[ship] = self.shipStates.idle 196 197 if self.callback: 198 self.callback()
199
200 - def _move_ship(self, ship, destination, callback):
201 # retry ad infinitum. Not the most elegant solution but will do for a while. 202 # Idea: mark ship as "blocked" through state and check whether they all are near the destination anyway 203 # 1. If they don't make them sail again. 204 # 2. If they do, assume they reached the spot. 205 try: 206 ship.move(destination, callback=callback, blocked_callback=Callback(self._move_ship, ship, destination, callback)) 207 self._ships[ship] = self.shipStates.moving 208 except MoveNotPossible: 209 self._ships[ship] = self.shipStates.blocked 210 if not self._was_target_reached(): 211 Scheduler().add_new_object(Callback(self._retry_moving_blocked_ships), self, run_in=self.RETRY_BLOCKED_TICKS)
212
213 - def _get_circle_size(self):
214 """ 215 Destination circle size for movement calls that involve more than one ship. 216 """ 217 return 10
218 #return min(self.size(), 5) 219
221 if self.state != self.fleetStates.moving: 222 return 223 224 for ship in filter(lambda ship: self._ships[ship] == self.shipStates.blocked, self.get_ships()): 225 self._move_ship(ship, self.destination, Callback(self._ship_reached, ship))
226
227 - def move(self, destination, callback=None, ratio=1.0):
228 """ 229 Move fleet to a destination. 230 @param ratio: what percentage of ships has to reach destination in order for the move to be considered done: 231 0.0 - None (not really useful, executes the callback right away) 232 0.0001 - effectively ANY ship 233 1.0 - ALL of the ships 234 0.5 - at least half of the ships 235 etc. 236 """ 237 assert self.size() > 0, "ordered to move a fleet consisting of 0 ships" 238 239 # it's ok to specify single point for a destination only when there's only one ship in a fleet 240 if isinstance(destination, Point) and self.size() > 1: 241 destination = Circle(destination, self._get_circle_size()) 242 243 self.destination = destination 244 self.state = self.fleetStates.moving 245 self.ratio = ratio 246 247 self.callback = callback 248 249 # This is a good place to do something fancier later like preserving ship formation instead sailing to the same point 250 for ship in self._ships.keys(): 251 self._move_ship(ship, destination, Callback(self._ship_reached, ship))
252
253 - def size(self):
254 return len(self._ships)
255
256 - def __str__(self):
257 if hasattr(self, '_ships'): 258 ships_str = "\n " + "\n ".join(["{} (fleet state:{})".format( 259 ship.get_component(NamedComponent).name, 260 self._ships[ship]) for ship in self._ships.keys()]) 261 else: 262 ships_str = 'N/A' 263 return "Fleet: {} , state: {}, ships:{}".format( 264 self.worldid, (self.state if hasattr(self, 'state') else 'unknown state'), ships_str)
265