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

Source Code for Module horizons.ai.aiplayer.building.farm

  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 collections import defaultdict 
 23  from typing import Dict, List, Tuple 
 24   
 25  from horizons.ai.aiplayer.basicbuilder import BasicBuilder 
 26  from horizons.ai.aiplayer.building import AbstractBuilding 
 27  from horizons.ai.aiplayer.buildingevaluator import BuildingEvaluator 
 28  from horizons.ai.aiplayer.constants import BUILD_RESULT, BUILDING_PURPOSE 
 29  from horizons.constants import BUILDINGS, RES 
 30  from horizons.world.buildability.terraincache import TerrainRequirement 
31 32 33 -class FarmOptionCache:
34 - def __init__(self, settlement_manager):
35 self.settlement_manager = settlement_manager 36 abstract_farm = AbstractBuilding.buildings[BUILDINGS.FARM] 37 self.field_spots_set = abstract_farm._get_buildability_intersection(settlement_manager, (3, 3), TerrainRequirement.LAND, False) 38 self.farm_spots_set = self.field_spots_set.intersection(settlement_manager.production_builder.simple_collector_area_cache.cache[(3, 3)]) 39 self.road_spots_set = abstract_farm._get_buildability_intersection(settlement_manager, (1, 1), TerrainRequirement.LAND, False).union(settlement_manager.land_manager.roads) 40 self.raw_options = self._get_raw_options(self.farm_spots_set, self.field_spots_set, self.road_spots_set) 41 self.max_fields = self._get_max_fields() 42 self._positive_alignment = None
43
44 - def _get_raw_options(self, farm_spots_set, field_spots_set, road_spots_set):
45 field_row3 = {} 46 field_col3 = {} 47 48 for coords in farm_spots_set: 49 x, y = coords 50 row_score = 1 51 if (x - 3, y) in field_spots_set: 52 row_score += 1 53 if (x + 3, y) in field_spots_set: 54 row_score += 1 55 field_row3[coords] = row_score 56 57 col_score = 1 58 if (x, y - 3) in field_spots_set: 59 col_score += 1 60 if (x, y + 3) in field_spots_set: 61 col_score += 1 62 field_col3[coords] = col_score 63 64 road_row3 = set() 65 road_col3 = set() 66 for (x, y) in road_spots_set: 67 if (x + 2, y) in road_spots_set and (x + 1, y) in road_spots_set: 68 road_row3.add((x, y)) 69 if (x, y + 2) in road_spots_set and (x, y + 1) in road_spots_set: 70 road_col3.add((x, y)) 71 72 road_row9 = set() 73 for (x, y) in road_row3: 74 if (x - 3, y) in road_row3 and (x + 3, y) in road_row3: 75 road_row9.add((x, y)) 76 77 road_col9 = set() 78 for (x, y) in road_col3: 79 if (x, y - 3) in road_col3 and (x, y + 3) in road_col3: 80 road_col9.add((x, y)) 81 82 raw_options = [] 83 for coords in sorted(farm_spots_set): 84 x, y = coords 85 86 row_score = field_row3[coords] - 1 87 if (x, y - 1) in road_row9: 88 score = row_score 89 if (x, y - 4) in field_row3: 90 score += field_row3[(x, y - 4)] 91 if (x, y + 3) in field_row3: 92 score += field_row3[(x, y + 3)] 93 if score > 0: 94 raw_options.append((score, coords, 0)) 95 if (x, y + 3) in road_row9: 96 score = row_score 97 if (x, y - 3) in field_row3: 98 score += field_row3[(x, y - 3)] 99 if (x, y + 4) in field_row3: 100 score += field_row3[(x, y + 4)] 101 if score > 0: 102 raw_options.append((score, coords, 1)) 103 104 col_score = field_col3[coords] - 1 105 if (x - 1, y) in road_col9: 106 score = col_score 107 if (x - 4, y) in field_col3: 108 score += field_col3[(x - 4, y)] 109 if (x + 3, y) in field_col3: 110 score += field_col3[(x + 3, y)] 111 if score > 0: 112 raw_options.append((score, coords, 2)) 113 if (x + 3, y) in road_col9: 114 score = col_score 115 if (x - 3, y) in field_col3: 116 score += field_col3[(x - 3, y)] 117 if (x + 4, y) in field_col3: 118 score += field_col3[(x + 4, y)] 119 if score > 0: 120 raw_options.append((score, coords, 3)) 121 122 return raw_options
123
124 - def _get_max_fields(self):
125 max_fields = 0 126 for (num_fields, _, _) in self.raw_options: 127 if num_fields > max_fields: 128 max_fields = num_fields 129 return max_fields
130
131 - def get_positive_alignment(self):
132 if self._positive_alignment is None: 133 land_manager = self.settlement_manager.land_manager 134 village_builder = self.settlement_manager.village_builder 135 positive_alignment = land_manager.coastline.union(land_manager.roads, iter(village_builder.plan.keys())) 136 production_builder_plan = self.settlement_manager.production_builder.plan 137 for (coords, purpose) in production_builder_plan: 138 if purpose != BUILDING_PURPOSE.NONE: 139 positive_alignment.add(coords) 140 self._positive_alignment = positive_alignment 141 return self._positive_alignment
142
143 144 -class AbstractFarm(AbstractBuilding):
145 @property
146 - def directly_buildable(self):
147 """ farms have to be triggered by fields """ 148 return False
149 150 @property
151 - def evaluator_class(self):
152 return FarmEvaluator
153
154 - def get_expected_cost(self, resource_id, production_needed, settlement_manager):
155 """ the fields have to take into account the farm cost """ 156 return 0
157 158 @classmethod
159 - def get_purpose(cls, resource_id):
167
168 - def get_evaluators(self, settlement_manager, resource_id):
169 options_cache = self._get_option_cache(settlement_manager) 170 raw_options = options_cache.raw_options 171 if not raw_options: 172 return [] 173 174 farm_field_buckets = [] 175 for _ in range(9): 176 farm_field_buckets.append([]) 177 178 for option in raw_options: 179 farm_field_buckets[option[0]].append(option) 180 181 personality = settlement_manager.owner.personality_manager.get('FarmEvaluator') 182 options_left = personality.max_options 183 chosen_raw_options = [] 184 for i in range(8, 0, -1): 185 if len(farm_field_buckets[i]) > options_left: 186 chosen_raw_options.extend(settlement_manager.session.random.sample(farm_field_buckets[i], options_left)) 187 options_left = 0 188 else: 189 chosen_raw_options.extend(farm_field_buckets[i]) 190 options_left -= len(farm_field_buckets[i]) 191 if options_left == 0: 192 break 193 194 max_fields = options_cache.max_fields 195 field_spots_set = options_cache.field_spots_set 196 road_spots_set = options_cache.road_spots_set 197 positive_alignment = options_cache.get_positive_alignment() 198 production_builder = settlement_manager.production_builder 199 field_purpose = self.get_purpose(resource_id) 200 road_configs = [(0, -1), (0, 3), (-1, 0), (3, 0)] 201 options = [] 202 203 # create evaluators for completely new farms 204 for (_, (x, y), road_config) in chosen_raw_options: 205 road_dx, road_dy = road_configs[road_config] 206 evaluator = FarmEvaluator.create(production_builder, x, y, road_dx, road_dy, max_fields, field_purpose, field_spots_set, road_spots_set, positive_alignment) 207 if evaluator is not None: 208 options.append(evaluator) 209 210 # create evaluators for modified farms (change unused field type) 211 for coords_list in production_builder.unused_fields.values(): 212 for x, y in coords_list: 213 evaluator = ModifiedFieldEvaluator.create(production_builder, x, y, field_purpose) 214 if evaluator is not None: 215 options.append(evaluator) 216 return options
217 218 __cache = {} # type: Dict[int, Tuple[Tuple[int, int], FarmOptionCache]] 219
220 - def _get_option_cache(self, settlement_manager):
221 production_builder = settlement_manager.production_builder 222 current_cache_changes = (production_builder.island.last_change_id, production_builder.last_change_id) 223 224 worldid = settlement_manager.worldid 225 if worldid in self.__cache and self.__cache[worldid][0] != current_cache_changes: 226 del self.__cache[worldid] 227 228 if worldid not in self.__cache: 229 self.__cache[worldid] = (current_cache_changes, FarmOptionCache(settlement_manager)) 230 return self.__cache[worldid][1]
231 232 @classmethod
233 - def clear_cache(cls):
234 cls.__cache.clear()
235
236 - def get_max_fields(self, settlement_manager):
237 return self._get_option_cache(settlement_manager).max_fields
238 239 @classmethod
240 - def register_buildings(cls):
242
243 244 -class FarmEvaluator(BuildingEvaluator):
245 __field_pos_offsets = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)] 246 __moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 247 __field_offsets = None # type: List[Tuple[int, int]] 248 249 __slots__ = ('farm_plan', 'field_purpose') 250
251 - def __init__(self, area_builder, builder, value, farm_plan, fields, field_purpose):
252 super().__init__(area_builder, builder, value) 253 self.farm_plan = farm_plan 254 self.field_purpose = field_purpose
255 256 @classmethod
257 - def init_field_offsets(cls):
258 # right next to the farm 259 first_class = [(-3, -3), (-3, 0), (-3, 3), (0, -3), (0, 3), (3, -3), (3, 0), (3, 3)] 260 # offset by a road right next to the farm 261 second_class = [(-4, -3), (-4, 0), (-4, 3), (-3, -4), (-3, 4), (0, -4), (0, 4), (3, -4), (3, 4), (4, -3), (4, 0), (4, 3)] 262 # offset by crossing roads 263 third_class = [(-4, -4), (-4, 4), (4, -4), (4, 4)] 264 cls.__field_offsets = first_class + second_class + third_class
265 266 @classmethod
267 - def _suitable_for_road(cls, production_builder, coords):
268 """check coordinates""" 269 return coords in production_builder.land_manager.roads or ( 270 coords in production_builder.plan and 271 production_builder.plan[coords][0] == BUILDING_PURPOSE.NONE)
272 273 @classmethod
274 - def create(cls, area_builder, farm_x, farm_y, road_dx, road_dy, min_fields, field_purpose, field_spots_set, road_spots_set, positive_alignment):
275 farm_plan = {} 276 277 # place the farm area road 278 existing_roads = 0 279 for other_offset in range(-3, 6): 280 coords = None 281 if road_dx == 0: 282 coords = (farm_x + other_offset, farm_y + road_dy) 283 else: 284 coords = (farm_x + road_dx, farm_y + other_offset) 285 assert coords in road_spots_set 286 287 farm_plan[coords] = BUILDING_PURPOSE.ROAD 288 if coords in area_builder.land_manager.roads: 289 existing_roads += 1 290 291 # place the fields 292 fields = 0 293 for (dx, dy) in cls.__field_offsets: 294 if fields >= 8: 295 break # unable to place more anyway 296 coords = (farm_x + dx, farm_y + dy) 297 if coords not in field_spots_set: 298 continue 299 300 field_fits = True 301 for (fdx, fdy) in cls.__field_pos_offsets: 302 coords2 = (coords[0] + fdx, coords[1] + fdy) 303 if coords2 in farm_plan: 304 field_fits = False 305 break 306 if not field_fits: 307 continue # some part of the area is reserved for something else 308 309 fields += 1 310 for (fdx, fdy) in cls.__field_pos_offsets: 311 coords2 = (coords[0] + fdx, coords[1] + fdy) 312 farm_plan[coords2] = BUILDING_PURPOSE.RESERVED 313 farm_plan[coords] = field_purpose 314 if fields < min_fields: 315 return None # go for the most fields possible 316 317 # add the farm itself to the plan 318 builder = BasicBuilder.create(BUILDINGS.FARM, (farm_x, farm_y), 0) 319 for coords in builder.position.tuple_iter(): 320 farm_plan[coords] = BUILDING_PURPOSE.RESERVED 321 farm_plan[(farm_x, farm_y)] = BUILDING_PURPOSE.FARM 322 323 # calculate the alignment value and the rectangle that contains the whole farm 324 alignment = 0 325 min_x, max_x, min_y, max_y = None, None, None, None 326 for x, y in farm_plan: 327 min_x = x if min_x is None or min_x > x else min_x 328 max_x = x if max_x is None or max_x < x else max_x 329 min_y = y if min_y is None or min_y > y else min_y 330 max_y = y if max_y is None or max_y < y else max_y 331 332 for dx, dy in cls.__moves: 333 coords = (x + dx, y + dy) 334 if coords not in farm_plan and coords in positive_alignment: 335 alignment += 1 336 337 # calculate the value of the farm road end points (larger is better) 338 personality = area_builder.owner.personality_manager.get('FarmEvaluator') 339 immediate_connections = 0 340 for other_offset in [-4, 6]: 341 if road_dx == 0: 342 coords = (farm_x + other_offset, farm_y + road_dy) 343 else: 344 coords = (farm_x + road_dx, farm_y + other_offset) 345 if coords in area_builder.land_manager.roads: 346 immediate_connections += personality.immediate_connection_road 347 elif coords in area_builder.plan: 348 if area_builder.plan[coords][0] == BUILDING_PURPOSE.NONE: 349 immediate_connections += personality.immediate_connection_free 350 351 extra_space = (max_x - min_x + 1) * (max_y - min_y + 1) - 9 * (fields + 2) 352 value = fields + existing_roads * personality.existing_road_importance + \ 353 alignment * personality.alignment_importance - extra_space * personality.wasted_space_penalty + \ 354 immediate_connections * personality.immediate_connection_importance 355 return FarmEvaluator(area_builder, builder, value, farm_plan, fields, field_purpose)
356
357 - def _register_changes(self, changes, just_roads):
358 for (purpose, data), coords_list in changes.items(): 359 if just_roads == (purpose == BUILDING_PURPOSE.ROAD): 360 self.area_builder.register_change_list(coords_list, purpose, data)
361
362 - def execute(self):
363 # cheap resource check first, then pre-reserve the tiles and check again 364 if not self.builder.have_resources(self.area_builder.land_manager): 365 return (BUILD_RESULT.NEED_RESOURCES, None) 366 367 changes = defaultdict(list) 368 reverse_changes = defaultdict(list) 369 for coords, purpose in self.farm_plan.items(): 370 # completely ignore the road in the plan for now 371 if purpose == BUILDING_PURPOSE.ROAD: 372 continue 373 assert coords not in self.area_builder.land_manager.roads 374 375 changes[(purpose, None)].append(coords) 376 reverse_changes[self.area_builder.plan[coords]].append(coords) 377 self._register_changes(changes, False) 378 379 resource_check = self.have_resources() 380 if resource_check is None: 381 self._register_changes(reverse_changes, False) 382 self.log.debug('%s, unable to reach by road', self) 383 return (BUILD_RESULT.IMPOSSIBLE, None) 384 elif not resource_check: 385 self._register_changes(reverse_changes, False) 386 return (BUILD_RESULT.NEED_RESOURCES, None) 387 assert self.area_builder.build_road_connection(self.builder) 388 389 building = self.builder.execute(self.area_builder.land_manager) 390 if not building: 391 # TODO: make sure the plan and the reality stay in a reasonable state 392 # the current code makes the plan look as if everything was built but in reality 393 # a farm may be missing if there was not enough money after building the road. 394 self.log.debug('%s, unknown error', self) 395 return (BUILD_RESULT.UNKNOWN_ERROR, None) 396 397 for coords, purpose in self.farm_plan.items(): 398 if purpose == self.field_purpose: 399 self.area_builder.unused_fields[self.field_purpose].append(coords) 400 self._register_changes(changes, True) 401 return (BUILD_RESULT.OK, building)
402
403 404 -class ModifiedFieldEvaluator(BuildingEvaluator):
405 """This evaluator evaluates the cost of changing the type of an unused field.""" 406 407 __slots__ = ('_old_field_purpose') 408
409 - def __init__(self, area_builder, builder, value, old_field_purpose):
410 super().__init__(area_builder, builder, value) 411 self._old_field_purpose = old_field_purpose
412 413 @classmethod
414 - def create(cls, area_builder, x, y, new_field_purpose):
443
444 - def execute(self):
445 if not self.builder.have_resources(self.area_builder.land_manager): 446 return (BUILD_RESULT.NEED_RESOURCES, None) 447 448 building = self.builder.execute(self.area_builder.land_manager) 449 if not building: 450 self.log.debug('%s, unknown error', self) 451 return (BUILD_RESULT.UNKNOWN_ERROR, None) 452 453 # remove the old designation 454 self.area_builder.unused_fields[self._old_field_purpose].remove(self.builder.position.origin.to_tuple()) 455 456 return (BUILD_RESULT.OK, building)
457 458 459 AbstractFarm.register_buildings() 460 FarmEvaluator.init_field_offsets() 461