コード例 #1
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')
コード例 #2
0
	def _init_simple_collector_area_cache(self):
		self.simple_collector_area_cache = SimpleCollectorAreaCache(self.island.terrain_cache)
コード例 #3
0
 def _init_simple_collector_area_cache(self):
     self.simple_collector_area_cache = SimpleCollectorAreaCache(
         self.island.terrain_cache)
コード例 #4
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')