def _init_buildability_cache(self): self.buildability_cache = BinaryBuildabilityCache(self.island.terrain_cache) free_coords_set = set() for coords, (purpose, _) in self.plan.iteritems(): if purpose == BUILDING_PURPOSE.NONE: free_coords_set.add(coords) for coords in self.land_manager.coastline: free_coords_set.add(coords) self.buildability_cache.add_area(free_coords_set)
def _init_buildability_cache(self): self.buildability_cache = BinaryBuildabilityCache(self.island.terrain_cache) free_coords_set = set() for coords, (purpose, _) in self.plan.iteritems(): if purpose == BUILDING_PURPOSE.NONE: free_coords_set.add(coords) for coords in self.land_manager.coastline: free_coords_set.add(coords) self.buildability_cache.add_area(free_coords_set)
class FreeIslandBuildabilityCache(object): """ An instance of this class is used to keep track of the unclaimed area on an island. Instances of this class can answer the same queries as BinaryBuildabilityCache. It is specialized for keeping track of the unclaimed land on an island. That way it is possible to use it in conjunction with the TerrainCache to find all available warehouse positions on an island. Note that the cache is initialized with all unclaimed tiles on the island and after that it can only reduce in size because it is currently impossible for land to change ownership after it has been claimed by the first player. """ def __init__(self, island): self._binary_cache = BinaryBuildabilityCache(island.terrain_cache) self.cache = self._binary_cache.cache # {(width, height): set((x, y), ...), ...} self.island = island self._init() def _init(self): land_or_coast = self._binary_cache.terrain_cache.land_or_coast coords_list = [] for (coords, tile) in self.island.ground_map.iteritems(): if coords not in land_or_coast: continue if tile.settlement is not None: continue if tile.object is not None and not tile.object.buildable_upon: continue coords_list.append(coords) self._binary_cache.add_area(coords_list) def remove_area(self, coords_list): """Remove a list of existing coordinates from the area.""" clean_list = [] for coords in coords_list: if coords in self._binary_cache.coords_set: clean_list.append(coords) self._binary_cache.remove_area(clean_list)
def __init__(self, island): self._binary_cache = BinaryBuildabilityCache(island.terrain_cache) self.cache = self._binary_cache.cache # {(width, height): set((x, y), ...), ...} self.island = island self._init()
class ProductionBuilder(AreaBuilder): """ An object of this class manages the production area of a settlement. Important attributes: * plan: a dictionary of the form {(x, y): (purpose, extra data), ...} where purpose is one of the BUILDING_PURPOSE constants. Coordinates being in the plan means that the tile doesn't belong to another player. * collector_buildings: a list of every building in the settlement that provides general collectors (warehouse, storages) * production_buildings: a list of buildings in the settlement where productions should be paused and resumed at appropriate times * unused_fields: a dictionary where the key is a BUILDING_PURPOSE constant of a field and the value is a deque that holds the coordinates of unused field spots. {building purpose: deque([(x, y), ...]), ...} * last_collector_improvement_storage: the last tick when a storage was built to improve collector coverage * last_collector_improvement_road: the last tick when a new road connection was built to improve collector coverage """ coastal_building_classes = [ BUILDINGS.FISHER, BUILDINGS.BOAT_BUILDER, BUILDINGS.SALT_PONDS ] def __init__(self, settlement_manager): super(ProductionBuilder, self).__init__(settlement_manager) self.plan = dict.fromkeys(self.land_manager.production, (BUILDING_PURPOSE.NONE, None)) self.__init(settlement_manager, Scheduler().cur_tick, Scheduler().cur_tick) self._init_buildability_cache() self._init_simple_collector_area_cache() self._init_road_connectivity_cache() self.register_change_list( list( settlement_manager.settlement.warehouse.position.tuple_iter()), BUILDING_PURPOSE.WAREHOUSE, None) self._refresh_unused_fields() def __init(self, settlement_manager, last_collector_improvement_storage, last_collector_improvement_road): self._init_cache() self.collector_buildings = [] # [building, ...] self.production_buildings = [] # [building, ...] self.personality = self.owner.personality_manager.get( 'ProductionBuilder') self.last_collector_improvement_storage = last_collector_improvement_storage self.last_collector_improvement_road = last_collector_improvement_road def _init_buildability_cache(self): self.buildability_cache = BinaryBuildabilityCache( self.island.terrain_cache) free_coords_set = set() for coords, (purpose, _) in self.plan.items(): if purpose == BUILDING_PURPOSE.NONE: free_coords_set.add(coords) for coords in self.land_manager.coastline: free_coords_set.add(coords) self.buildability_cache.add_area(free_coords_set) def _init_simple_collector_area_cache(self): self.simple_collector_area_cache = SimpleCollectorAreaCache( self.island.terrain_cache) def _init_road_connectivity_cache(self): self.road_connectivity_cache = PotentialRoadConnectivityCache(self) coords_set = set() for coords in self.plan: coords_set.add(coords) for coords in self.land_manager.roads: coords_set.add(coords) self.road_connectivity_cache.modify_area(list(sorted(coords_set))) def save(self, db): super(ProductionBuilder, self).save(db) translated_last_collector_improvement_storage = self.last_collector_improvement_storage - Scheduler( ).cur_tick # pre-translate for the loading process translated_last_collector_improvement_road = self.last_collector_improvement_road - Scheduler( ).cur_tick # pre-translate for the loading process db( "INSERT INTO ai_production_builder(rowid, settlement_manager, last_collector_improvement_storage, last_collector_improvement_road) VALUES(?, ?, ?, ?)", self.worldid, self.settlement_manager.worldid, translated_last_collector_improvement_storage, translated_last_collector_improvement_road) for (x, y), (purpose, _) in self.plan.items(): db( "INSERT INTO ai_production_builder_plan(production_builder, x, y, purpose) VALUES(?, ?, ?, ?)", self.worldid, x, y, purpose) def _load(self, db, settlement_manager): worldid, last_storage, last_road = \ db("SELECT rowid, last_collector_improvement_storage, last_collector_improvement_road FROM ai_production_builder WHERE settlement_manager = ?", settlement_manager.worldid)[0] super(ProductionBuilder, self)._load(db, settlement_manager, worldid) self.__init(settlement_manager, last_storage, last_road) db_result = db( "SELECT x, y, purpose FROM ai_production_builder_plan WHERE production_builder = ?", worldid) for x, y, purpose in db_result: self.plan[(x, y)] = (purpose, None) if purpose == BUILDING_PURPOSE.ROAD: self.land_manager.roads.add((x, y)) self._init_buildability_cache() self._init_simple_collector_area_cache() self._init_road_connectivity_cache() self._refresh_unused_fields() def have_deposit(self, resource_id): """Returns True if there is a resource deposit of the relevant type inside the settlement.""" for tile in self.land_manager.resource_deposits[resource_id]: if tile.object.settlement is None: continue coords = tile.object.position.origin.to_tuple() if coords in self.settlement.ground_map: return True return False def extend_settlement_with_storage(self, target_position): """Build a storage to extend the settlement towards the given position. Return a BUILD_RESULT constant.""" if not self.have_resources(BUILDINGS.STORAGE): return BUILD_RESULT.NEED_RESOURCES storage_class = Entities.buildings[BUILDINGS.STORAGE] storage_spots = self.island.terrain_cache.get_buildability_intersection( storage_class.terrain_type, storage_class.size, self.settlement.buildability_cache, self.buildability_cache) storage_surrounding_offsets = Rect.get_surrounding_offsets( storage_class.size) coastline = self.land_manager.coastline options = [] for (x, y) in sorted(storage_spots): builder = BasicBuilder.create(BUILDINGS.STORAGE, (x, y), 0) alignment = 1 for (dx, dy) in storage_surrounding_offsets: coords = (x + dx, y + dy) if coords in coastline or coords not in self.plan or self.plan[ coords][0] != BUILDING_PURPOSE.NONE: alignment += 1 distance = distances.distance_rect_rect(target_position, builder.position) value = distance - alignment * 0.7 options.append((-value, builder)) return self.build_best_option(options, BUILDING_PURPOSE.STORAGE) def get_collector_area(self): """Return the set of all coordinates that are reachable from at least one collector by road or open space.""" if self.__collector_area_cache is not None and self.last_change_id == self.__collector_area_cache[ 0]: return self.__collector_area_cache[1] moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] collector_area = set( ) # unused tiles that are reachable from at least one collector for building in self.collector_buildings: coverage_area = set() for coords in building.position.get_radius_coordinates( building.radius, True): coverage_area.add(coords) reachable = set() queue = deque() for coords in building.position.tuple_iter(): reachable.add(coords) queue.append(coords) while queue: x, y = queue.popleft() for dx, dy in moves: coords = (x + dx, y + dy) if coords not in reachable and coords in coverage_area: if coords in self.land_manager.roads or ( coords in self.plan and coords not in self.land_manager.coastline and self.plan[coords][0] == BUILDING_PURPOSE.NONE): queue.append(coords) reachable.add(coords) if coords in self.plan and self.plan[coords][ 0] == BUILDING_PURPOSE.NONE: collector_area.add(coords) self.__collector_area_cache = (self.last_change_id, collector_area) return collector_area def count_available_squares(self, size, max_num=None): """ Count the number of available and usable (covered by collectors) size x size squares. @param size: the square side length @param max_num: if non-None then stop counting once the number of total squares is max_num @return: (available squares, total squares) """ key = (size, max_num) if key in self.__available_squares_cache and self.last_change_id == self.__available_squares_cache[ key][0]: return self.__available_squares_cache[key][1] offsets = list(itertools.product(range(size), range(size))) collector_area = self.get_collector_area() available_squares = 0 total_squares = 0 for x, y in self.plan: ok = True accessible = False for dx, dy in offsets: coords = (x + dx, y + dy) if coords not in self.plan or self.plan[coords][ 0] != BUILDING_PURPOSE.NONE: ok = False break if coords in collector_area: accessible = True if ok: total_squares += 1 if max_num is not None and total_squares >= max_num: break if accessible: available_squares += 1 self.__available_squares_cache[key] = (self.last_change_id, (available_squares, total_squares)) return self.__available_squares_cache[key][1] def _refresh_unused_fields(self): """Refresh the unused_fields object to make sure no impossible fields spots are in the list.""" self.unused_fields = { BUILDING_PURPOSE.POTATO_FIELD: deque(), BUILDING_PURPOSE.PASTURE: deque(), BUILDING_PURPOSE.SUGARCANE_FIELD: deque(), BUILDING_PURPOSE.TOBACCO_FIELD: deque(), BUILDING_PURPOSE.HERBARY: deque(), } for coords, (purpose, _) in sorted(self.plan.items()): usable = True # is every tile of the field spot still usable for new normal buildings for dx in range(3): for dy in range(3): coords2 = (coords[0] + dx, coords[1] + dy) if coords2 not in self.island.ground_map: usable = False else: object = self.island.ground_map[coords2].object if object is not None and not object.buildable_upon: usable = False break if usable and purpose in self.unused_fields: self.unused_fields[purpose].append(coords) def display(self): """Show the plan on the map unless it is disabled in the settings.""" if not AI.HIGHLIGHT_PLANS: return tile_colors = { BUILDING_PURPOSE.ROAD: (30, 30, 30), BUILDING_PURPOSE.FISHER: (128, 128, 128), BUILDING_PURPOSE.LUMBERJACK: (30, 255, 30), BUILDING_PURPOSE.TREE: (0, 255, 0), BUILDING_PURPOSE.FARM: (128, 0, 255), BUILDING_PURPOSE.POTATO_FIELD: (255, 0, 128), BUILDING_PURPOSE.PASTURE: (0, 192, 0), BUILDING_PURPOSE.WEAVER: (0, 64, 64), BUILDING_PURPOSE.SUGARCANE_FIELD: (192, 192, 0), BUILDING_PURPOSE.DISTILLERY: (255, 128, 40), BUILDING_PURPOSE.TOBACCO_FIELD: (64, 64, 0), BUILDING_PURPOSE.TOBACCONIST: (128, 64, 40), BUILDING_PURPOSE.CLAY_PIT: (0, 64, 0), BUILDING_PURPOSE.BRICKYARD: (0, 32, 0), BUILDING_PURPOSE.BOAT_BUILDER: (163, 73, 164), BUILDING_PURPOSE.SALT_PONDS: (153, 217, 234), BUILDING_PURPOSE.HERBARY: (64, 200, 0), BUILDING_PURPOSE.RESERVED: (0, 0, 128), } misc_color = (0, 255, 255) unknown_color = (128, 0, 0) renderer = self.session.view.renderer['InstanceRenderer'] for coords, (purpose, _) in self.plan.items(): tile = self.island.ground_map[coords] color = tile_colors.get(purpose, misc_color) if purpose == BUILDING_PURPOSE.NONE: color = unknown_color renderer.addColored(tile._instance, *color) def _init_cache(self): """Initialize the cache that knows the last time the buildability of a rectangle may have changed in this area.""" super(ProductionBuilder, self)._init_cache() self.__collector_area_cache = None self.__available_squares_cache = {} def register_change(self, x, y, purpose, data): """Register the possible buildability change of a rectangle on this island.""" super(ProductionBuilder, self).register_change(x, y, purpose, data) coords = (x, y) if coords in self.land_manager.village or ( coords not in self.plan and coords not in self.land_manager.coastline): return self.last_change_id += 1 def register_change_list(self, coords_list, purpose, data): add_list = [] remove_list = [] for coords in coords_list: if coords in self.land_manager.village or coords not in self.plan: continue if purpose == BUILDING_PURPOSE.NONE and self.plan[coords][ 0] != BUILDING_PURPOSE.NONE: add_list.append(coords) elif purpose != BUILDING_PURPOSE.NONE and self.plan[coords][ 0] == BUILDING_PURPOSE.NONE: remove_list.append(coords) if add_list: self.buildability_cache.add_area(add_list) if remove_list: self.buildability_cache.remove_area(remove_list) super(ProductionBuilder, self).register_change_list(coords_list, purpose, data) self.road_connectivity_cache.modify_area(coords_list) self.display() def handle_lost_area(self, coords_list): """Handle losing the potential land in the given coordinates list.""" # remove planned fields that are now impossible lost_coords_list = [] for coords in coords_list: if coords in self.plan: lost_coords_list.append(coords) self.register_change_list(lost_coords_list, BUILDING_PURPOSE.NONE, None) field_size = Entities.buildings[BUILDINGS.POTATO_FIELD].size removed_list = [] for coords, (purpose, _) in self.plan.items(): if purpose in [ BUILDING_PURPOSE.POTATO_FIELD, BUILDING_PURPOSE.PASTURE, BUILDING_PURPOSE.SUGARCANE_FIELD, BUILDING_PURPOSE.TOBACCO_FIELD, BUILDING_PURPOSE.HERBARY ]: rect = Rect.init_from_topleft_and_size_tuples( coords, field_size) for field_coords in rect.tuple_iter(): if field_coords not in self.land_manager.production: removed_list.append(coords) break for coords in removed_list: rect = Rect.init_from_topleft_and_size_tuples(coords, field_size) self.register_change_list(list(rect.tuple_iter()), BUILDING_PURPOSE.NONE, None) self._refresh_unused_fields() super(ProductionBuilder, self).handle_lost_area(coords_list) self.road_connectivity_cache.modify_area(lost_coords_list) def handle_new_area(self): """Handle receiving more land to the production area (this can happen when the village area gives some up).""" new_coords_list = [] for coords in self.land_manager.production: if coords not in self.plan: new_coords_list.append(coords) self.register_change_list(new_coords_list, BUILDING_PURPOSE.NONE, None) collector_building_classes = [BUILDINGS.WAREHOUSE, BUILDINGS.STORAGE] field_building_classes = [ BUILDINGS.POTATO_FIELD, BUILDINGS.PASTURE, BUILDINGS.SUGARCANE_FIELD, BUILDINGS.TOBACCO_FIELD ] production_building_classes = { BUILDINGS.FISHER, BUILDINGS.LUMBERJACK, BUILDINGS.FARM, BUILDINGS.CLAY_PIT, BUILDINGS.BRICKYARD, BUILDINGS.WEAVER, BUILDINGS.DISTILLERY, BUILDINGS.MINE, BUILDINGS.SMELTERY, BUILDINGS.TOOLMAKER, BUILDINGS.CHARCOAL_BURNER, BUILDINGS.TOBACCONIST, BUILDINGS.SALT_PONDS } def add_building(self, building): """Called when a new building is added in the area (the building already exists during the call).""" if building.id in self.collector_building_classes: self.collector_buildings.append(building) self.simple_collector_area_cache.add_building(building) elif building.id in self.production_building_classes: self.production_buildings.append(building) super(ProductionBuilder, self).add_building(building) def _handle_lumberjack_removal(self, building): """Release the unused trees around the lumberjack building being removed.""" trees_used_by_others = set() for lumberjack_building in self.settlement.buildings_by_id.get( BUILDINGS.LUMBERJACK, []): if lumberjack_building.worldid == building.worldid: continue for coords in lumberjack_building.position.get_radius_coordinates( lumberjack_building.radius): if coords in self.plan and self.plan[coords][ 0] == BUILDING_PURPOSE.TREE: trees_used_by_others.add(coords) coords_list = [] for coords in building.position.get_radius_coordinates( building.radius): if coords not in trees_used_by_others and coords in self.plan and self.plan[ coords][0] == BUILDING_PURPOSE.TREE: coords_list.append(coords) self.register_change_list(coords_list, BUILDING_PURPOSE.NONE, None) def _handle_farm_removal(self, building): """Handle farm removal by removing planned fields and tearing existing ones that can't be serviced by another farm.""" unused_fields = set() farms = self.settlement.buildings_by_id.get(BUILDINGS.FARM, []) for coords in building.position.get_radius_coordinates( building.radius): if coords not in self.plan: continue object = self.island.ground_map[coords].object if object is None or object.id not in self.field_building_classes: continue used_by_another_farm = False for farm in farms: if farm.worldid != building.worldid and object.position.distance( farm.position) <= farm.radius: used_by_another_farm = True break if not used_by_another_farm: unused_fields.add(object) # tear the finished but no longer used fields down for unused_field in unused_fields: self.register_change_list(list(unused_field.position.tuple_iter()), BUILDING_PURPOSE.NONE, None) Tear(unused_field).execute(self.session) # remove the planned but never built fields from the plan self._refresh_unused_fields() for unused_fields_list in self.unused_fields.values(): for coords in unused_fields_list: position = Rect.init_from_topleft_and_size_tuples( coords, Entities.buildings[BUILDINGS.POTATO_FIELD].size) if building.position.distance(position) > building.radius: continue # it never belonged to the removed building used_by_another_farm = False for farm in farms: if farm.worldid != building.worldid and position.distance( farm.position) <= farm.radius: used_by_another_farm = True break if not used_by_another_farm: self.register_change_list(list(position.tuple_iter()), BUILDING_PURPOSE.NONE, None) self._refresh_unused_fields() def remove_building(self, building): """Called when a building is removed from the area (the building still exists during the call).""" if building.id in self.field_building_classes: # this can't be handled right now because the building still exists Scheduler().add_new_object(Callback(self._refresh_unused_fields), self, run_in=0) Scheduler().add_new_object(Callback( partial( super(ProductionBuilder, self).remove_building, building)), self, run_in=0) elif building.buildable_upon or building.id == BUILDINGS.TRAIL: pass # don't react to road, trees and ruined tents being destroyed else: self.register_change_list(list(building.position.tuple_iter()), BUILDING_PURPOSE.NONE, None) if building.id in self.collector_building_classes: self.collector_buildings.remove(building) self.simple_collector_area_cache.remove_building(building) elif building.id in self.production_building_classes: self.production_buildings.remove(building) if building.id == BUILDINGS.LUMBERJACK: self._handle_lumberjack_removal(building) elif building.id == BUILDINGS.FARM: self._handle_farm_removal(building) super(ProductionBuilder, self).remove_building(building) def manage_production(self): """Pauses and resumes production buildings when they have full input and output inventories.""" for building in self.production_buildings: producer = building.get_component(Producer) for production in producer.get_productions(): if not production.get_produced_resources(): continue all_full = True # inventory full of the produced resources? for resource_id, min_amount in production.get_produced_resources( ).items(): if production.inventory.get_free_space_for( resource_id) >= min_amount: all_full = False break # inventory full of the input resource? if all_full and not isinstance(building, Mine): for resource_id in production.get_consumed_resources(): if production.inventory.get_free_space_for( resource_id) > 0: all_full = False break if all_full: if not production.is_paused(): ToggleActive(producer, production).execute( self.land_manager.session) self.log.info('%s paused a production at %s/%d', self, building.name, building.worldid) else: if production.is_paused(): ToggleActive(producer, production).execute( self.land_manager.session) self.log.info('%s resumed a production at %s/%d', self, building.name, building.worldid) def handle_mine_empty(self, mine): Tear(mine).execute(self.session) self.land_manager.refresh_resource_deposits() def __str__(self): return '%s.PB(%s/%s)' % ( self.owner, self.settlement.get_component(NamedComponent).name if hasattr(self, 'settlement') else 'unknown', self.worldid if hasattr(self, 'worldid') else 'none')
class ProductionBuilder(AreaBuilder): """ An object of this class manages the production area of a settlement. Important attributes: * plan: a dictionary of the form {(x, y): (purpose, extra data), ...} where purpose is one of the BUILDING_PURPOSE constants. Coordinates being in the plan means that the tile doesn't belong to another player. * collector_buildings: a list of every building in the settlement that provides general collectors (warehouse, storages) * production_buildings: a list of buildings in the settlement where productions should be paused and resumed at appropriate times * unused_fields: a dictionary where the key is a BUILDING_PURPOSE constant of a field and the value is a deque that holds the coordinates of unused field spots. {building purpose: deque([(x, y), ...]), ...} * last_collector_improvement_storage: the last tick when a storage was built to improve collector coverage * last_collector_improvement_road: the last tick when a new road connection was built to improve collector coverage """ coastal_building_classes = [BUILDINGS.FISHER, BUILDINGS.BOAT_BUILDER, BUILDINGS.SALT_PONDS] def __init__(self, settlement_manager): super(ProductionBuilder, self).__init__(settlement_manager) self.plan = dict.fromkeys(self.land_manager.production, (BUILDING_PURPOSE.NONE, None)) self.__init(settlement_manager, Scheduler().cur_tick, Scheduler().cur_tick) self._init_buildability_cache() self._init_simple_collector_area_cache() self._init_road_connectivity_cache() self.register_change_list(list(settlement_manager.settlement.warehouse.position.tuple_iter()), BUILDING_PURPOSE.WAREHOUSE, None) self._refresh_unused_fields() def __init(self, settlement_manager, last_collector_improvement_storage, last_collector_improvement_road): self._init_cache() self.collector_buildings = [] # [building, ...] self.production_buildings = [] # [building, ...] self.personality = self.owner.personality_manager.get('ProductionBuilder') self.last_collector_improvement_storage = last_collector_improvement_storage self.last_collector_improvement_road = last_collector_improvement_road def _init_buildability_cache(self): self.buildability_cache = BinaryBuildabilityCache(self.island.terrain_cache) free_coords_set = set() for coords, (purpose, _) in self.plan.iteritems(): if purpose == BUILDING_PURPOSE.NONE: free_coords_set.add(coords) for coords in self.land_manager.coastline: free_coords_set.add(coords) self.buildability_cache.add_area(free_coords_set) def _init_simple_collector_area_cache(self): self.simple_collector_area_cache = SimpleCollectorAreaCache(self.island.terrain_cache) def _init_road_connectivity_cache(self): self.road_connectivity_cache = PotentialRoadConnectivityCache(self) coords_set = set() for coords in self.plan: coords_set.add(coords) for coords in self.land_manager.roads: coords_set.add(coords) self.road_connectivity_cache.modify_area(list(sorted(coords_set))) def save(self, db): super(ProductionBuilder, self).save(db) translated_last_collector_improvement_storage = self.last_collector_improvement_storage - Scheduler().cur_tick # pre-translate for the loading process translated_last_collector_improvement_road = self.last_collector_improvement_road - Scheduler().cur_tick # pre-translate for the loading process db("INSERT INTO ai_production_builder(rowid, settlement_manager, last_collector_improvement_storage, last_collector_improvement_road) VALUES(?, ?, ?, ?)", self.worldid, self.settlement_manager.worldid, translated_last_collector_improvement_storage, translated_last_collector_improvement_road) for (x, y), (purpose, _) in self.plan.iteritems(): db("INSERT INTO ai_production_builder_plan(production_builder, x, y, purpose) VALUES(?, ?, ?, ?)", self.worldid, x, y, purpose) def _load(self, db, settlement_manager): worldid, last_storage, last_road = \ db("SELECT rowid, last_collector_improvement_storage, last_collector_improvement_road FROM ai_production_builder WHERE settlement_manager = ?", settlement_manager.worldid)[0] super(ProductionBuilder, self)._load(db, settlement_manager, worldid) self.__init(settlement_manager, last_storage, last_road) db_result = db("SELECT x, y, purpose FROM ai_production_builder_plan WHERE production_builder = ?", worldid) for x, y, purpose in db_result: self.plan[(x, y)] = (purpose, None) if purpose == BUILDING_PURPOSE.ROAD: self.land_manager.roads.add((x, y)) self._init_buildability_cache() self._init_simple_collector_area_cache() self._init_road_connectivity_cache() self._refresh_unused_fields() def have_deposit(self, resource_id): """Returns true if there is a resource deposit of the relevant type inside the settlement.""" for tile in self.land_manager.resource_deposits[resource_id]: if tile.object.settlement is None: continue coords = tile.object.position.origin.to_tuple() if coords in self.settlement.ground_map: return True return False def extend_settlement_with_storage(self, target_position): """Build a storage to extend the settlement towards the given position. Return a BUILD_RESULT constant.""" if not self.have_resources(BUILDINGS.STORAGE): return BUILD_RESULT.NEED_RESOURCES storage_class = Entities.buildings[BUILDINGS.STORAGE] storage_spots = self.island.terrain_cache.get_buildability_intersection(storage_class.terrain_type, storage_class.size, self.settlement.buildability_cache, self.buildability_cache) storage_surrounding_offsets = Rect.get_surrounding_offsets(storage_class.size) coastline = self.land_manager.coastline options = [] for (x, y) in sorted(storage_spots): builder = BasicBuilder.create(BUILDINGS.STORAGE, (x, y), 0) alignment = 1 for (dx, dy) in storage_surrounding_offsets: coords = (x + dx, y + dy) if coords in coastline or coords not in self.plan or self.plan[coords][0] != BUILDING_PURPOSE.NONE: alignment += 1 distance = distances.distance_rect_rect(target_position, builder.position) value = distance - alignment * 0.7 options.append((-value, builder)) return self.build_best_option(options, BUILDING_PURPOSE.STORAGE) def get_collector_area(self): """Return the set of all coordinates that are reachable from at least one collector by road or open space.""" if self.__collector_area_cache is not None and self.last_change_id == self.__collector_area_cache[0]: return self.__collector_area_cache[1] moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] collector_area = set() # unused tiles that are reachable from at least one collector for building in self.collector_buildings: coverage_area = set() for coords in building.position.get_radius_coordinates(building.radius, True): coverage_area.add(coords) reachable = set() queue = deque() for coords in building.position.tuple_iter(): reachable.add(coords) queue.append(coords) while queue: x, y = queue[0] queue.popleft() for dx, dy in moves: coords = (x + dx, y + dy) if coords not in reachable and coords in coverage_area: if coords in self.land_manager.roads or (coords in self.plan and coords not in self.land_manager.coastline and self.plan[coords][0] == BUILDING_PURPOSE.NONE): queue.append(coords) reachable.add(coords) if coords in self.plan and self.plan[coords][0] == BUILDING_PURPOSE.NONE: collector_area.add(coords) self.__collector_area_cache = (self.last_change_id, collector_area) return collector_area def count_available_squares(self, size, max_num=None): """ Count the number of available and usable (covered by collectors) size x size squares. @param size: the square side length @param max_num: if non-None then stop counting once the number of total squares is max_num @return: (available squares, total squares) """ key = (size, max_num) if key in self.__available_squares_cache and self.last_change_id == self.__available_squares_cache[key][0]: return self.__available_squares_cache[key][1] offsets = list(itertools.product(xrange(size), xrange(size))) collector_area = self.get_collector_area() available_squares = 0 total_squares = 0 for x, y in self.plan: ok = True accessible = False for dx, dy in offsets: coords = (x + dx, y + dy) if coords not in self.plan or self.plan[coords][0] != BUILDING_PURPOSE.NONE: ok = False break if coords in collector_area: accessible = True if ok: total_squares += 1 if max_num is not None and total_squares >= max_num: break if accessible: available_squares += 1 self.__available_squares_cache[key] = (self.last_change_id, (available_squares, total_squares)) return self.__available_squares_cache[key][1] def _refresh_unused_fields(self): """Refresh the unused_fields object to make sure no impossible fields spots are in the list.""" self.unused_fields = { BUILDING_PURPOSE.POTATO_FIELD: deque(), BUILDING_PURPOSE.PASTURE: deque(), BUILDING_PURPOSE.SUGARCANE_FIELD: deque(), BUILDING_PURPOSE.TOBACCO_FIELD: deque(), } for coords, (purpose, _) in sorted(self.plan.iteritems()): usable = True # is every tile of the field spot still usable for new normal buildings for dx in xrange(3): for dy in xrange(3): coords2 = (coords[0] + dx, coords[1] + dy) if coords2 not in self.island.ground_map: usable = False else: object = self.island.ground_map[coords2].object if object is not None and not object.buildable_upon: usable = False break if usable and purpose in self.unused_fields: self.unused_fields[purpose].append(coords) def display(self): """Show the plan on the map unless it is disabled in the settings.""" if not AI.HIGHLIGHT_PLANS: return road_color = (30, 30, 30) fisher_color = (128, 128, 128) lumberjack_color = (30, 255, 30) tree_color = (0, 255, 0) reserved_color = (0, 0, 128) unknown_color = (128, 0, 0) farm_color = (128, 0, 255) potato_field_color = (255, 0, 128) pasture_color = (0, 192, 0) weaver_color = (0, 64, 64) sugarcane_field_color = (192, 192, 0) distillery_color = (255, 128, 40) tobacco_field_color = (64, 64, 0) tobacconist_color = (128, 64, 40) clay_pit_color = (0, 64, 0) brickyard_color = (0, 32, 0) boatbuilder_color = (163, 73, 164) salt_ponds_color = (153, 217, 234) misc_color = (0, 255, 255) renderer = self.session.view.renderer['InstanceRenderer'] for coords, (purpose, _) in self.plan.iteritems(): tile = self.island.ground_map[coords] if purpose == BUILDING_PURPOSE.ROAD: renderer.addColored(tile._instance, *road_color) elif purpose == BUILDING_PURPOSE.FISHER: renderer.addColored(tile._instance, *fisher_color) elif purpose == BUILDING_PURPOSE.LUMBERJACK: renderer.addColored(tile._instance, *lumberjack_color) elif purpose == BUILDING_PURPOSE.TREE: renderer.addColored(tile._instance, *tree_color) elif purpose == BUILDING_PURPOSE.FARM: renderer.addColored(tile._instance, *farm_color) elif purpose == BUILDING_PURPOSE.POTATO_FIELD: renderer.addColored(tile._instance, *potato_field_color) elif purpose == BUILDING_PURPOSE.PASTURE: renderer.addColored(tile._instance, *pasture_color) elif purpose == BUILDING_PURPOSE.WEAVER: renderer.addColored(tile._instance, *weaver_color) elif purpose == BUILDING_PURPOSE.SUGARCANE_FIELD: renderer.addColored(tile._instance, *sugarcane_field_color) elif purpose == BUILDING_PURPOSE.DISTILLERY: renderer.addColored(tile._instance, *distillery_color) elif purpose == BUILDING_PURPOSE.TOBACCO_FIELD: renderer.addColored(tile._instance, *tobacco_field_color) elif purpose == BUILDING_PURPOSE.TOBACCONIST: renderer.addColored(tile._instance, *tobacconist_color) elif purpose == BUILDING_PURPOSE.CLAY_PIT: renderer.addColored(tile._instance, *clay_pit_color) elif purpose == BUILDING_PURPOSE.BRICKYARD: renderer.addColored(tile._instance, *brickyard_color) elif purpose == BUILDING_PURPOSE.BOAT_BUILDER: renderer.addColored(tile._instance, *boatbuilder_color) elif purpose == BUILDING_PURPOSE.SALT_PONDS: renderer.addColored(tile._instance, *salt_ponds_color) elif purpose == BUILDING_PURPOSE.RESERVED: renderer.addColored(tile._instance, *reserved_color) elif purpose != BUILDING_PURPOSE.NONE: renderer.addColored(tile._instance, *misc_color) else: renderer.addColored(tile._instance, *unknown_color) def _init_cache(self): """Initialize the cache that knows the last time the buildability of a rectangle may have changed in this area.""" super(ProductionBuilder, self)._init_cache() self.__collector_area_cache = None self.__available_squares_cache = {} def register_change(self, x, y, purpose, data): """Register the possible buildability change of a rectangle on this island.""" super(ProductionBuilder, self).register_change(x, y, purpose, data) coords = (x, y) if coords in self.land_manager.village or (coords not in self.plan and coords not in self.land_manager.coastline): return self.last_change_id += 1 def register_change_list(self, coords_list, purpose, data): add_list = [] remove_list = [] for coords in coords_list: if coords in self.land_manager.village or coords not in self.plan: continue if purpose == BUILDING_PURPOSE.NONE and self.plan[coords][0] != BUILDING_PURPOSE.NONE: add_list.append(coords) elif purpose != BUILDING_PURPOSE.NONE and self.plan[coords][0] == BUILDING_PURPOSE.NONE: remove_list.append(coords) if add_list: self.buildability_cache.add_area(add_list) if remove_list: self.buildability_cache.remove_area(remove_list) super(ProductionBuilder, self).register_change_list(coords_list, purpose, data) self.road_connectivity_cache.modify_area(coords_list) self.display() def handle_lost_area(self, coords_list): """Handle losing the potential land in the given coordinates list.""" # remove planned fields that are now impossible lost_coords_list = [] for coords in coords_list: if coords in self.plan: lost_coords_list.append(coords) self.register_change_list(lost_coords_list, BUILDING_PURPOSE.NONE, None) field_size = Entities.buildings[BUILDINGS.POTATO_FIELD].size removed_list = [] for coords, (purpose, _) in self.plan.iteritems(): if purpose in [BUILDING_PURPOSE.POTATO_FIELD, BUILDING_PURPOSE.PASTURE, BUILDING_PURPOSE.SUGARCANE_FIELD, BUILDING_PURPOSE.TOBACCO_FIELD]: rect = Rect.init_from_topleft_and_size_tuples(coords, field_size) for field_coords in rect.tuple_iter(): if field_coords not in self.land_manager.production: removed_list.append(coords) break for coords in removed_list: rect = Rect.init_from_topleft_and_size_tuples(coords, field_size) self.register_change_list(list(rect.tuple_iter()), BUILDING_PURPOSE.NONE, None) self._refresh_unused_fields() super(ProductionBuilder, self).handle_lost_area(coords_list) self.road_connectivity_cache.modify_area(lost_coords_list) def handle_new_area(self): """Handle receiving more land to the production area (this can happen when the village area gives some up).""" new_coords_list = [] for coords in self.land_manager.production: if coords not in self.plan: new_coords_list.append(coords) self.register_change_list(new_coords_list, BUILDING_PURPOSE.NONE, None) collector_building_classes = [BUILDINGS.WAREHOUSE, BUILDINGS.STORAGE] field_building_classes = [BUILDINGS.POTATO_FIELD, BUILDINGS.PASTURE, BUILDINGS.SUGARCANE_FIELD, BUILDINGS.TOBACCO_FIELD] production_building_classes = set([BUILDINGS.FISHER, BUILDINGS.LUMBERJACK, BUILDINGS.FARM, BUILDINGS.CLAY_PIT, BUILDINGS.BRICKYARD, BUILDINGS.WEAVER, BUILDINGS.DISTILLERY, BUILDINGS.IRON_MINE, BUILDINGS.SMELTERY, BUILDINGS.TOOLMAKER, BUILDINGS.CHARCOAL_BURNER, BUILDINGS.TOBACCONIST, BUILDINGS.SALT_PONDS]) def add_building(self, building): """Called when a new building is added in the area (the building already exists during the call).""" if building.id in self.collector_building_classes: self.collector_buildings.append(building) self.simple_collector_area_cache.add_building(building) elif building.id in self.production_building_classes: self.production_buildings.append(building) super(ProductionBuilder, self).add_building(building) def _handle_lumberjack_removal(self, building): """Release the unused trees around the lumberjack building being removed.""" trees_used_by_others = set() for lumberjack_building in self.settlement.buildings_by_id.get(BUILDINGS.LUMBERJACK, []): if lumberjack_building.worldid == building.worldid: continue for coords in lumberjack_building.position.get_radius_coordinates(lumberjack_building.radius): if coords in self.plan and self.plan[coords][0] == BUILDING_PURPOSE.TREE: trees_used_by_others.add(coords) coords_list = [] for coords in building.position.get_radius_coordinates(building.radius): if coords not in trees_used_by_others and coords in self.plan and self.plan[coords][0] == BUILDING_PURPOSE.TREE: coords_list.append(coords) self.register_change_list(coords_list, BUILDING_PURPOSE.NONE, None) def _handle_farm_removal(self, building): """Handle farm removal by removing planned fields and tearing existing ones that can't be serviced by another farm.""" unused_fields = set() farms = self.settlement.buildings_by_id.get(BUILDINGS.FARM, []) for coords in building.position.get_radius_coordinates(building.radius): if not coords in self.plan: continue object = self.island.ground_map[coords].object if object is None or object.id not in self.field_building_classes: continue used_by_another_farm = False for farm in farms: if farm.worldid != building.worldid and object.position.distance(farm.position) <= farm.radius: used_by_another_farm = True break if not used_by_another_farm: unused_fields.add(object) # tear the finished but no longer used fields down for unused_field in unused_fields: self.register_change_list(list(unused_field.position.tuple_iter()), BUILDING_PURPOSE.NONE, None) Tear(unused_field).execute(self.session) # remove the planned but never built fields from the plan self._refresh_unused_fields() for unused_fields_list in self.unused_fields.itervalues(): for coords in unused_fields_list: position = Rect.init_from_topleft_and_size_tuples(coords, Entities.buildings[BUILDINGS.POTATO_FIELD].size) if building.position.distance(position) > building.radius: continue # it never belonged to the removed building used_by_another_farm = False for farm in farms: if farm.worldid != building.worldid and position.distance(farm.position) <= farm.radius: used_by_another_farm = True break if not used_by_another_farm: self.register_change_list(list(position.tuple_iter()), BUILDING_PURPOSE.NONE, None) self._refresh_unused_fields() def remove_building(self, building): """Called when a building is removed from the area (the building still exists during the call).""" if building.id in self.field_building_classes: # this can't be handled right now because the building still exists Scheduler().add_new_object(Callback(self._refresh_unused_fields), self, run_in=0) Scheduler().add_new_object(Callback(partial(super(ProductionBuilder, self).remove_building, building)), self, run_in=0) elif building.buildable_upon or building.id == BUILDINGS.TRAIL: pass # don't react to road, trees and ruined tents being destroyed else: self.register_change_list(list(building.position.tuple_iter()), BUILDING_PURPOSE.NONE, None) if building.id in self.collector_building_classes: self.collector_buildings.remove(building) self.simple_collector_area_cache.remove_building(building) elif building.id in self.production_building_classes: self.production_buildings.remove(building) if building.id == BUILDINGS.LUMBERJACK: self._handle_lumberjack_removal(building) elif building.id == BUILDINGS.FARM: self._handle_farm_removal(building) super(ProductionBuilder, self).remove_building(building) def manage_production(self): """Pauses and resumes production buildings when they have full input and output inventories.""" for building in self.production_buildings: producer = building.get_component(Producer) for production in producer.get_productions(): if not production.get_produced_resources(): continue all_full = True # inventory full of the produced resources? for resource_id, min_amount in production.get_produced_resources().iteritems(): if production.inventory.get_free_space_for(resource_id) >= min_amount: all_full = False break # inventory full of the input resource? if all_full and not isinstance(building, Mine): for resource_id in production.get_consumed_resources(): if production.inventory.get_free_space_for(resource_id) > 0: all_full = False break if all_full: if not production.is_paused(): ToggleActive(producer, production).execute(self.land_manager.session) self.log.info('%s paused a production at %s/%d', self, building.name, building.worldid) else: if production.is_paused(): ToggleActive(producer, production).execute(self.land_manager.session) self.log.info('%s resumed a production at %s/%d', self, building.name, building.worldid) def handle_mine_empty(self, mine): Tear(mine).execute(self.session) self.land_manager.refresh_resource_deposits() def __str__(self): return '%s.PB(%s/%s)' % (self.owner, self.settlement.get_component(NamedComponent).name if hasattr(self, 'settlement') else 'unknown', self.worldid if hasattr(self, 'worldid') else 'none')
def __init__(self, island): self._binary_cache = BinaryBuildabilityCache(island.terrain_cache) self.cache = self._binary_cache.cache # {(width, height): set((x, y), ...), ...} self.island = island self._init()