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

Source Code for Module horizons.ai.aiplayer.landmanager

  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.storagecomponent import StorageComponent 
 27  from horizons.constants import AI, BUILDINGS, RES 
 28  from horizons.util.worldobject import WorldObject 
29 30 31 -class LandManager(WorldObject):
32 """ 33 Divides and manages the division of the land of one island. 34 35 The idea is that the LandManager object divides the land of the island between 36 different purposes (currently the production area and on non-feeder islands the 37 village area) and from that point on the different area managers are limited to that 38 land unless they decide to give some of it up (currently happens with the village area). 39 """ 40 41 log = logging.getLogger("ai.aiplayer.land_manager") 42
43 - class purpose:
44 production = 0 45 village = 1
46
47 - def __init__(self, island, owner, feeder_island):
48 """ 49 @param island: Island instance 50 @param owner: AIPlayer instance 51 @param feeder_island: boolean showing whether this is a feeder island (no village area) 52 """ 53 54 super().__init__() 55 self.__init(island, owner, feeder_island) 56 if self.feeder_island: 57 self._prepare_feeder_island() 58 else: 59 self._divide_island()
60
61 - def __init(self, island, owner, feeder_island):
62 self.island = island 63 self.settlement = None 64 self.owner = owner 65 self.feeder_island = feeder_island 66 self.session = self.island.session 67 self.production = {} 68 self.village = {} 69 self.roads = set() # set((x, y), ...) of coordinates where road can be built independent of the area purpose 70 self.coastline = self._get_coastline() # set((x, y), ...) of coordinates which coastal buildings could use in the production area 71 self.personality = self.owner.personality_manager.get('LandManager') 72 self.refresh_resource_deposits()
73
74 - def save(self, db):
75 super().save(db) 76 db("INSERT INTO ai_land_manager(rowid, owner, island, feeder_island) VALUES(?, ?, ?, ?)", self.worldid, 77 self.owner.worldid, self.island.worldid, self.feeder_island) 78 for (x, y) in self.production: 79 db("INSERT INTO ai_land_manager_coords(land_manager, x, y, purpose) VALUES(?, ?, ?, ?)", 80 self.worldid, x, y, self.purpose.production) 81 for (x, y) in self.village: 82 db("INSERT INTO ai_land_manager_coords(land_manager, x, y, purpose) VALUES(?, ?, ?, ?)", 83 self.worldid, x, y, self.purpose.village)
84 85 @classmethod
86 - def load(cls, db, owner, worldid):
87 self = cls.__new__(cls) 88 self._load(db, owner, worldid) 89 return self
90
91 - def _load(self, db, owner, worldid):
92 super().load(db, worldid) 93 island_id, feeder_island = db("SELECT island, feeder_island FROM ai_land_manager WHERE rowid = ?", worldid)[0] 94 self.__init(WorldObject.get_object_by_id(island_id), owner, feeder_island) 95 96 for x, y, purpose in db("SELECT x, y, purpose FROM ai_land_manager_coords WHERE land_manager = ?", self.worldid): 97 coords = (x, y) 98 if purpose == self.purpose.production: 99 self.production[coords] = self.island.ground_map[coords] 100 elif purpose == self.purpose.village: 101 self.village[coords] = self.island.ground_map[coords]
102
103 - def _get_coastline(self):
104 result = set() 105 for coords in self.island.ground_map: 106 tile = self.island.ground_map[coords] 107 if 'coastline' not in tile.classes: 108 continue 109 if tile.object is not None and not tile.object.buildable_upon: 110 continue 111 if tile.settlement is not None and tile.settlement.owner is not self.owner: 112 continue 113 result.add(coords) 114 return result
115
117 self.resource_deposits = defaultdict(list) # {resource_id: [tile, ...]} all resource deposits of a type on the island 118 for resource_id, building_ids in {RES.RAW_CLAY: [BUILDINGS.CLAY_DEPOSIT, BUILDINGS.CLAY_PIT], RES.RAW_IRON: [BUILDINGS.MOUNTAIN, BUILDINGS.MINE], 119 RES.STONE_DEPOSIT: [BUILDINGS.STONE_DEPOSIT, BUILDINGS.STONE_PIT]}.items(): 120 for building in self.island.buildings: 121 if building.id in building_ids: 122 if building.get_component(StorageComponent).inventory[resource_id] > 0: 123 self.resource_deposits[resource_id].append(self.island.ground_map[building.position.origin.to_tuple()])
124
125 - def _divide_island(self):
126 """Divide the whole island between the purposes. The proportions depend on the personality.""" 127 min_x, max_x = None, None 128 min_y, max_y = None, None 129 land = 0 130 for x, y in self.island.ground_map: 131 if self.coords_usable((x, y)): 132 land += 1 133 if min_x is None or x < min_x: 134 min_x = x 135 if max_x is None or x > max_x: 136 max_x = x 137 if min_y is None or y < min_y: 138 min_y = y 139 if max_y is None or y > max_y: 140 max_y = y 141 width = max_x - min_x + 1 142 height = max_y - min_y + 1 143 self.log.info('%s island width %d, height %d', self, width, height) 144 145 village_area = self.personality.village_area_small 146 if land > 60 * 60: 147 village_area = self.personality.village_area_60 148 elif land > 50 * 50: 149 village_area = self.personality.village_area_50 150 elif land > 40 * 40: 151 village_area = self.personality.village_area_40 152 chosen_area = max(self.personality.min_village_size, int(round(land * village_area))) 153 min_village_area = int(round(chosen_area * self.personality.min_village_proportion)) 154 self.log.info('%s land %d, village area %.2f, chosen area %d, minimum preliminary village area %d', self, land, village_area, chosen_area, min_village_area) 155 156 side = int(math.floor(math.sqrt(chosen_area))) 157 if side <= self.personality.max_section_side: 158 side = min(side, width) 159 self._divide(side, chosen_area // side) 160 else: 161 best_sections = 1000 162 best_side1 = None 163 best_side2 = None 164 165 for side1 in range(9, max(10, chosen_area // 9 + 1)): 166 real_side1 = min(side1, width) 167 real_side2 = min(chosen_area // real_side1, height) 168 if real_side1 * real_side2 < min_village_area: 169 continue 170 171 horizontal_sections = int(math.ceil(float(real_side1) / self.personality.max_section_side)) 172 vertical_sections = int(math.ceil(float(real_side2) / self.personality.max_section_side)) 173 sections = horizontal_sections * vertical_sections 174 if best_sections > sections or (best_sections == sections and abs(real_side1 - real_side2) < abs(best_side1 - best_side2)): 175 best_sections = sections 176 best_side1 = real_side1 177 best_side2 = real_side2 178 self._divide(best_side1, best_side2)
179
180 - def coords_usable(self, coords, use_coast=False):
181 """Return a boolean showing whether the land on the given coordinate is usable for a normal building.""" 182 if coords in self.island.ground_map: 183 tile = self.island.ground_map[coords] 184 if use_coast: 185 if 'constructible' not in tile.classes and 'coastline' not in tile.classes: 186 return False 187 elif 'constructible' not in tile.classes: 188 return False 189 if tile.object is not None and not tile.object.buildable_upon: 190 return False 191 return tile.settlement is None or tile.settlement.owner is self.owner 192 return False
193
194 - def legal_for_production(self, rect):
195 """Return a boolean showing whether every tile in the Rect is either in the production area or on the coast.""" 196 for coords in rect.tuple_iter(): 197 if coords in self.village: 198 return False 199 return True
200
201 - def _get_usability_map(self, extra_space):
202 """ 203 Return a tuple describing the usability of the island. 204 205 The return format is ({x, y): usable, ..}, min_x - extra_space, max_x, min_y - extra_space, max_y) 206 where the dict contains ever key for x in [min_x, max_x] and y in [min_y, max_y] and the 207 usability value says whether we can use that part of the land for normal buildings. 208 """ 209 210 map = {} 211 for coords, tile in self.island.ground_map.items(): 212 if 'constructible' not in tile.classes: 213 continue 214 if tile.object is not None and not tile.object.buildable_upon: 215 continue 216 if tile.settlement is None or tile.settlement.owner == self.owner: 217 map[coords] = 1 218 219 xs, ys = list(zip(*map.keys())) 220 min_x = min(xs) - extra_space 221 max_x = max(xs) 222 min_y = min(ys) - extra_space 223 max_y = max(ys) 224 225 for x in range(min_x, max_x + 1): 226 for y in range(min_y, max_y + 1): 227 coords = (x, y) 228 if coords not in map: 229 map[coords] = 0 230 return (map, min_x, max_x, min_y, max_y)
231
232 - def _divide(self, side1, side2):
233 """Divide the total land area between different purposes trying to achieve a side1 x side2 rectangle for the village.""" 234 usability_map, min_x, max_x, min_y, max_y = self._get_usability_map(max(side1, side2)) 235 self.log.info('%s divide %d x %d', self, side1, side2) 236 237 best_coords = (0, 0) 238 best_buildable = 0 239 best_sides = (None, None) 240 241 sizes = [(side1, side2)] 242 if side1 != side2: 243 sizes.append((side2, side1)) 244 245 for width, height in sizes: 246 horizontal_strip = {} # (x, y): number of usable tiles from (x - width + 1, y) to (x, y) 247 usable_area = {} # (x, y): number of usable tiles from (x - width + 1, y - height + 1) to (x, y) 248 for x in range(min_x, max_x + 1): 249 for dy in range(height): 250 horizontal_strip[(x, min_y + dy)] = 0 251 usable_area[(x, min_y + dy)] = 0 252 for y in range(min_y, max_y + 1): 253 for dx in range(width): 254 horizontal_strip[(min_x + dx, y)] = 0 255 usable_area[(min_x + dx, y)] = 0 256 257 for y in range(min_y + height, max_y + 1): 258 for x in range(min_x + width, max_x + 1): 259 horizontal_strip[(x, y)] = horizontal_strip[(x - 1, y)] + usability_map[(x, y)] - usability_map[(x - width, y)] 260 261 for x in range(min_x + width, max_x + 1): 262 for y in range(min_y + height, max_y + 1): 263 coords = (x, y) 264 usable_area[coords] = usable_area[(x, y - 1)] + horizontal_strip[(x, y)] - horizontal_strip[(x, y - height)] 265 266 if usable_area[coords] > best_buildable: 267 best_coords = (x - width + 1, y - height + 1) 268 best_buildable = usable_area[coords] 269 best_sides = (width, height) 270 271 self.production = {} 272 self.village = {} 273 274 for dx in range(best_sides[0]): 275 for dy in range(best_sides[1]): 276 coords = (best_coords[0] + dx, best_coords[1] + dy) 277 if usability_map[coords] == 1: 278 self.village[coords] = self.island.ground_map[coords] 279 280 for coords, tile in self.island.ground_map.items(): 281 if coords not in self.village and self.coords_usable(coords, use_coast=True): 282 self.production[coords] = tile
283
284 - def _prepare_feeder_island(self):
285 """Assign all the usable land of the island to the production area.""" 286 self.production = {} 287 self.village = {} 288 for coords, tile in self.island.ground_map.items(): 289 if self.coords_usable(coords, use_coast=True): 290 self.production[coords] = tile
291
292 - def add_to_production(self, coords):
293 """Assign a current village tile to the production area.""" 294 self.production[coords] = self.village[coords] 295 del self.village[coords]
296
297 - def handle_lost_area(self, coords_list):
298 """Handle losing the potential land in the given coordinates list.""" 299 # reduce the areas for the village, production, roads, and coastline 300 for coords in coords_list: 301 if coords in self.village: 302 del self.village[coords] 303 elif coords in self.production: 304 del self.production[coords] 305 self.roads.discard(coords) 306 self.coastline.discard(coords)
307
308 - def display(self):
309 """Show the plan on the map unless it is disabled in the settings.""" 310 if not AI.HIGHLIGHT_PLANS: 311 return 312 313 village_color = (255, 255, 255) 314 production_color = (255, 255, 0) 315 coastline_color = (0, 0, 255) 316 renderer = self.island.session.view.renderer['InstanceRenderer'] 317 318 for tile in self.production.values(): 319 renderer.addColored(tile._instance, *production_color) 320 321 for tile in self.village.values(): 322 renderer.addColored(tile._instance, *village_color) 323 324 for coords in self.coastline: 325 renderer.addColored(self.island.ground_map[coords]._instance, *coastline_color)
326
327 - def __str__(self):
328 return '{} LandManager({})'.format( 329 getattr(self, 'owner', 'unknown player'), 330 getattr(self, 'worldid', 'none'))
331