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

Source Code for Module horizons.ai.aiplayer.areabuilder

  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 copy 
 23  import logging 
 24  from collections import deque 
 25   
 26  from horizons.ai.aiplayer.basicbuilder import BasicBuilder 
 27  from horizons.ai.aiplayer.constants import BUILD_RESULT, BUILDING_PURPOSE 
 28  from horizons.ai.aiplayer.roadplanner import RoadPlanner 
 29  from horizons.constants import BUILDINGS 
 30  from horizons.entities import Entities 
 31  from horizons.util.shapes import Rect 
 32  from horizons.util.worldobject import WorldObject 
33 34 35 -class AreaBuilder(WorldObject):
36 """A class governing the use of a specific type of area of a settlement.""" 37 38 log = logging.getLogger("ai.aiplayer.area_builder") 39
40 - def __init__(self, settlement_manager):
41 super().__init__() 42 self.__init(settlement_manager)
43
44 - def __init(self, settlement_manager):
45 self.settlement_manager = settlement_manager 46 self.land_manager = settlement_manager.land_manager 47 self.island = self.land_manager.island 48 self.session = self.island.session 49 self.owner = self.land_manager.owner 50 self.settlement = self.land_manager.settlement 51 self.plan = {} # {(x, y): (purpose, subclass specific data), ...}
52 53 @classmethod
54 - def load(cls, db, settlement_manager):
55 self = cls.__new__(cls) 56 self._load(db, settlement_manager) 57 return self
58
59 - def _load(self, db, settlement_manager, worldid):
60 self.__init(settlement_manager) 61 super().load(db, worldid)
62
63 - def iter_neighbor_tiles(self, rect):
64 """Iterate over the tiles that share a side with the given Rect.""" 65 moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 66 for x, y in rect.tuple_iter(): 67 for dx, dy in moves: 68 coords = (x + dx, y + dy) 69 if not rect.contains_tuple(coords): 70 yield self.island.get_tile_tuple(coords)
71
72 - def iter_possible_road_coords(self, rect, blocked_rect):
73 """Iterate over the possible road tiles that share a side with 74 the given Rect and are not in the blocked Rect.""" 75 blocked_coords_set = {coords for coords in blocked_rect.tuple_iter()} 76 for tile in self.iter_neighbor_tiles(rect): 77 if tile is None: 78 continue 79 coords = (tile.x, tile.y) 80 if coords in blocked_coords_set or coords in self.land_manager.coastline or coords not in self.settlement.ground_map: 81 continue 82 if coords in self.land_manager.roads or (coords in self.plan and self.plan[coords][0] == BUILDING_PURPOSE.NONE): 83 yield coords
84 85 @classmethod
86 - def __fill_distance(cls, distance, nodes):
87 """Fill the distance dict with the shortest distance from the starting nodes. 88 89 @param distance: {(x, y): distance, ...} 90 @param nodes: {(x, y): penalty, ...} 91 """ 92 93 moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 94 queue = deque([item for item in distance.items()]) 95 96 while queue: 97 (coords, dist) = queue.popleft() 98 for dx, dy in moves: 99 coords2 = (coords[0] + dx, coords[1] + dy) 100 if coords2 in nodes and coords2 not in distance: 101 distance[coords2] = dist + 1 102 queue.append((coords2, dist + 1))
103
104 - def get_path_nodes(self):
105 """Return a dict {(x, y): penalty, ...} 106 of current and possible future road tiles in the settlement.""" 107 moves = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] 108 109 nodes = {} # {(x, y): penalty, ...} 110 distance_to_road = {} 111 distance_to_boundary = {} 112 for coords in self.plan: 113 if coords not in self.settlement.ground_map or coords in self.land_manager.coastline: 114 continue 115 if self.plan[coords][0] == BUILDING_PURPOSE.NONE: 116 nodes[coords] = 1 117 elif self.plan[coords][0] == BUILDING_PURPOSE.ROAD: 118 nodes[coords] = 1 119 distance_to_road[coords] = 0 120 121 for (dx, dy) in moves: 122 coords2 = (coords[0] + dx, coords[1] + dy) 123 if coords2 not in self.land_manager.production: 124 distance_to_boundary[coords] = 1 125 break 126 127 for coords in self.land_manager.village: 128 if coords in self.land_manager.roads and coords in self.settlement.ground_map: 129 nodes[coords] = 1 130 distance_to_road[coords] = 0 131 for (dx, dy) in moves: 132 coords2 = (coords[0] + dx, coords[1] + dy) 133 if coords2 not in self.land_manager.production: 134 distance_to_boundary[coords] = 1 135 break 136 137 self.__fill_distance(distance_to_road, self.island.path_nodes.nodes) 138 self.__fill_distance(distance_to_boundary, self.island.path_nodes.nodes) 139 140 for coords in nodes: 141 if coords in distance_to_road: 142 distance = distance_to_road[coords] 143 if distance > self.personality.path_road_penalty_threshold: 144 nodes[coords] += self.personality.path_distant_road_penalty 145 elif distance > 0: 146 nodes[coords] += self.personality.path_near_road_constant_penalty + \ 147 (self.personality.path_road_penalty_threshold - distance + 1) * self.personality.path_near_road_linear_penalty 148 else: 149 nodes[coords] += self.personality.path_unreachable_road_penalty 150 151 if coords in distance_to_boundary: 152 distance = distance_to_boundary[coords] 153 if 1 < distance <= self.personality.path_boundary_penalty_threshold: 154 nodes[coords] += self.personality.path_near_boundary_constant_penalty + \ 155 (self.personality.path_boundary_penalty_threshold - distance + 1) * self.personality.path_near_boundary_linear_penalty 156 else: 157 nodes[coords] += self.personality.path_unreachable_boundary_penalty 158 159 return nodes
160
161 - def _get_road_to_builder(self, builder):
162 """Return a path from the builder to a building with general 163 collectors (None if impossible).""" 164 loading_area = builder.get_loading_area() 165 collector_coords = set() 166 for building in self.collector_buildings: 167 if loading_area.distance(building.position) == 1: 168 return [] 169 if loading_area.distance(building.position) > building.radius: 170 continue # the collector building is too far to be useful 171 for coords in self.iter_possible_road_coords(building.position, building.position): 172 collector_coords.add(coords) 173 174 destination_coords = set(self.iter_possible_road_coords(loading_area, builder.position)) 175 if self is self.settlement_manager.production_builder: 176 if not self.settlement_manager.production_builder.road_connectivity_cache.is_connection_possible(collector_coords, destination_coords): 177 return None 178 179 blocked_coords = {coords for coords in builder.position.tuple_iter()}.union(self.land_manager.coastline) 180 beacon = Rect.init_from_borders(loading_area.left - 1, loading_area.top - 1, 181 loading_area.right + 1, loading_area.bottom + 1) 182 183 return RoadPlanner()(self.owner.personality_manager.get('RoadPlanner'), collector_coords, 184 destination_coords, beacon, self.get_path_nodes(), blocked_coords=blocked_coords)
185
186 - def build_road(self, path):
187 """Build the road given a valid path or None. 188 Return True if it worked, False if the path was None.""" 189 if path is not None: 190 for x, y in path: 191 self.register_change_list([(x, y)], BUILDING_PURPOSE.ROAD, None) 192 building = self.island.ground_map[(x, y)].object 193 if building is not None and building.id == BUILDINGS.TRAIL: 194 continue 195 assert BasicBuilder(BUILDINGS.TRAIL, (x, y), 0).execute(self.land_manager) 196 return path is not None
197
198 - def build_road_connection(self, builder):
199 """Build a road connecting the builder to a building with general collectors. 200 201 Return True if it worked, False if the path was None.""" 202 path = self._get_road_to_builder(builder) 203 return self.build_road(path)
204
205 - def get_road_cost(self, path):
206 """Return the cost of building a road on the given path as {resource_id: amount, ...} or None if impossible.""" 207 if path is None: 208 return None 209 length = 0 210 if path is not None: 211 for x, y in path: 212 building = self.island.ground_map[(x, y)].object 213 if building is None or building.id != BUILDINGS.TRAIL: 214 length += 1 215 if length == 0: 216 return {} 217 costs = copy.copy(Entities.buildings[BUILDINGS.TRAIL].costs) 218 for resource in costs: 219 costs[resource] *= length 220 return costs
221
222 - def get_road_connection_cost(self, builder):
223 """Return the cost of building a road from the builder to a building with general collectors. 224 225 The returned format is {resource_id: amount, ...} if it is possible to build a road and None otherwise. 226 """ 227 return self.get_road_cost(self._get_road_to_builder(builder))
228
229 - def have_resources(self, building_id):
230 """Return a boolean showing whether we currently have the resources to build a building of the given type.""" 231 return Entities.buildings[building_id].have_resources([self.settlement], self.owner)
232
233 - def build_best_option(self, options, purpose):
234 """Try to build the highest valued option. 235 Return a BUILD_RESULT constant showing how it went. 236 237 @param options: [(value, builder), ...] 238 @param purpose: a BUILDING_PURPOSE constant 239 """ 240 241 if not options: 242 return BUILD_RESULT.IMPOSSIBLE 243 244 best_index = 0 245 best_value = options[0][0] 246 for i in range(1, len(options)): 247 if options[i][0] > best_value: 248 best_index = i 249 best_value = options[i][0] 250 251 builder = options[best_index][1] 252 if not builder.execute(self.land_manager): 253 return BUILD_RESULT.UNKNOWN_ERROR 254 self.register_change_list(list(builder.position.tuple_iter()), BUILDING_PURPOSE.RESERVED, None) 255 self.register_change_list([builder.position.origin.to_tuple()], purpose, None) 256 return BUILD_RESULT.OK
257
258 - def extend_settlement(self, position):
259 """Build a storage to extend the settlement towards the given position. 260 261 Return a BUILD_RESULT constant.""" 262 return self.settlement_manager.production_builder.extend_settlement_with_storage(position)
263
264 - def handle_lost_area(self, coords_list):
265 """Handle losing the potential land in the given coordinates list.""" 266 # remove the affected tiles from the plan 267 for coords in coords_list: 268 if coords in self.plan: 269 del self.plan[coords]
270
271 - def add_building(self, building):
272 """Called when a new building is added in the area (the building already exists during the call).""" 273 self.display()
274
275 - def remove_building(self, building):
276 """Called when a building is removed from the area (the building still exists during the call).""" 277 self.display()
278
279 - def display(self):
280 """Show the plan on the map unless it is disabled in the settings.""" 281 raise NotImplementedError('This function has to be overridden.')
282
283 - def _init_cache(self):
284 """Initialize the cache that knows the last time the buildability of a rectangle may have changed in this area.""" 285 self.last_change_id = -1
286
287 - def register_change(self, x, y, purpose, data):
288 """Register the (potential) change of the purpose of land at the given coordinates.""" 289 if (x, y) in self.plan: 290 self.plan[(x, y)] = (purpose, data) 291 if purpose == BUILDING_PURPOSE.ROAD: 292 self.land_manager.roads.add((x, y))
293
294 - def register_change_list(self, coords_list, purpose, data):
295 for (x, y) in coords_list: 296 self.register_change(x, y, purpose, data)
297