コード例 #1
0
	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)
コード例 #2
0
	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)
コード例 #3
0
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)
コード例 #4
0
 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()
コード例 #5
0
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')
コード例 #6
0
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')
コード例 #7
0
	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()