Пример #1
0
class PRODUCTION:
    # ./development/print_db_data.py lines
    STATES = Enum('none', 'waiting_for_res', 'inventory_full', 'producing',
                  'paused', 'done')
    # NOTE: 'done' is only for SingleUseProductions
    # NOTE: 'none' is not used by an actual production, just for a producer
    STATISTICAL_WINDOW = 1000  # How many latest ticks are relevant for keeping track of how busy a production is
Пример #2
0
class GenericAI(Player):
    """Class for AI players implementing generic stuff."""

    shipStates = Enum('idle', 'moving_random')

    def __init__(self, *args, **kwargs):
        super(GenericAI, self).__init__(*args, **kwargs)
        self.__init()

    def __init(self):
        self.ships = weakref.WeakValueDictionary(
        )  # {ship : state}. used as list of ships and structure to know their state

    def _load(self, db, worldid):
        super(GenericAI, self)._load(db, worldid)
        self.__init()

    def send_ship(self, ship):
        self.send_ship_random(ship)

    def send_ship_random(self, ship):
        """Sends a ship to a random position on the map.
		@param ship: Ship instance that is to be used."""
        # find random position
        point = self.session.world.get_random_possible_ship_position()
        self.log.debug("%s %s: moving to random location %d, %d",
                       self.__class__.__name__, self.worldid, point.x, point.y)
        # move ship there:
        try:
            ship.move(point, Callback(self.ship_idle, ship))
        except MoveNotPossible:
            self.log.info("%s %s: ship blocked", self.__class__.__name__,
                          self.worldid)
            # retry moving ship in 2 secs
            Scheduler().add_new_object(Callback(self.ship_idle, ship), self,
                                       GAME_SPEED.TICKS_PER_SECOND * 2)
            return
        self.ships[ship] = self.shipStates.moving_random

    def ship_idle(self, ship):
        """Called if a ship is idle. Sends ship to a random place.
		@param ship: ship instance"""
        self.log.debug("%s %s: idle, moving to random location",
                       self.__class__.__name__, self.worldid)
        Scheduler().add_new_object(Callback(self.send_ship, ship), self)

    def end(self):
        self.ships = None
        super(GenericAI, self).end()
Пример #3
0
class AIPlayer(Player):
    """Class for AI players implementing generic stuff."""

    shipStates = Enum('idle', 'moving_random')

    def __init__(self, *args, **kwargs):
        super(AIPlayer, self).__init__(*args, **kwargs)
        self.__init()

    def __init(self):
        self.ships = {
        }  # {ship : state}. used as list of ships and structure to know their state

    def _load(self, db, worldid):
        super(AIPlayer, self)._load(db, worldid)
        self.__init()

    def send_ship(self, ship):
        self.send_ship_random(ship)

    def send_ship_random(self, ship):
        """Sends a ship to a random position on the map.
		@param ship: Ship instance that is to be used."""
        self.log.debug("%s %s: moving to random location",
                       self.__class__.__name__, self.worldid)
        # find random position
        point = self.session.world.get_random_possible_ship_position()
        # move ship there:
        try:
            ship.move(point, Callback(self.ship_idle, ship))
        except MoveNotPossible:
            # select new target soon:
            self.notify_unit_path_blocked(ship)
            return
        self.ships[ship] = self.shipStates.moving_random

    def ship_idle(self, ship):
        """Called if a ship is idle. Sends ship to a random place.
		@param ship: ship instance"""
        self.log.debug("%s %s: idle, moving to random location",
                       self.__class__.__name__, self.worldid)
        Scheduler().add_new_object(Callback(self.send_ship, ship), self)

    def notify_unit_path_blocked(self, unit):
        self.log.warning("%s %s: ship blocked", self.__class__.__name__,
                         self.worldid)
        # retry moving ship in 2 secs
        Scheduler().add_new_object(Callback(self.ship_idle, unit), self, \
                                   GAME_SPEED.TICKS_PER_SECOND * 2)
Пример #4
0
class CombatManager:
    """
	CombatManager object is responsible for handling close combat in game.
	It scans the environment (lookout) and requests certain actions from behavior
	"""
    log = logging.getLogger("ai.aiplayer.combat.combatmanager")

    # states to keep track of combat movement of each ship.

    shipStates = Enum('idle', 'moving')

    combat_range = 18

    def __init__(self, owner):
        super().__init__()  # TODO: check if this call is needed
        self.__init(owner)

    def __init(self, owner):
        self.owner = owner
        self.unit_manager = owner.unit_manager
        self.world = owner.world
        self.session = owner.session

        # Dictionary of ship => shipState
        self.ships = DefaultWeakKeyDictionary(
            lambda ship: self.shipStates.idle)

    @classmethod
    def close_range(cls, ship):
        """
		Range used when wanting to get close to ships.
		"""
        return (2 * ship._max_range + ship._min_range) / 3 + 1

    @classmethod
    def fallback_range(cls, ship):
        """
		Range used when wanting to get away from ships.
		"""
        return cls.combat_range - 1

    def save(self, db):
        for ship, state in self.ships.items():
            db(
                "INSERT INTO ai_combat_ship (owner_id, ship_id, state_id) VALUES (?, ?, ?)",
                self.owner.worldid, ship.worldid, state.index)

    def set_ship_state(self, ship, state):
        self.ships[ship] = state

    def get_ship_state(self, ship):
        if ship not in self.ships:
            self.ships[ship] = self.shipStates.idle
        return self.ships[ship]

    def add_new_unit(self, ship, state=None):
        if not state:
            state = self.shipStates.idle

        self.set_ship_state(ship, state)

    def remove_unit(self, ship):
        if ship in self.ships:
            del self.ships[ship]

    @classmethod
    def load(cls, db, owner):
        self = cls.__new__(cls)
        self._load(db, owner)
        return self

    def _load(self, db, owner):
        self.__init(owner)

        db_result = db(
            "SELECT ship_id, state_id FROM ai_combat_ship WHERE owner_id = ?",
            self.owner.worldid)
        for (
                ship_id,
                state_id,
        ) in db_result:
            ship = WorldObject.get_object_by_id(ship_id)
            state = self.shipStates[state_id]

            # add move callbacks corresponding to given state
            if state == self.shipStates.moving:
                ship.add_move_callback(
                    Callback(BehaviorMoveCallback._arrived, ship))

            self.add_new_unit(ship, state)

    # DISPLAY-RELATED FUNCTIONS
    def _init_fake_tile(self):
        """Sets the _fake_tile_obj class variable with a ready to use fife object. To create a new fake tile, use _add_fake_tile()"""
        # use fixed SelectableBuildingComponent here, to make sure subclasses also read the same variable
        if not hasattr(CombatManager, "_fake_range_tile_obj"):
            # create object to create instances from
            CombatManager._fake_range_tile_obj = horizons.globals.fife.engine.getModel(
            ).createObject('_fake_range_tile_obj', 'ground')
            fife.ObjectVisual.create(CombatManager._fake_range_tile_obj)

            img_path = 'content/gfx/fake_water.png'
            img = horizons.globals.fife.imagemanager.load(img_path)
            for rotation in [45, 135, 225, 315]:
                CombatManager._fake_range_tile_obj.get2dGfxVisual(
                ).addStaticImage(rotation, img.getHandle())
        if not hasattr(self, '_selected_fake_tiles'):
            self._selected_fake_tiles = []
        if not hasattr(self, '_selected_tiles'):
            self._selected_tiles = []

    def _add_fake_tile(self, x, y, layer, renderer, color):
        """Adds a fake tile to the position. Requires 'self._fake_tile_obj' to be set."""
        inst = layer.createInstance(CombatManager._fake_range_tile_obj,
                                    fife.ModelCoordinate(x, y, 0), "")
        fife.InstanceVisual.create(inst)
        self._selected_fake_tiles.append(inst)
        renderer.addColored(inst, *color)

    def _add_tile(self, tile, renderer, color):
        self._selected_tiles.append(tile)
        renderer.addColored(tile._instance, *color)

    def _clear_fake_tiles(self):
        if not hasattr(self, '_selected_fake_tiles'):
            return
        renderer = self.session.view.renderer['InstanceRenderer']
        for tile in self._selected_fake_tiles:
            renderer.removeColored(tile)
            self.session.view.layers[LAYERS.FIELDS].deleteInstance(tile)
        self._selected_fake_tiles = []

        for tile in self._selected_tiles:
            renderer.removeColored(tile._instance)
        self._selected_tiles = []

    def _highlight_points(self, points, color):
        renderer = self.session.view.renderer['InstanceRenderer']
        layer = self.session.world.session.view.layers[LAYERS.FIELDS]
        for point in points:
            tup = (point.x, point.y)
            island_tile = [
                island for island in self.session.world.islands
                if island.get_tile_tuple(tup)
            ]
            if island_tile:
                tile = island_tile[0].get_tile_tuple(tup)
                self._add_tile(tile, renderer, color)
            else:
                self._add_fake_tile(tup[0], tup[1], layer, renderer, color)

    def _highlight_circle(self, position, radius, color):
        points = set(self.session.world.get_points_in_radius(position, radius))
        points2 = set(
            self.session.world.get_points_in_radius(position, radius - 1))
        self._highlight_points(list(points - points2), color)

    def display(self):
        """
		Display combat ranges.
		"""
        if not AI.HIGHLIGHT_COMBAT:
            return

        combat_range_color = (80, 0, 250)
        attack_range_color = (255, 0, 0)
        close_range_color = (0, 0, 100)
        fallback_range_color = (0, 180, 100)
        center_point_color = (0, 200, 0)

        self._clear_fake_tiles()
        self._init_fake_tile()

        for ship, state in self.ships.items():
            range = self.combat_range
            self._highlight_circle(ship.position, range, combat_range_color)
            self._highlight_circle(ship.position, self.close_range(ship),
                                   close_range_color)
            self._highlight_circle(ship.position, self.fallback_range(ship),
                                   fallback_range_color)
            self._highlight_circle(ship.position, ship._max_range,
                                   attack_range_color)
            self._highlight_circle(ship.position, ship._min_range,
                                   attack_range_color)
            self._highlight_points([ship.position], center_point_color)

    def handle_mission_combat(self, mission):
        """
		Routine for handling combat in mission that requests for it.
		"""
        filters = self.unit_manager.filtering_rules
        fleet = mission.fleet

        ship_group = fleet.get_ships()
        ship_group = self.unit_manager.filter_ships(
            ship_group, (filters.ship_state(self.ships, self.shipStates.idle)))

        if not ship_group:
            mission.abort_mission()

        ships_around = self.unit_manager.find_ships_near_group(
            ship_group, self.combat_range)
        ships_around = self.unit_manager.filter_ships(ships_around,
                                                      (filters.hostile(), ))
        pirate_ships = self.unit_manager.filter_ships(ships_around,
                                                      (filters.pirate, ))
        fighting_ships = self.unit_manager.filter_ships(
            ships_around, (filters.fighting(), ))
        working_ships = self.unit_manager.filter_ships(ships_around,
                                                       (filters.working(), ))

        environment = {'ship_group': ship_group}

        # begin combat if it's still unresolved
        if fighting_ships:
            environment['enemies'] = fighting_ships
            environment['power_balance'] = UnitManager.calculate_power_balance(
                ship_group, fighting_ships)
            self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                           self.owner.name, fighting_ships[0].owner.name,
                           environment['power_balance'])
            self.owner.behavior_manager.request_action(
                BehaviorManager.action_types.offensive,
                'fighting_ships_in_sight', **environment)
        elif pirate_ships:
            environment['enemies'] = pirate_ships
            environment['power_balance'] = UnitManager.calculate_power_balance(
                ship_group, pirate_ships)
            self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                           self.owner.name, pirate_ships[0].owner.name,
                           environment['power_balance'])
            self.owner.behavior_manager.request_action(
                BehaviorManager.action_types.offensive,
                'pirate_ships_in_sight', **environment)
        elif working_ships:
            environment['enemies'] = working_ships
            self.owner.behavior_manager.request_action(
                BehaviorManager.action_types.offensive,
                'working_ships_in_sight', **environment)
        else:
            # no one else is around to fight -> continue mission
            mission.continue_mission()

    def handle_uncertain_combat(self, mission):
        """
		Handles fleets that may way to be in combat.
		"""
        filters = self.unit_manager.filtering_rules

        # test first whether requesting for combat is of any use (any ships nearby)
        ship_group = mission.fleet.get_ships()

        # filter out ships that are already doing a combat move
        ship_group = self.unit_manager.filter_ships(
            ship_group, (filters.ship_state(self.ships, self.shipStates.idle)))
        ships_around = self.unit_manager.find_ships_near_group(
            ship_group, self.combat_range)
        ships_around = self.unit_manager.filter_ships(ships_around,
                                                      (filters.hostile()))
        pirate_ships = self.unit_manager.filter_ships(ships_around,
                                                      (filters.pirate, ))
        fighting_ships = self.unit_manager.filter_ships(
            ships_around, (filters.fighting(), ))
        working_ships = self.unit_manager.filter_ships(ships_around,
                                                       (filters.working(), ))

        if fighting_ships:
            environment = {'enemies': fighting_ships}
            if self.owner.strategy_manager.request_to_pause_mission(
                    mission, **environment):
                self.handle_mission_combat(mission)
        elif pirate_ships:
            environment = {'enemies': pirate_ships}
            if self.owner.strategy_manager.request_to_pause_mission(
                    mission, **environment):
                self.handle_mission_combat(mission)
        elif working_ships:
            environment = {'enemies': working_ships}
            if self.owner.strategy_manager.request_to_pause_mission(
                    mission, **environment):
                self.handle_mission_combat(mission)

    def handle_casual_combat(self):
        """
		Handles combat for ships wandering around the map (not assigned to any fleet/mission).
		"""
        filters = self.unit_manager.filtering_rules

        rules = (filters.not_in_fleet, filters.fighting(),
                 filters.ship_state(self.ships, self.shipStates.idle))
        for ship in self.unit_manager.get_ships(rules):
            # Turn into one-ship group, since reasoning is based around groups of ships
            ship_group = [
                ship,
            ]
            # TODO: create artificial groups by dividing ships that are nearby into groups based on their distance.
            # This may end up being costly, so postpone until we have more cpu resources to spare.

            ships_around = self.unit_manager.find_ships_near_group(
                ship_group, self.combat_range)
            pirate_ships = self.unit_manager.filter_ships(
                ships_around, (filters.pirate, ))
            fighting_ships = self.unit_manager.filter_ships(
                ships_around, (filters.fighting(), ))
            working_ships = self.unit_manager.filter_ships(
                ships_around, (filters.working(), ))
            environment = {'ship_group': ship_group}
            if fighting_ships:
                environment['enemies'] = fighting_ships
                environment[
                    'power_balance'] = UnitManager.calculate_power_balance(
                        ship_group, fighting_ships)
                self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                               self.owner.name, fighting_ships[0].owner.name,
                               environment['power_balance'])
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.offensive,
                    'fighting_ships_in_sight', **environment)
            elif pirate_ships:
                environment['enemies'] = pirate_ships
                environment[
                    'power_balance'] = UnitManager.calculate_power_balance(
                        ship_group, pirate_ships)
                self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                               self.owner.name, pirate_ships[0].owner.name,
                               environment['power_balance'])
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.offensive,
                    'pirate_ships_in_sight', **environment)
            elif working_ships:
                environment['enemies'] = working_ships
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.offensive,
                    'working_ships_in_sight', **environment)
            else:
                # execute idle action only if whole fleet is idle
                # we check for AIPlayer state here
                if all((self.owner.ships[ship] == self.owner.shipStates.idle
                        for ship in ship_group)):
                    self.owner.behavior_manager.request_action(
                        BehaviorManager.action_types.idle, 'no_one_in_sight',
                        **environment)

    def lookout(self):
        """
		Basically do 3 things:
		1. Handle combat for missions that explicitly request for it.
		2. Check whether any of current missions may want to be interrupted to resolve potential
			combat that was not planned (e.g. hostile ships nearby fleet on a mission)
		3. Handle combat for ships currently not used in any mission.
		"""
        # handle fleets that explicitly request to be in combat
        for mission in self.owner.strategy_manager.get_missions(
                condition=lambda mission: mission.combat_phase):
            self.handle_mission_combat(mission)

        # handle fleets that may way to be in combat, but request for it first
        for mission in self.owner.strategy_manager.get_missions(
                condition=lambda mission: not mission.combat_phase):
            self.handle_uncertain_combat(mission)

        # handle idle ships that are wandering around the map
        self.handle_casual_combat()

        # Log ship states every tick
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Player:%s Ships combat states:", self.owner.name)
            for ship, state in self.ships.items():
                self.log.debug(" %s: %s",
                               ship.get_component(NamedComponent).name, state)

    def tick(self):
        self.lookout()
        self.display()
Пример #5
0
class PirateCombatManager(CombatManager):
    """
	Pirate player requires slightly different handling of combat, thus it gets his own CombatManager.
	Pirate player is able to use standard BehaviorComponents in it's BehaviorManager.
	"""
    log = logging.getLogger("ai.aiplayer.piratecombatmanager")

    shipStates = Enum.get_extended(CombatManager.shipStates, 'chasing_ship',
                                   'going_home')

    def __init__(self, owner):
        super().__init__(owner)

    def handle_mission_combat(self, mission):
        """
		Routine for handling combat in mission that requests for it.
		"""
        filters = self.unit_manager.filtering_rules
        fleet = mission.fleet

        ship_group = fleet.get_ships()
        ship_group = self.unit_manager.filter_ships(
            ship_group, (filters.ship_state(self.ships, self.shipStates.idle)))

        if not ship_group:
            mission.abort_mission()

        ships_around = self.unit_manager.find_ships_near_group(
            ship_group, self.combat_range)
        ships_around = self.unit_manager.filter_ships(ships_around,
                                                      (filters.hostile(), ))
        fighting_ships = self.unit_manager.filter_ships(
            ships_around, (filters.fighting(), ))
        working_ships = self.unit_manager.filter_ships(ships_around,
                                                       (filters.working(), ))

        environment = {'ship_group': ship_group}

        # begin combat if it's still unresolved
        if fighting_ships:
            environment['enemies'] = fighting_ships
            environment['power_balance'] = UnitManager.calculate_power_balance(
                ship_group, fighting_ships)
            self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                           self.owner.name, fighting_ships[0].owner.name,
                           environment['power_balance'])
            self.owner.behavior_manager.request_action(
                BehaviorManager.action_types.offensive,
                'fighting_ships_in_sight', **environment)
        elif working_ships:
            environment['enemies'] = working_ships
            self.owner.behavior_manager.request_action(
                BehaviorManager.action_types.offensive,
                'working_ships_in_sight', **environment)
        else:
            # no one else is around to fight -> continue mission
            mission.continue_mission()

    def handle_uncertain_combat(self, mission):
        """
		Handles fleets that may way to be in combat.
		"""
        filters = self.unit_manager.filtering_rules

        # test first whether requesting for combat is of any use (any ships nearby)
        ship_group = mission.fleet.get_ships()
        ship_group = self.unit_manager.filter_ships(
            ship_group, (filters.ship_state(self.ships, self.shipStates.idle)))
        ships_around = self.unit_manager.find_ships_near_group(
            ship_group, self.combat_range)
        ships_around = self.unit_manager.filter_ships(ships_around,
                                                      (filters.hostile()))
        fighting_ships = self.unit_manager.filter_ships(
            ships_around, (filters.fighting(), ))
        working_ships = self.unit_manager.filter_ships(ships_around,
                                                       (filters.working(), ))

        if fighting_ships:
            environment = {'enemies': fighting_ships}
            if self.owner.strategy_manager.request_to_pause_mission(
                    mission, **environment):
                self.handle_mission_combat(mission)
        elif working_ships:
            environment = {'enemies': working_ships}
            if self.owner.strategy_manager.request_to_pause_mission(
                    mission, **environment):
                self.handle_mission_combat(mission)

    def handle_casual_combat(self):
        """
		Combat with idle ships (not assigned to a mission)
		"""
        filters = self.unit_manager.filtering_rules

        rules = (filters.not_in_fleet, filters.pirate,
                 filters.ship_state(self.ships, self.shipStates.idle))
        for ship in self.unit_manager.get_ships(rules):
            # Turn into one-ship group, since reasoning is based around groups of ships
            ship_group = [
                ship,
            ]

            ships_around = self.unit_manager.find_ships_near_group(
                ship_group, self.combat_range)
            fighting_ships = self.unit_manager.filter_ships(
                ships_around, (filters.fighting(), ))
            working_ships = self.unit_manager.filter_ships(
                ships_around, (filters.working(), ))
            environment = {'ship_group': ship_group}
            if fighting_ships:
                environment['enemies'] = fighting_ships
                environment[
                    'power_balance'] = UnitManager.calculate_power_balance(
                        ship_group, fighting_ships)
                self.log.debug("Player: %s vs Player: %s -> power_balance:%s",
                               self.owner.name, fighting_ships[0].owner.name,
                               environment['power_balance'])
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.offensive,
                    'fighting_ships_in_sight', **environment)
            elif working_ships:
                environment['enemies'] = working_ships
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.offensive,
                    'working_ships_in_sight', **environment)
            else:
                self.owner.behavior_manager.request_action(
                    BehaviorManager.action_types.idle, 'no_one_in_sight',
                    **environment)
class PirateRoutine(FleetMission):
    """
	Never ending mission of:
	1. Start moving to random places.
	2. Chase nearby ships along the way.
	3. Go back home
	"""

    missionStates = Enum.get_extended(FleetMission.missionStates,
                                      'sailing_to_target', 'chasing_ship',
                                      'going_home')

    # range at which the ship is considered "caught"
    caught_range = 5

    def __init__(self, success_callback, failure_callback, ships):
        super(PirateRoutine, self).__init__(success_callback, failure_callback,
                                            ships)
        self.target_point = self.owner.session.world.get_random_possible_ship_position(
        )

    def _setup_state_callbacks(self):
        self.combatIntermissions = {
            self.missionStates.sailing_to_target:
            (self.sail_to_target, self.flee_home),
            self.missionStates.chasing_ship: (self.chase_ship, self.flee_home),
            self.missionStates.going_home: (self.go_home, self.flee_home),
            self.missionStates.fleeing_home: (self.flee_home, self.flee_home),
        }

        self._state_fleet_callbacks = {
            self.missionStates.sailing_to_target:
            Callback(self.go_home),
            self.missionStates.chasing_ship:
            Callback(self.chase_ship),
            self.missionStates.going_home:
            Callback(self.report_success, "Pirate routine ended successfully"),
            self.missionStates.fleeing_home:
            Callback(self.report_failure,
                     "Mission was a failure, ships fled home successfully"),
        }

    def save(self, db):
        super(PirateRoutine, self).save(db)
        db(
            "INSERT INTO ai_mission_pirate_routine (rowid, target_point_x, target_point_y) VALUES(?, ?, ?)",
            self.worldid, self.target_point.x, self.target_point.y)

    def _load(self, worldid, owner, db, success_callback, failure_callback):
        super(PirateRoutine, self)._load(db, worldid, success_callback,
                                         failure_callback, owner)
        db_result = db(
            "SELECT target_point_x, target_point_y FROM ai_mission_pirate_routine WHERE rowid = ?",
            worldid)[0]

        self.target_point = Point(*db_result)

    def start(self):
        self.sail_to_target()

    def sail_to_target(self):
        self.log.debug(
            "Pirate %s, Mission %s, 1/2 set off to random point at %s",
            self.owner.name, self.__class__.__name__, self.target_point)
        try:
            self.fleet.move(
                self.target_point, self._state_fleet_callbacks[
                    self.missionStates.sailing_to_target])
            self.state = self.missionStates.sailing_to_target
        except MoveNotPossible:
            self.report_failure("Move was not possible when moving to target")

    def go_home(self):
        self.log.debug("Pirate %s, Mission %s, 2/2 going home at point %s",
                       self.owner.name, self.__class__.__name__,
                       self.owner.home_point)
        try:
            self.fleet.move(
                self.owner.home_point,
                self._state_fleet_callbacks[self.missionStates.going_home])
            self.state = self.missionStates.going_home
        except MoveNotPossible:
            self.report_failure(
                "Pirate: %s, Mission: %s, Pirate ship couldn't go home." %
                (self.owner.name, self.__class__.__name__))

    def chase_ship(self):
        pass

    def flee_home(self):
        # check if fleet still exists
        if self.fleet.size() > 0:
            try:
                self.fleet.move(
                    self.owner.home_point, self._state_fleet_callbacks[
                        self.missionStates.fleeing_home])
                self.state = self.missionStates.fleeing_home
            except MoveNotPossible:
                self.report_failure(
                    "Pirate: %s, Mission: %s, Pirate ship couldn't flee home after combat"
                    % (self.owner.name, self.__class__.__name__))
        else:
            self.report_failure("Combat was lost, all ships were wiped out")

    @classmethod
    def create(cls, success_callback, failure_callback, ships):
        return PirateRoutine(success_callback, failure_callback, ships)
Пример #7
0
class FoundSettlement(ShipMission):
	"""
	Given a ship with the required resources and a warehouse_location the ship is taken near
	the location and a warehouse is built.
	"""

	missionStates = Enum('created', 'moving')

	def __init__(self, success_callback, failure_callback, land_manager, ship, warehouse_location):
		super(FoundSettlement, self).__init__(success_callback, failure_callback, ship)
		self.land_manager = land_manager
		self.warehouse_location = warehouse_location
		self.warehouse = None
		self.state = self.missionStates.created

	def save(self, db):
		super(FoundSettlement, self).save(db)
		db("INSERT INTO ai_mission_found_settlement(rowid, land_manager, ship, warehouse_builder, state) VALUES(?, ?, ?, ?, ?)",
			self.worldid, self.land_manager.worldid, self.ship.worldid, self.warehouse_location.worldid, self.state.index)
		assert isinstance(self.warehouse_location, Builder)
		self.warehouse_location.save(db)

	@classmethod
	def load(cls, db, worldid, success_callback, failure_callback):
		self = cls.__new__(cls)
		self._load(db, worldid, success_callback, failure_callback)
		return self

	def _load(self, db, worldid, success_callback, failure_callback):
		db_result = db("SELECT land_manager, ship, warehouse_builder, state FROM ai_mission_found_settlement WHERE rowid = ?", worldid)[0]
		self.land_manager = WorldObject.get_object_by_id(db_result[0])
		self.warehouse_location = Builder.load(db, db_result[2], self.land_manager)
		self.warehouse = None
		self.state = self.missionStates[db_result[3]]
		super(FoundSettlement, self).load(db, worldid, success_callback, failure_callback, WorldObject.get_object_by_id(db_result[1]))

		if self.state == self.missionStates.moving:
			self.ship.add_move_callback(Callback(self._reached_destination_area))
			self.ship.add_blocked_callback(Callback(self._move_to_destination_area))
		else:
			assert False, 'invalid state'

	def start(self):
		self.state = self.missionStates.moving
		self._move_to_destination_area()

	def _move_to_destination_area(self):
		if self.warehouse_location is None:
			self.report_failure('No possible warehouse location')
			return

		self._move_to_warehouse_area(self.warehouse_location.position, Callback(self._reached_destination_area),
			Callback(self._move_to_destination_area), 'Move not possible')

	def _reached_destination_area(self):
		self.log.info('%s reached BO area', self)

		self.warehouse = self.warehouse_location.execute()
		if not self.warehouse:
			self.report_failure('Unable to build the warehouse')
			return

		island = self.warehouse_location.land_manager.island
		self.land_manager.settlement = island.get_settlement(self.warehouse_location.point)
		self.log.info('%s built the warehouse', self)

		self._unload_all_resources(self.land_manager.settlement)
		self.report_success('Built the warehouse, transferred resources')

	@classmethod
	def find_warehouse_location(cls, ship, land_manager):
		"""
		Finds a location for the warehouse on the given island
		@param LandManager: the LandManager of the island
		@return _BuildPosition: a possible build location
		"""
		moves = [(-1, 0), (0, -1), (0, 1), (1, 0)]
		island = land_manager.island
		world = island.session.world
		personality = land_manager.owner.personality_manager.get('FoundSettlement')
		options = []

		for (x, y), tile in sorted(island.ground_map.iteritems()):
			ok = False
			for x_offset, y_offset in moves:
				for d in xrange(2, 6):
					coords = (x + d * x_offset, y + d * y_offset)
					if coords in world.water_body and world.water_body[coords] == world.water_body[ship.position.to_tuple()]:
						# the planned warehouse should be reachable from the ship's water body
						ok = True
			if not ok:
				continue

			build_info = None
			point = Point(x, y)
			warehouse = Builder(BUILDINGS.WAREHOUSE, land_manager, point, ship = ship)
			if not warehouse:
				continue

			cost = 0
			for coords in land_manager.village:
				distance = point.distance(coords)
				if distance < personality.too_close_penalty_threshold:
					cost += personality.too_close_constant_penalty + personality.too_close_linear_penalty / (distance + 1.0)
				else:
					cost += distance

			for settlement_manager in land_manager.owner.settlement_managers:
				cost += warehouse.position.distance(settlement_manager.settlement.warehouse.position) * personality.linear_warehouse_penalty

			options.append((cost, warehouse))

		for _, build_info in sorted(options):
			(x, y) = build_info.position.get_coordinates()[4]
			if ship.check_move(Circle(Point(x, y), BUILDINGS.BUILD.MAX_BUILDING_SHIP_DISTANCE)):
				return build_info
		return None

	@classmethod
	def create(cls, ship, land_manager, success_callback, failure_callback):
		warehouse_location = cls.find_warehouse_location(ship, land_manager)
		return FoundSettlement(success_callback, failure_callback, land_manager, ship, warehouse_location)
class SpecialDomesticTrade(ShipMission):
    """
	Given a ship and two settlement managers the ship will go to the source destination,
	load the most useful resources for the destination settlement, and then unload at the
	destination settlement.
	"""

    missionStates = Enum('created', 'moving_to_source_settlement',
                         'moving_to_destination_settlement')

    def __init__(self, source_settlement_manager,
                 destination_settlement_manager, ship, success_callback,
                 failure_callback):
        super(SpecialDomesticTrade, self).__init__(success_callback,
                                                   failure_callback, ship)
        self.source_settlement_manager = source_settlement_manager
        self.destination_settlement_manager = destination_settlement_manager
        self.state = self.missionStates.created

    def save(self, db):
        super(SpecialDomesticTrade, self).save(db)
        db(
            "INSERT INTO ai_mission_special_domestic_trade(rowid, source_settlement_manager, destination_settlement_manager, ship, state) VALUES(?, ?, ?, ?, ?)",
            self.worldid, self.source_settlement_manager.worldid,
            self.destination_settlement_manager.worldid, self.ship.worldid,
            self.state.index)

    @classmethod
    def load(cls, db, worldid, success_callback, failure_callback):
        self = cls.__new__(cls)
        self._load(db, worldid, success_callback, failure_callback)
        return self

    def _load(self, db, worldid, success_callback, failure_callback):
        db_result = db(
            "SELECT source_settlement_manager, destination_settlement_manager, ship, state FROM ai_mission_special_domestic_trade WHERE rowid = ?",
            worldid)[0]
        self.source_settlement_manager = WorldObject.get_object_by_id(
            db_result[0])
        self.destination_settlement_manager = WorldObject.get_object_by_id(
            db_result[1])
        self.state = self.missionStates[db_result[3]]
        super(SpecialDomesticTrade,
              self).load(db, worldid, success_callback, failure_callback,
                         WorldObject.get_object_by_id(db_result[2]))

        if self.state is self.missionStates.moving_to_source_settlement:
            self.ship.add_move_callback(
                Callback(self._reached_source_settlement))
            self.ship.add_blocked_callback(
                Callback(self._move_to_source_settlement))
        elif self.state is self.missionStates.moving_to_destination_settlement:
            self.ship.add_move_callback(
                Callback(self._reached_destination_settlement))
            self.ship.add_blocked_callback(
                Callback(self._move_to_destination_settlement))
        else:
            assert False, 'invalid state'

    def start(self):
        self.state = self.missionStates.moving_to_source_settlement
        self._move_to_source_settlement()
        self.log.info(
            '%s started a special domestic trade mission from %s to %s using %s',
            self,
            self.source_settlement_manager.settlement.get_component(
                NamedComponent).name,
            self.destination_settlement_manager.settlement.get_component(
                NamedComponent).name, self.ship)

    def _move_to_source_settlement(self):
        self._move_to_warehouse_area(
            self.source_settlement_manager.settlement.warehouse.position,
            Callback(self._reached_source_settlement),
            Callback(self._move_to_source_settlement),
            'Unable to move to the source settlement ({})'.format(
                self.source_settlement_manager.settlement.get_component(
                    NamedComponent).name))

    def _load_resources(self):
        source_resource_manager = self.source_settlement_manager.resource_manager
        source_inventory = self.source_settlement_manager.settlement.get_component(
            StorageComponent).inventory
        destination_resource_manager = self.destination_settlement_manager.resource_manager
        destination_inventory = self.destination_settlement_manager.settlement.get_component(
            StorageComponent).inventory

        options = []
        for resource_id, limit in destination_resource_manager.resource_requirements.items(
        ):
            if destination_inventory[resource_id] >= limit:
                continue  # the destination settlement doesn't need the resource
            if source_inventory[
                    resource_id] <= source_resource_manager.resource_requirements[
                        resource_id]:
                continue  # the source settlement doesn't have a surplus of the resource

            price = self.owner.session.db.get_res_value(resource_id)
            tradable_amount = min(
                self.ship.get_component(StorageComponent).inventory.get_limit(
                    resource_id), limit - destination_inventory[resource_id],
                source_inventory[resource_id] -
                source_resource_manager.resource_requirements[resource_id])
            options.append(
                (tradable_amount * price, tradable_amount, resource_id))

        if not options:
            return False  # no resources to transport

        options.sort(reverse=True)
        for _, amount, resource_id in options:
            self.move_resource(self.source_settlement_manager.settlement,
                               self.ship, resource_id, amount)
        return True

    def _reached_source_settlement(self):
        self.log.info(
            '%s reached the first warehouse area (%s)', self,
            self.source_settlement_manager.settlement.get_component(
                NamedComponent).name)
        if self._load_resources():
            self.state = self.missionStates.moving_to_destination_settlement
            self._move_to_destination_settlement()
        else:
            self.report_failure('No resources to transport')

    def _move_to_destination_settlement(self):
        self._move_to_warehouse_area(
            self.destination_settlement_manager.settlement.warehouse.position,
            Callback(self._reached_destination_settlement),
            Callback(self._move_to_destination_settlement),
            'Unable to move to the destination settlement ({})'.format(
                self.destination_settlement_manager.settlement.get_component(
                    NamedComponent).name))

    def _reached_destination_settlement(self):
        self._unload_all_resources(
            self.destination_settlement_manager.settlement)
        self.log.info(
            '%s reached the destination warehouse area (%s)', self,
            self.destination_settlement_manager.settlement.get_component(
                NamedComponent).name)
        self.report_success('Unloaded resources')
Пример #9
0
class Collector(Unit):
    """Base class for every collector. Does not depend on any home building.

	Timeline:
	 * search_job
	 * * get_job
	 * * handle_no_possible_job
	 * * begin_current_job
	 * * * setup_new_job
	 * * * move to target
	 on arrival there:
	 * begin_working
	 after some pretended working:
	 * finish_working
	 * * transfer_res
	 after subclass has done actions to finish job:
	 * end_job
	"""
    log = logging.getLogger("world.units.collector")

    work_duration = COLLECTORS.DEFAULT_WORK_DURATION  # default is 16
    destination_always_in_building = False

    # all states, any (subclass) instance may have. Keeping a list in one place
    # is important, because every state must have a distinct number.
    # Handling of subclass specific states is done by subclass.
    states = Enum(
        'idle',  # doing nothing, waiting for job
        'moving_to_target',
        'working',
        'moving_home',
        'waiting_for_animal_to_stop',  # herder: wait for job target to finish for collecting
        'waiting_for_herder',  # animal: has stopped, now waits for herder
        'no_job_walking_randomly',  # animal: like idle, but moving instead of standing still
        'no_job_waiting',  # animal: as idle, but no move target can be found
        # TODO: merge no_job_waiting with idle
        'decommissioned',  # fisher ship: When home building got demolished. No more collecting.
    )

    # INIT/DESTRUCT

    def __init__(self, x, y, slots=1, start_hidden=True, **kwargs):
        super(Collector, self).__init__(slots=slots, x=x, y=y, **kwargs)

        self.__init(self.states.idle, start_hidden)

        # start searching jobs just when construction (of subclass) is completed
        Scheduler().add_new_object(self.search_job, self, 1)

    def __init(self, state, start_hidden):
        self.state = state
        self.start_hidden = start_hidden
        if self.start_hidden:
            self.hide()

        self.job = None  # here we store the current job as Job object

    def remove(self):
        """Removes the instance. Useful when the home building is destroyed"""
        self.log.debug("%s: remove called", self)
        self.cancel(continue_action=lambda: 42)
        # remove from target collector list
        self._abort_collector_job()
        self.hide()
        self.job = None
        super(Collector, self).remove()

    def _abort_collector_job(self):
        if self.job is None or self.state == self.states.moving_home:
            # in the move_home state, there still is a job, but the collector is
            # already deregistered
            return
        if not hasattr(self.job.object, 'remove_incoming_collector'):
            # when loading a game fails and the world is destructed again, the
            # worldid may not yet have been resolved to an actual in-game object
            return
        self.job.object.remove_incoming_collector(self)

    # SAVE/LOAD

    def save(self, db):
        super(Collector, self).save(db)

        # save state and remaining ticks for next callback
        # retrieve remaining ticks according current callback according to state
        current_callback = None
        remaining_ticks = None
        if self.state == self.states.idle:
            current_callback = self.search_job
        elif self.state == self.states.working:
            current_callback = self.finish_working
        if current_callback is not None:
            calls = Scheduler().get_classinst_calls(self, current_callback)
            assert len(
                calls
            ) == 1, 'Collector should have callback {} scheduled, but has {}'.format(
                current_callback,
                [str(i) for i in Scheduler().get_classinst_calls(self).keys()])
            remaining_ticks = max(list(calls.values())[0],
                                  1)  # save a number > 0

        db(
            "INSERT INTO collector(rowid, state, remaining_ticks, start_hidden) VALUES(?, ?, ?, ?)",
            self.worldid, self.state.index, remaining_ticks, self.start_hidden)

        # save the job
        if self.job is not None:
            obj_id = -1 if self.job.object is None else self.job.object.worldid
            # this is not in 3rd normal form since the object is saved multiple times but
            # it preserves compatibility with old savegames this way.
            for entry in self.job.reslist:
                db(
                    "INSERT INTO collector_job(collector, object, resource, amount) VALUES(?, ?, ?, ?)",
                    self.worldid, obj_id, entry.res, entry.amount)

    def load(self, db, worldid):
        super(Collector, self).load(db, worldid)

        # load collector properties
        state_id, remaining_ticks, start_hidden = \
                db("SELECT state, remaining_ticks, start_hidden FROM collector \
		            WHERE rowid = ?"                                    , worldid)[0]
        self.__init(self.states[state_id], start_hidden)

        # load job
        job_db = db(
            "SELECT object, resource, amount FROM collector_job WHERE collector = ?",
            worldid)
        if job_db:
            reslist = []
            for obj, res, amount in job_db:
                reslist.append(Job.ResListEntry(res, amount, False))
            # create job with worldid of object as object. This is used to defer the target resolution,
            # which might not have been loaded
            self.job = Job(obj, reslist)

        def fix_job_object():
            # resolve worldid to object later
            if self.job:
                if self.job.object == -1:
                    self.job.object = None  # e.g. when hunters have killed their prey
                else:
                    self.job.object = WorldObject.get_object_by_id(
                        self.job.object)

        # apply state when job object is loaded for sure
        Scheduler().add_new_object(Callback.ChainedCallbacks(
            fix_job_object,
            Callback(self.apply_state, self.state, remaining_ticks)),
                                   self,
                                   run_in=0)

    def apply_state(self, state, remaining_ticks=None):
        """Takes actions to set collector to a state. Useful after loading.
		@param state: EnumValue from states
		@param remaining_ticks: ticks after which current state is finished
		"""
        if state == self.states.idle:
            # we do nothing, so schedule a new search for a job
            Scheduler().add_new_object(self.search_job, self, remaining_ticks)
        elif state == self.states.moving_to_target:
            # we are on the way to target, so save the job
            self.setup_new_job()
            # and notify us, when we're at target
            self.add_move_callback(self.begin_working)
            self.add_blocked_callback(self.handle_path_to_job_blocked)
            self.show()
        elif state == self.states.working:
            # we are at the target and work
            # register the new job
            self.setup_new_job()
            # job finishes in remaining_ticks ticks
            Scheduler().add_new_object(self.finish_working, self,
                                       remaining_ticks)

    # GETTER

    def get_home_inventory(self):
        """Returns inventory where collected res will be stored.
		This could be the inventory of a home_building, or it's own.
		"""
        raise NotImplementedError

    def get_colleague_collectors(self):
        """Returns a list of collectors, that work for the same "inventory"."""
        return []

    def get_collectable_res(self):
        """Return all resources the collector can collect"""
        raise NotImplementedError

    def get_job(self):
        """Returns the next job or None"""
        raise NotImplementedError

    # BEHAVIOR
    def search_job(self):
        """Search for a job, only called if the collector does not have a job.
		If no job is found, a new search will be scheduled in a few ticks."""
        self.job = self.get_job()
        if self.job is None:
            self.handle_no_possible_job()
        else:
            self.begin_current_job()

    def handle_no_possible_job(self):
        """Called when we can't find a job. default is to wait and try again in a few secs"""
        self.log.debug("%s: found no possible job, retry in %s ticks", self,
                       COLLECTORS.DEFAULT_WAIT_TICKS)
        Scheduler().add_new_object(self.search_job, self,
                                   COLLECTORS.DEFAULT_WAIT_TICKS)

    def setup_new_job(self):
        """Executes the necessary actions to begin a new job"""
        self.job.object.add_incoming_collector(self)

    def check_possible_job_target(self, target):
        """Checks if we "are allowed" and able to pick up from the target"""
        # Discard building if it works for same inventory (happens when both are storage buildings
        # or home_building is checked out)
        if target.get_component(
                StorageComponent).inventory is self.get_home_inventory():
            #self.log.debug("nojob: same inventory")
            return False

        if self.has_component(
                RestrictedPickup):  # check if we're allowed to pick up there
            return self.get_component(RestrictedPickup).pickup_allowed_at(
                target.id)

        # pathfinding would fit in here, but it's too expensive,
        # we just do that at targets where we are sure to get a lot of res later on.

        return True

    def check_possible_job_target_for(self, target, res):
        """Checks out if we could get res from target.
		Does _not_ check for anything else (e.g. if we are able to walk there).
		@param target: possible target. buildings are supported, support for more can be added.
		@param res: resource id
		@return: instance of Job or None, if we can't collect anything
		"""
        res_amount = target.get_available_pickup_amount(res, self)
        if res_amount <= 0:
            #self.log.debug("nojob: no pickup amount")
            return None

        # check if other collectors get this resource, because our inventory could
        # get full if they arrive.
        total_registered_amount_consumer = sum(
            entry.amount for collector in self.get_colleague_collectors()
            if collector.job is not None for entry in collector.job.reslist
            if entry.res == res)

        inventory = self.get_home_inventory()

        # check if there are resources left to pickup
        home_inventory_free_space = inventory.get_free_space_for(res) \
                                    - total_registered_amount_consumer
        if home_inventory_free_space <= 0:
            #self.log.debug("nojob: no home inventory space")
            return None

        collector_inventory_free_space = self.get_component(
            StorageComponent).inventory.get_free_space_for(res)
        if collector_inventory_free_space <= 0:
            #self.log.debug("nojob: no collector inventory space")
            return None

        possible_res_amount = min(res_amount, home_inventory_free_space,
                                  collector_inventory_free_space)

        target_inventory_full = (target.get_component(
            StorageComponent).inventory.get_free_space_for(res) == 0)

        # create a new data line.
        return Job.ResListEntry(res, possible_res_amount,
                                target_inventory_full)

    def get_best_possible_job(self, jobs):
        """Return best possible job from jobs.
		"Best" means that the job is highest when the job list was sorted.
		"Possible" means that we can find a path there.
		@param jobs: unsorted JobList instance
		@return: selected Job instance from list or None if no jobs are possible."""
        jobs.sort_jobs()
        # check if we can move to that targets
        for job in jobs:
            path = self.check_move(job.object.loading_area)
            if path:
                job.path = path
                return job

        return None

    def begin_current_job(self, job_location=None):
        """Starts executing the current job by registering itself and moving to target.
		@param job_location: Where collector should work. default: job.object.loading_area"""
        self.log.debug("%s prepares job %s", self, self.job)
        self.setup_new_job()
        self.show()
        if job_location is None:
            job_location = self.job.object.loading_area
        self.move(job_location,
                  self.begin_working,
                  destination_in_building=self.destination_always_in_building,
                  blocked_callback=self.handle_path_to_job_blocked,
                  path=self.job.path)
        self.state = self.states.moving_to_target

    def resume_movement(self):
        """Try to resume movement after getting blocked. If that fails then wait and try again."""
        try:
            self._move_tick(resume=True)
        except PathBlockedError:
            Scheduler().add_new_object(self.resume_movement, self,
                                       COLLECTORS.DEFAULT_WAIT_TICKS)

    def handle_path_to_job_blocked(self):
        """Called when we get blocked while trying to move to the job location.
		The default action is to resume movement in a few seconds."""
        self.log.debug(
            "%s: got blocked while moving to the job location, trying again in %s ticks.",
            self, COLLECTORS.DEFAULT_WAIT_TICKS)
        Scheduler().add_new_object(self.resume_movement, self,
                                   COLLECTORS.DEFAULT_WAIT_TICKS)

    def begin_working(self):
        """Pretends that the collector works by waiting some time. finish_working is
		called after that time."""
        self.log.debug("%s begins working", self)
        assert self.job is not None, '{} job is None in begin_working'.format(
            self)
        Scheduler().add_new_object(self.finish_working, self,
                                   self.work_duration)
        # play working sound
        if self.has_component(AmbientSoundComponent):
            am_comp = self.get_component(AmbientSoundComponent)
            if am_comp.soundfiles:
                am_comp.play_ambient(am_comp.soundfiles[0],
                                     position=self.position)
        self.state = self.states.working

    def finish_working(self):
        """Called when collector has stayed at the target for a while.
		Picks up the resources.
		Should be overridden to specify what the collector should do after this."""
        self.log.debug("%s finished working", self)
        self.act("idle", self._instance.getFacingLocation(), True)
        # deregister at the target we're at
        self.job.object.remove_incoming_collector(self)
        # reconsider job now: there might now be more res available than there were when we started

        reslist = (self.check_possible_job_target_for(self.job.object, res)
                   for res in self.get_collectable_res())
        reslist = [i for i in reslist if i]
        if reslist:
            self.job.reslist = reslist

        # transfer res (this must be the last step, it will trigger consecutive actions through the
        # target inventory changelistener, and the collector must be in a consistent state then.
        self.transfer_res_from_target()
        # stop playing ambient sound if any
        if self.has_component(AmbientSoundComponent):
            self.get_component(AmbientSoundComponent).stop_sound()

    def transfer_res_from_target(self):
        """Transfers resources from target to collector inventory"""
        new_reslist = []
        for entry in self.job.reslist:
            actual_amount = self.job.object.pickup_resources(
                entry.res, entry.amount, self)
            if entry.amount != actual_amount:
                new_reslist.append(
                    Job.ResListEntry(entry.res, actual_amount, False))
            else:
                new_reslist.append(entry)

            remnant = self.get_component(StorageComponent).inventory.alter(
                entry.res, actual_amount)
            assert remnant == 0, "{} couldn't take all of res {}; remnant: {}; planned: {}".format(
                self, entry.res, remnant, entry.amount)
        self.job.reslist = new_reslist

    def transfer_res_to_home(self, res, amount):
        """Transfer resources from collector to the home inventory"""
        self.log.debug("%s brought home %s of %s", self, amount, res)
        remnant = self.get_home_inventory().alter(res, amount)
        #assert remnant == 0, "Home building could not take all resources from collector."
        remnant = self.get_component(StorageComponent).inventory.alter(
            res, -amount)
        assert remnant == 0, "{} couldn't give all of res {}; remnant: {}; inventory: {}".format(
            self, res, remnant,
            self.get_component(StorageComponent).inventory)

    # unused reroute code removed in 2aef7bba77536da333360566467d9a2f08d38cab

    def end_job(self):
        """Contrary to setup_new_job"""
        # the job now is finished now
        # before the new job can begin this will be executed
        self.log.debug("%s end_job - waiting for new search_job", self)
        if self.start_hidden:
            self.hide()
        self.job = None
        Scheduler().add_new_object(self.search_job, self,
                                   COLLECTORS.DEFAULT_WAIT_TICKS)
        self.state = self.states.idle

    def cancel(self, continue_action):
        """Aborts the current job.
		@param continue_action: Callback, gets called after cancel. Specifies what collector
		                        is supposed to do now.
		NOTE: Subclasses set this to a proper action that makes the collector continue to work.
		      If the collector is supposed to be remove, use a noop.
		"""
        self.stop()
        self.log.debug("%s was cancelled, continue action is %s", self,
                       continue_action)
        # remove us as incoming collector at target
        self._abort_collector_job()
        if self.job is not None:
            # clean up depending on state
            if self.state == self.states.working:
                removed_calls = Scheduler().rem_call(self, self.finish_working)
                assert removed_calls == 1, 'removed {} calls instead of one'.format(
                    removed_calls)
            self.job = None
            self.state = self.states.idle
        # NOTE:
        # Some blocked movement callbacks use this callback. All blocked
        # movement callbacks have to be cancelled here, else the unit will try
        # to continue the movement later when its state has already changed.
        # This line should fix it sufficiently for now and the problem could be
        # deprecated when the switch to a component-based system is accomplished.
        Scheduler().rem_call(self, self.resume_movement)
        continue_action()

    def __str__(self):
        try:
            return super(Collector, self).__str__() + "(state={})".format(
                self.state)
        except AttributeError:  # state has not been set
            return super(Collector, self).__str__()
Пример #10
0
class Trader(GenericAI):
    """A trader represents the free trader that travels around the map with its trading ship(s) and
	sells resources to players and buys resources from them. This is a very simple form of AI, as it
	doesn't do any more then drive to a place on water or a warehouse randomly and then buys and
	sells resources. A game should not have more then one free trader (it could though)
	@param id: int - player id, every Player needs a unique id, as the free trader is a Player instance, it also does.
	@param name: Traders name, also needed for the Player class.
	@param color: util.Color instance with the traders banner color, also needed for the Player class"""

    shipStates = Enum.get_extended(GenericAI.shipStates, 'moving_to_warehouse',
                                   'reached_warehouse')

    log = logging.getLogger("ai.trader")
    regular_player = False

    def __init__(self, session, id, name, color, **kwargs):
        super(Trader, self).__init__(session, id, name, color, **kwargs)
        self.__init()
        map_size = self.session.world.map_dimensions.width
        while map_size > 0:
            self.create_ship()
            map_size -= TRADER.TILES_PER_TRADER

    def create_ship(self):
        """Create a ship and place it randomly"""
        self.log.debug("Trader %s: creating new ship", self.worldid)
        point = self.session.world.get_random_possible_ship_position()
        ship = CreateUnit(self.worldid, UNITS.TRADER_SHIP, point.x,
                          point.y)(issuer=self)
        self.ships[ship] = self.shipStates.reached_warehouse
        Scheduler().add_new_object(Callback(self.ship_idle, ship),
                                   self,
                                   run_in=0)

    def __init(self):
        self.warehouse = {
        }  # { ship.worldid : warehouse }. stores the warehouse the ship is currently heading to
        self.allured_by_signal_fire = {
        }  # bool, used to get away from a signal fire (and not be allured again immediately)

        NewSettlement.subscribe(self._on_new_settlement)

    def _on_new_settlement(self, msg):
        # make sure there's a trader ship for SETTLEMENTS_PER_SHIP settlements
        if len(self.session.world.settlements
               ) > self.get_ship_count() * TRADER.SETTLEMENTS_PER_SHIP:
            self.create_ship()

    def save(self, db):
        super(Trader, self).save(db)

        # mark self as a trader
        db("UPDATE player SET is_trader = 1 WHERE rowid = ?", self.worldid)

        for ship in self.ships:
            # prepare values
            ship_state = self.ships[ship]

            remaining_ticks = None
            # get current callback in scheduler, according to ship state, to retrieve
            # the number of ticks, when the call will actually be done
            current_callback = None
            if ship_state == self.shipStates.reached_warehouse:
                current_callback = Callback(self.ship_idle, ship)
            if current_callback is not None:
                # current state has a callback
                calls = Scheduler().get_classinst_calls(self, current_callback)
                assert len(
                    calls) == 1, "got {} calls for saving {}: {}".format(
                        len(calls), current_callback, calls)
                remaining_ticks = max(list(calls.values())[0], 1)

            targeted_warehouse = None if ship.worldid not in self.warehouse else self.warehouse[
                ship.worldid].worldid

            # put them in the database
            db(
                "INSERT INTO trader_ships(rowid, state, remaining_ticks, targeted_warehouse) \
			   VALUES(?, ?, ?, ?)", ship.worldid, ship_state.index, remaining_ticks,
                targeted_warehouse)

    def _load(self, db, worldid):
        super(Trader, self)._load(db, worldid)
        self.__init()

    def load_ship_states(self, db):
        # load ships one by one from db (ship instances themselves are loaded already, but
        # we have to use them here)
        for ship_id, state_id, remaining_ticks, targeted_warehouse in \
          db("SELECT rowid, state, remaining_ticks, targeted_warehouse FROM trader_ships"):
            state = self.shipStates[state_id]
            ship = WorldObject.get_object_by_id(ship_id)

            self.ships[ship] = state

            if state == self.shipStates.moving_random:
                ship.add_move_callback(Callback(self.ship_idle, ship))
            elif state == self.shipStates.moving_to_warehouse:
                ship.add_move_callback(Callback(self.reached_warehouse, ship))
                assert targeted_warehouse is not None
                self.warehouse[ship.worldid] = WorldObject.get_object_by_id(
                    targeted_warehouse)
            elif state == self.shipStates.reached_warehouse:
                assert remaining_ticks is not None
                Scheduler().add_new_object(Callback(self.ship_idle, ship),
                                           self, remaining_ticks)

    def get_ship_count(self):
        """Returns number of ships"""
        return len(self.ships)

    def send_ship_random(self, ship):
        """Sends a ship to a random position on the map.
		@param ship: Ship instance that is to be used"""
        super(Trader, self).send_ship_random(ship)
        ship.add_conditional_callback(
            Callback(self._check_for_signal_fire_in_ship_range, ship),
            callback=Callback(self._ship_found_signal_fire, ship))

    def _check_for_signal_fire_in_ship_range(self, ship):
        """Returns the signal fire instance, if there is one in the ships range, else False"""
        if ship in self.allured_by_signal_fire and self.allured_by_signal_fire[
                ship]:
            return False  # don't visit signal fire again
        for tile in self.session.world.get_tiles_in_radius(
                ship.position, ship.radius):
            try:
                if tile.object.id == BUILDINGS.SIGNAL_FIRE:
                    return tile.object
            except AttributeError:
                pass  # tile has no object or object has no id
        return False

    def _ship_found_signal_fire(self, ship):
        signal_fire = self._check_for_signal_fire_in_ship_range(ship)
        self.log.debug("Trader %s ship %s found signal fire %s", self.worldid,
                       ship.worldid, signal_fire)
        # search a warehouse in the range of the signal fire and move to it
        warehouses = self.session.world.get_warehouses()
        for house in warehouses:
            if house.position.distance(signal_fire.position) <= signal_fire.radius and \
               house.owner == signal_fire.owner:
                self.log.debug("Trader %s moving to house %s", self.worldid,
                               house)
                self.allured_by_signal_fire[ship] = True

                # HACK: remove allured flag in a few ticks
                def rem_allured(self, ship):
                    self.allured_by_signal_fire[ship] = False

                Scheduler().add_new_object(Callback(rem_allured, self, ship),
                                           self,
                                           Scheduler().get_ticks(60))
                self.send_ship_random_warehouse(ship, house)
                return
        self.log.debug("Trader can't find warehouse in range of signal fire")

    def send_ship_random_warehouse(self, ship, warehouse=None):
        """Sends a ship to a random warehouse on the map
		@param ship: Ship instance that is to be used
		@param warehouse: warehouse instance to move to. Random one is selected on None."""
        self.log.debug("Trader %s ship %s moving to warehouse (random=%s)",
                       self.worldid, ship.worldid, (warehouse is None))
        #TODO maybe this kind of list should be saved somewhere, as this is pretty performance intense
        warehouses = self.session.world.get_warehouses()
        # Remove all warehouses that are not safe to visit
        warehouses = list(filter(self.is_warehouse_safe, warehouses))
        if not warehouses:  # there aren't any warehouses, move randomly
            self.send_ship_random(ship)
        else:  # select a warehouse
            if warehouse is None:
                self.warehouse[ship.worldid] = self.session.random.choice(
                    warehouses)
            else:
                self.warehouse[ship.worldid] = warehouse
            try:  # try to find a possible position near the warehouse
                ship.move(
                    Circle(self.warehouse[ship.worldid].position.center,
                           ship.radius), Callback(self.reached_warehouse,
                                                  ship))
                self.ships[ship] = self.shipStates.moving_to_warehouse
            except MoveNotPossible:
                self.send_ship_random(ship)

    def is_warehouse_safe(self, warehouse):
        """Checkes whether a warehouse is safe to visit"""
        return not isinstance(
            self.session.world.disaster_manager.get_disaster(
                warehouse.settlement), BlackDeathDisaster)

    def reached_warehouse(self, ship):
        """Actions that need to be taken when reaching a warehouse:
		Sell demanded res, buy offered res, simulate load/unload, continue route.
		@param ship: ship instance"""
        self.log.debug("Trader %s ship %s: reached warehouse", self.worldid,
                       ship.worldid)
        settlement = self.warehouse[ship.worldid].settlement
        # NOTE: must be sorted for mp games (same order everywhere)
        trade_comp = settlement.get_component(TradePostComponent)
        for res in sorted(trade_comp.buy_list.keys(
        )):  # check for resources that the settlement wants to buy
            # select a random amount to sell
            amount = self.session.random.randint(TRADER.SELL_AMOUNT_MIN,
                                                 TRADER.SELL_AMOUNT_MAX)
            # try to sell all, else try smaller pieces
            for try_amount in range(amount, 0, -1):
                price = int(
                    self.session.db.get_res_value(res) *
                    TRADER.PRICE_MODIFIER_SELL * try_amount)
                trade_successful = trade_comp.buy(res, try_amount, price,
                                                  self.worldid)
                self.log.debug(
                    "Trader %s: offered sell %s tons of res %s, success: %s",
                    self.worldid, try_amount, res, trade_successful)
                if trade_successful:
                    break

        # NOTE: must be sorted for mp games (same order everywhere)
        for res in sorted(trade_comp.sell_list.keys()):
            # select a random amount to buy from the settlement
            amount = self.session.random.randint(TRADER.BUY_AMOUNT_MIN,
                                                 TRADER.BUY_AMOUNT_MAX)
            # try to buy all, else try smaller pieces
            for try_amount in range(amount, 0, -1):
                price = int(
                    self.session.db.get_res_value(res) *
                    TRADER.PRICE_MODIFIER_BUY * try_amount)
                trade_successful = trade_comp.sell(res, try_amount, price,
                                                   self.worldid)
                self.log.debug(
                    "Trader %s: offered buy %s tons of res %s, success: %s",
                    self.worldid, try_amount, res, trade_successful)
                if trade_successful:
                    break

        del self.warehouse[ship.worldid]
        # wait a few seconds before going on to simulate loading/unloading process
        Scheduler().add_new_object(
            Callback(self.ship_idle, ship), self,
            Scheduler().get_ticks(TRADER.TRADING_DURATION))
        self.ships[ship] = self.shipStates.reached_warehouse

    def ship_idle(self, ship):
        """Called if a ship is idle. Sends ship to either a random place or warehouse.
		Probability for 'random warehouse' in percent: TRADER.BUSINESS_SENSE.
		@param ship: ship instance"""
        if self.session.random.randint(0, 100) < TRADER.BUSINESS_SENSE:
            # delay one tick, to allow old movement calls to completely finish
            self.log.debug(
                "Trader %s ship %s: idle, moving to random warehouse",
                self.worldid, ship.worldid)
            Scheduler().add_new_object(Callback(
                self.send_ship_random_warehouse, ship),
                                       self,
                                       run_in=0)
        else:
            self.log.debug(
                "Trader %s ship %s: idle, moving to random location",
                self.worldid, ship.worldid)
            Scheduler().add_new_object(Callback(self.send_ship_random, ship),
                                       self,
                                       run_in=0)

    def end(self):
        super(Trader, self).end()
        NewSettlement.unsubscribe(self._on_new_settlement)
Пример #11
0
class Pirate(AIPlayer):
    """A pirate ship moving randomly around. If another ship comes into the reach
	of it, it will be followed for a short time."""

    shipStates = Enum.get_extended(AIPlayer.shipStates, 'chasing_ship',
                                   'going_home')

    log = logging.getLogger("ai.pirate")

    caught_ship_radius = 5
    home_radius = 2
    sight_radius = 15

    def __init__(self, session, id, name, color, **kwargs):
        super(Pirate, self).__init__(session, id, name, color, **kwargs)

        # choose a random water tile on the coast and call it home
        self.home_point = self.session.world.get_random_possible_coastal_ship_position(
        )
        self.log.debug(
            "Pirate: home at (%d, %d), radius %d" %
            (self.home_point.x, self.home_point.y, self.home_radius))

        # create a ship and place it randomly (temporary hack)
        point = self.session.world.get_random_possible_ship_position()
        ship = CreateUnit(self.worldid, UNITS.PIRATE_SHIP_CLASS, point.x,
                          point.y)(issuer=self.session.world.player)
        self.ships[ship] = self.shipStates.idle
        self.calculate_visibility_points(ship)

        for ship in self.ships.keys():
            Scheduler().add_new_object(Callback(self.send_ship, ship), self)
            Scheduler().add_new_object(Callback(self.lookout, ship), self, 8,
                                       -1)

    @staticmethod
    def get_nearest_player_ship(base_ship):
        lowest_distance = None
        nearest_ship = None
        for ship in base_ship.find_nearby_ships():
            if isinstance(ship, (PirateShip, TradeShip)):
                continue  # don't attack these ships
            distance = base_ship.position.distance_to_point(ship.position)
            if lowest_distance is None or distance < lowest_distance:
                lowest_distance = distance
                nearest_ship = ship
        return nearest_ship

    def lookout(self, pirate_ship):
        if self.ships[pirate_ship] != self.shipStates.going_home:
            ship = self.get_nearest_player_ship(pirate_ship)
            if ship:
                self.log.debug("Pirate: Scout found ship: %s" % ship.name)
                self.send_ship(pirate_ship)
            else:
                self.predict_player_position(pirate_ship)

    def save(self, db):
        super(Pirate, self).save(db)
        db("UPDATE player SET is_pirate = 1 WHERE rowid = ?", self.worldid)
        db("INSERT INTO pirate_home_point(x, y) VALUES(?, ?)",
           self.home_point.x, self.home_point.y)

        for ship in self.ships:
            # prepare values
            ship_state = self.ships[ship]
            current_callback = Callback(self.lookout, ship)
            calls = Scheduler().get_classinst_calls(self, current_callback)
            assert len(calls) == 1, "got %s calls for saving %s: %s" % (
                len(calls), current_callback, calls)
            remaining_ticks = max(calls.values()[0], 1)

            db(
                "INSERT INTO pirate_ships(rowid, state, remaining_ticks) VALUES(?, ?, ?)",
                ship.worldid, ship_state.index, remaining_ticks)

    def _load(self, db, worldid):
        super(Pirate, self)._load(db, worldid)
        home = db("SELECT x, y FROM pirate_home_point")[0]
        self.home_point = Point(home[0], home[1])
        self.log.debug(
            "Pirate: home at (%d, %d), radius %d" %
            (self.home_point.x, self.home_point.y, self.home_radius))

    def load_ship_states(self, db):
        # load ships one by one from db (ship instances themselves are loaded already, but
        # we have to use them here)
        for ship_id, state_id, remaining_ticks in \
          db("SELECT rowid, state, remaining_ticks FROM pirate_ships"):
            state = self.shipStates[state_id]
            ship = WorldObject.get_object_by_id(ship_id)
            self.ships[ship] = state
            assert remaining_ticks is not None
            Scheduler().add_new_object(Callback(self.lookout, ship), self,
                                       remaining_ticks, -1, 8)
            ship.add_move_callback(Callback(self.ship_idle, ship))
            self.calculate_visibility_points(ship)

    def send_ship(self, pirate_ship):
        self.log.debug(
            'Pirate %s: send_ship(%s) start transition: %s' %
            (self.worldid, pirate_ship.name, self.ships[pirate_ship]))
        done = False

        #transition the pirate ship state to 'idle' once it is inside home circumference
        if pirate_ship.position.distance_to_point(self.home_point) <= self.home_radius and \
         self.ships[pirate_ship] == self.shipStates.going_home:
            self.ships[pirate_ship] = self.shipStates.idle
            self.log.debug(
                'Pirate %s: send_ship(%s) new state: %s' %
                (self.worldid, pirate_ship.name, self.ships[pirate_ship]))

        if self.ships[pirate_ship] != self.shipStates.going_home:
            if self._chase_closest_ship(pirate_ship):
                done = True

        if not done:
            ship = self.get_nearest_player_ship(pirate_ship)
            if self.ships[pirate_ship] == self.shipStates.chasing_ship and (ship is None or \
              ship.position.distance_to_point(pirate_ship.position) <= self.caught_ship_radius):
                # caught the ship, go home
                for point in self.session.world.get_points_in_radius(
                        self.home_point, self.home_radius, shuffle=True):
                    try:
                        pirate_ship.move(point,
                                         Callback(self.send_ship, pirate_ship))
                    except MoveNotPossible:
                        continue
                    self.log.debug(
                        'Pirate %s: send_ship(%s) going home (%d, %d)' %
                        (self.worldid, pirate_ship.name, point.x, point.y))
                    self.ships[pirate_ship] = self.shipStates.going_home
                    done = True
                    break

        #visit those points which are most frequented by player
        if not done and self.ships[pirate_ship] != self.shipStates.going_home:
            self.predict_player_position(pirate_ship)

        self.log.debug(
            'Pirate %s: send_ship(%s) new state: %s' %
            (self.worldid, pirate_ship.name, self.ships[pirate_ship]))

    def _chase_closest_ship(self, pirate_ship):
        ship = self.get_nearest_player_ship(pirate_ship)
        if ship:
            # The player is in sight here
            # Note down all the point's visibility the player belongs to
            for point in self.visibility_points:
                if ship.position.distance_to_point(
                        point[0]) <= self.sight_radius:
                    point[
                        1] += 1  # point[1] represents the chance of seeing a player
            if ship.position.distance_to_point(
                    pirate_ship.position) <= self.caught_ship_radius:
                self.ships[pirate_ship] = self.shipStates.chasing_ship
                return False  # already caught it

            # move ship there:
            for point in self.session.world.get_points_in_radius(
                    ship.position, self.caught_ship_radius - 1):
                try:
                    pirate_ship.move(point,
                                     Callback(self.send_ship, pirate_ship))
                except MoveNotPossible:
                    continue
                self.log.debug(
                    'Pirate %s: chasing %s (next point %d, %d)' %
                    (self.worldid, pirate_ship.name, point.x, point.y))
                self.ships[pirate_ship] = self.shipStates.chasing_ship
                return True
        return False

    def predict_player_position(self, pirate_ship):
        """Moves the pirate_ship to one of it's visibility points where 
		   the player has spent the maximum time"""

        self.ships[pirate_ship] = self.shipStates.moving_random
        self.log.debug(
            'Pirate %s: send_ship(%s) new state: %s' %
            (self.worldid, pirate_ship.name, self.ships[pirate_ship]))
        # See if we have reached the destination
        if pirate_ship.position.distance_to_point(
                self.visibility_points[0][0]) <= self.home_radius:
            # Every unsuccessful try halves the chances of ship being on that point next time
            self.visibility_points[0][1] = self.visibility_points[0][1] / 2

            self.visibility_points.sort(key=self._sort_preference)
            max_point = self.visibility_points[0]
            if max_point[1] is 0:  # choose a random location in this case
                # TODO: If this seems inefficient in larger maps, change to 'shift elements by 1'
                index = self.session.random.randint(
                    0,
                    len(self.visibility_points) - 1)
                max_point = self.visibility_points[index]
                self.visibility_points[index] = self.visibility_points[0]
                self.visibility_points[0] = max_point
            self.visible_player = [True] * len(self.visibility_points)
            self.log.debug(
                'Pirate %s: ship %s, Frequencies of visibility_points:' %
                (self.worldid, pirate_ship.name))
            for point in self.visibility_points:
                self.log.debug('(%d, %d) -> %d' %
                               (point[0].x, point[0].y, point[1]))
        # Consider the points that are being visited while visiting the destination point, and reduce
        # the frequency values for them
        elif len(self.visible_player) is len(self.visibility_points):
            for point in self.visibility_points:
                index = self.visibility_points.index(point)
                if self.visible_player[index] and pirate_ship.position.distance_to_point\
                 (self.visibility_points[index][0]) <= self.home_radius:
                    self.visibility_points[index][
                        1] = self.visibility_points[index][1] / 2
                    self.visible_player[index] = False
        try:
            pirate_ship.move(self.visibility_points[0][00])
        except MoveNotPossible:
            self.notify_unit_path_blocked(pirate_ship)
            return

    def _sort_preference(self, a):
        return -a[1]

    def calculate_visibility_points(self, pirate_ship):
        """Finds out the points from which the entire map will be accessible.
		Should be called only once either during initialization or loading"""
        rect = self.session.world.map_dimensions.get_corners()
        x_min, x_max, y_min, y_max = rect[0][0], rect[1][0], rect[0][0], rect[
            2][1]

        # Necessary points (with tolerable overlapping) which cover the entire map
        # Each element contains two values: it's co-ordinate, and frequency of player ship's visit
        self.visibility_points = []
        y = y_min + self.sight_radius
        while y <= y_max:
            x = x_min + self.sight_radius
            while x <= x_max:
                self.visibility_points.append([Point(x, y), 0])
                x += self.sight_radius  # should be 2*self.sight_radius for no overlap
            y += self.sight_radius

        self.log.debug("Pirate %s: %s, visibility points" %
                       (self.worldid, pirate_ship.name))
        copy_points = list(self.visibility_points)
        for point in copy_points:
            if not self.session.world.get_tile(point[0]).is_water:
                # Make a position compromise to obtain a water tile on the point
                for x in (3, -3, 2, -2, 1, -1, 0):
                    for y in (3, -3, 2, -2, 1, -1, 0):
                        new_point = Point(point[0].x + x, point[0].y + y)
                        if self.session.world.get_tile(new_point).is_water:
                            self.visibility_points.remove(
                                point)  #remove old point
                            point[0] = new_point
                            self.visibility_points.append([point[0],
                                                           0])  #add new point
                            break
                    if self.session.world.get_tile(point[0]).is_water: break
                if not self.session.world.get_tile(point[0]).is_water:
                    self.visibility_points.remove(point)
                    continue
            self.log.debug("(%d, %d)" % (point[0].x, point[0].y))
        # A list needed to reduce visibility frequencies of a point if player is not sighted on that point
        self.visible_player = []
Пример #12
0
class JobList(list):
    """Data structure for evaluating best jobs.
	It's a list extended by special sort functions.
	"""
    order_by = Enum('rating', 'amount', 'random', 'fewest_available',
                    'fewest_available_and_distance', 'for_storage_collector')

    def __init__(self, collector, job_order):
        """
		@param collector: collector instance
		@param job_order: instance of order_by-Enum
		"""
        super(JobList, self).__init__()
        self.collector = collector
        # choose acctual function by name of enum value
        sort_fun_name = '_sort_jobs_' + str(job_order)
        if not hasattr(self, sort_fun_name):
            self.sort_jobs = self._sort_jobs_rating
            print 'WARNING: invalid job order: ', job_order
        else:
            self.sort_jobs = getattr(self, sort_fun_name)

    def sort_jobs(self, obj):
        """Call this to sort jobs"""
        # (this is overwritten in __init__)
        raise NotImplementedError

    def _sort_jobs_rating(self):
        """Sorts jobs by job rating"""
        self.sort(key=operator.attrgetter('rating'), reverse=True)

    def _sort_jobs_random(self):
        """Sorts jobs randomly"""
        self.collector.session.random.shuffle(self)

    def _sort_jobs_amount(self):
        """Sorts the jobs by the amount of resources available"""
        self.sort(key=operator.attrgetter('amount'), reverse=True)

    def _sort_jobs_fewest_available(self, shuffle_first=True):
        """Prefer jobs where least amount is available in obj's inventory"""
        # shuffle list before sorting, so that jobs with same value have equal chance
        if shuffle_first:
            self.collector.session.random.shuffle(self)
        inventory = self.collector.get_home_inventory()
        self.sort(key=lambda job: inventory[job.res], reverse=False)

    def _sort_jobs_fewest_available_and_distance(self):
        """Sort jobs by fewest available, but secondaryly also consider distance"""
        # python sort is stable, so two sequenced sorts work.
        self._sort_distance()
        self._sort_jobs_fewest_available(shuffle_first=False)

    def _sort_jobs_for_storage_collector(self):
        """Special sophisticated sorting routing for storage collectors.
		Same as fewest_available_and_distance_, but also considers whether target inv is full."""
        self._sort_jobs_fewest_available_and_distance()
        self._sort_target_inventory_full()

    def _sort_distance(self):
        """Prefer targets that are nearer"""
        self.sort(key=lambda job: self.collector.position.distance(
            job.object.loading_area))

    def _sort_target_inventory_full(self):
        """Prefer targets with full inventory"""
        self.sort(key=operator.attrgetter('target_inventory_full'),
                  reverse=True)

    def __str__(self):
        return str([str(i) for i in self])
Пример #13
0
class InternationalTrade(ShipMission):
    """
	Given a ship, a settlement_manager of our settlement, a settlement of another player,
	and either a resource to be bought or sold (or both) the ship will load/unload the
	required resources at our settlement and do the necessary trading at the other player's one.
	"""

    missionStates = Enum('created', 'moving_to_my_settlement',
                         'moving_to_other_settlement',
                         'returning_to_my_settlement')

    def __init__(self, settlement_manager, settlement, ship, bought_resource,
                 sold_resource, success_callback, failure_callback):
        super(InternationalTrade, self).__init__(success_callback,
                                                 failure_callback, ship)
        assert sold_resource is not None or bought_resource is not None
        self.settlement_manager = settlement_manager
        self.settlement = settlement
        self.bought_resource = bought_resource
        self.sold_resource = sold_resource
        self.state = self.missionStates.created

    def save(self, db):
        super(InternationalTrade, self).save(db)
        db(
            "INSERT INTO ai_mission_international_trade(rowid, settlement_manager, settlement, ship, bought_resource, sold_resource, state) VALUES(?, ?, ?, ?, ?, ?, ?)",
            self.worldid, self.settlement_manager.worldid,
            self.settlement.worldid, self.ship.worldid, self.bought_resource,
            self.sold_resource, self.state.index)

    @classmethod
    def load(cls, db, worldid, success_callback, failure_callback):
        self = cls.__new__(cls)
        self._load(db, worldid, success_callback, failure_callback)
        return self

    def _load(self, db, worldid, success_callback, failure_callback):
        db_result = db(
            "SELECT settlement_manager, settlement, ship, bought_resource, sold_resource, state FROM ai_mission_international_trade WHERE rowid = ?",
            worldid)[0]
        self.settlement_manager = WorldObject.get_object_by_id(db_result[0])
        self.settlement = WorldObject.get_object_by_id(db_result[1])
        self.bought_resource = db_result[3]
        self.sold_resource = db_result[4]
        self.state = self.missionStates[db_result[5]]
        super(InternationalTrade,
              self).load(db, worldid, success_callback, failure_callback,
                         WorldObject.get_object_by_id(db_result[2]))

        if self.state is self.missionStates.moving_to_my_settlement:
            self.ship.add_move_callback(Callback(self._reached_my_settlement))
            self.ship.add_blocked_callback(
                Callback(self._move_to_my_settlement))
        elif self.state is self.missionStates.moving_to_other_settlement:
            self.ship.add_move_callback(
                Callback(self._reached_other_settlement))
            self.ship.add_blocked_callback(
                Callback(self._move_to_other_settlement))
        elif self.state is self.missionStates.returning_to_my_settlement:
            self.ship.add_move_callback(
                Callback(self._returned_to_my_settlement))
            self.ship.add_blocked_callback(
                Callback(self._return_to_my_settlement))
        else:
            assert False, 'invalid state'

    def start(self):
        if self.sold_resource is not None:
            self.state = self.missionStates.moving_to_my_settlement
            self._move_to_my_settlement()
        else:
            self.state = self.missionStates.moving_to_other_settlement
            self._move_to_other_settlement()
        self.log.info(
            '%s started an international trade mission between %s and %s to sell %s and buy %s using %s',
            self,
            self.settlement_manager.settlement.get_component(
                NamedComponent).name,
            self.settlement.get_component(NamedComponent).name,
            self.sold_resource, self.bought_resource, self.ship)

    def _move_to_my_settlement(self):
        self._move_to_warehouse_area(
            self.settlement_manager.settlement.warehouse.position,
            Callback(self._reached_my_settlement),
            Callback(self._move_to_my_settlement),
            'Unable to move to my settlement (%s)' %
            self.settlement_manager.settlement.get_component(
                NamedComponent).name)

    def _get_max_sellable_amount(self, available_amount):
        if self.sold_resource not in self.settlement.get_component(
                TradePostComponent).buy_list:
            return 0
        if self.settlement.get_component(TradePostComponent).buy_list[
                self.sold_resource] >= self.settlement.get_component(
                    StorageComponent).inventory[self.sold_resource]:
            return 0
        if available_amount <= 0:
            return 0
        price = int(
            self.owner.session.db.get_res_value(self.sold_resource) *
            TRADER.PRICE_MODIFIER_SELL)
        return min(
            self.settlement.get_component(StorageComponent).inventory[
                self.sold_resource] - self.settlement.get_component(
                    TradePostComponent).buy_list[self.sold_resource],
            self.settlement.owner.get_component(StorageComponent).inventory[
                RES.GOLD] // price, available_amount)

    def _reached_my_settlement(self):
        self.log.info(
            '%s reached my warehouse area (%s)', self,
            self.settlement_manager.settlement.get_component(
                NamedComponent).name)
        available_amount = max(
            0,
            self.settlement_manager.settlement.get_component(
                StorageComponent).inventory[self.sold_resource] -
            self.settlement_manager.resource_manager.resource_requirements[
                self.sold_resource])
        sellable_amount = self._get_max_sellable_amount(available_amount)
        if sellable_amount <= 0:
            self.log.info('%s no resources can be sold', self)
            if self.bought_resource is None:
                self.report_failure('No resources need to be sold nor bought')
                return
        else:
            self.move_resource(self.ship, self.settlement_manager.settlement,
                               self.sold_resource, -sellable_amount)
            self.log.info('%s loaded resources', self)
        self.state = self.missionStates.moving_to_other_settlement
        self._move_to_other_settlement()

    def _move_to_other_settlement(self):
        self._move_to_warehouse_area(
            self.settlement.warehouse.position,
            Callback(self._reached_other_settlement),
            Callback(self._move_to_other_settlement),
            'Unable to move to the other settlement (%s)' %
            self.settlement.get_component(NamedComponent).name)

    def _get_max_buyable_amount(self):
        if self.bought_resource is None:
            return 0
        if self.bought_resource not in self.settlement.get_component(
                TradePostComponent).sell_list:
            return 0
        if self.settlement.get_component(TradePostComponent).sell_list[
                self.bought_resource] >= self.settlement.get_component(
                    StorageComponent).inventory[self.bought_resource]:
            return 0
        needed_amount = self.settlement_manager.resource_manager.resource_requirements[self.bought_resource] - \
         self.settlement_manager.settlement.get_component(StorageComponent).inventory[self.bought_resource]
        if needed_amount <= 0:
            return 0
        price = int(
            self.owner.session.db.get_res_value(self.bought_resource) *
            TRADER.PRICE_MODIFIER_BUY)
        return min(
            self.settlement.get_component(StorageComponent).inventory[
                self.bought_resource] - self.settlement.get_component(
                    TradePostComponent).sell_list[self.bought_resource],
            self.settlement_manager.owner.get_component(
                StorageComponent).inventory[RES.GOLD] // price, needed_amount)

    def _reached_other_settlement(self):
        self.log.info('%s reached the other warehouse area (%s)', self,
                      self.settlement.get_component(NamedComponent).name)
        if self.sold_resource is not None:
            sellable_amount = self._get_max_sellable_amount(
                self.ship.get_component(StorageComponent).inventory[
                    self.sold_resource])
            if sellable_amount > 0:
                BuyResource(self.settlement.get_component(TradePostComponent),
                            self.ship, self.sold_resource,
                            sellable_amount).execute(self.owner.session)
                if self.bought_resource is None:
                    self.report_success('Sold %d of resource %d' %
                                        (sellable_amount, self.sold_resource))
                    return
                else:
                    self.log.info('%s sold %d of resource %d', self,
                                  sellable_amount, self.sold_resource)

        buyable_amount = self._get_max_buyable_amount()
        if buyable_amount <= 0:
            self.report_failure('No resources can be bought')
            return

        SellResource(self.settlement.get_component(TradePostComponent),
                     self.ship, self.bought_resource,
                     buyable_amount).execute(self.owner.session)
        self.log.info('%s bought %d of resource %d', self, buyable_amount,
                      self.bought_resource)
        self.state = self.missionStates.returning_to_my_settlement
        self._return_to_my_settlement()

    def _return_to_my_settlement(self):
        self._move_to_warehouse_area(
            self.settlement_manager.settlement.warehouse.position,
            Callback(self._returned_to_my_settlement),
            Callback(self._return_to_my_settlement), 'Unable to return to %s' %
            self.settlement_manager.settlement.get_component(
                NamedComponent).name)

    def _returned_to_my_settlement(self):
        self._unload_all_resources(self.settlement_manager.settlement)
        self.report_success('Unloaded the bought resources at %s' %
                            self.settlement_manager.settlement.get_component(
                                NamedComponent).name)
Пример #14
0
class Collector(StorageHolder, Unit):
    """Base class for every collector. Does not depend on any home building.

	Timeline:
	 * search_job
	 * * get_job
	 * * handle_no_possible_job
	 * * begin_current_job
	 * * * setup_new_job
	 * * * move to target
	 on arrival there:
	 * begin_working
	 after some pretended working:
	 * finish_working
	 * * transfer_res
	 after subclass has done actions to finish job:
	 * end_job
	"""
    log = logging.getLogger("world.units.collector")

    work_duration = COLLECTORS.DEFAULT_WORK_DURATION  # default is 16
    destination_always_in_building = False

    # all states, any (subclass) instance may have. Keeping a list in one place
    # is important, because every state must have a distinct number.
    # Handling of subclass specific states is done by subclass.
    states = Enum('idle', # doing nothing, waiting for job
                  'moving_to_target', \
                  'working', \
                  'moving_home', \
                  'waiting_for_animal_to_stop', # herder: wait for job target to finish for collecting
                  'waiting_for_herder', # animal: has stopped, now waits for herder
                  'no_job_walking_randomly', # animal: like idle, but moving instead of standing still
                  'no_job_waiting', # animal: as idle, but no move target can be found
                  # TODO: merge no_job_waiting with idle
                  'decommissioned', # fisher ship: When home building got demolished. No more collecting.
                  )

    # INIT/DESTRUCT

    def __init__(self, x, y, slots=1, size=4, start_hidden=True, **kwargs):
        super(Collector, self).__init__(slots = slots, \
                                        size = size, \
                                        x = x, \
                                        y = y, \
                                        **kwargs)

        self.inventory.limit = size
        # TODO: use different storage to support multiple slots. see StorageHolder

        self.__init(self.states.idle, start_hidden)

        # start searching jobs just when construction (of subclass) is completed
        Scheduler().add_new_object(self.search_job, self, 1)

    def __init(self, state, start_hidden):
        self.state = state
        self.start_hidden = start_hidden
        if self.start_hidden:
            self.hide()

        self.job = None  # here we store the current job as Job object

        # list of class ids of buildings, where we may pick stuff up
        # empty means pick up from everywhere
        # NOTE: this is not allowed to change at runtime.
        self.possible_target_classes = []
        for (object_class, ) in self.session.db(
                "SELECT object FROM collector_restrictions WHERE \
		                                        collector = ?", self.id):
            self.possible_target_classes.append(object_class)
        self.is_restricted = (len(self.possible_target_classes) != 0)

    def remove(self):
        """Removes the instance. Useful when the home building is destroyed"""
        self.log.debug("%s: remove called", self)
        # remove from target collector list
        if self.job is not None and self.state != self.states.moving_home:
            # in the move_home state, there still is a job, but the collector is already deregistered
            self.job.object.remove_incoming_collector(self)
        self.hide()
        self.job = None
        super(Collector, self).remove()

    # SAVE/LOAD

    def save(self, db):
        super(Collector, self).save(db)

        # save state and remaining ticks for next callback
        # retrieve remaining ticks according current callback according to state
        current_callback = None
        remaining_ticks = None
        if self.state == self.states.idle:
            current_callback = self.search_job
        elif self.state == self.states.working:
            current_callback = self.finish_working
        if current_callback is not None:
            calls = Scheduler().get_classinst_calls(self, current_callback)
            assert len(calls) == 1, 'Collector should have callback %s scheduled, but has %s' % \
                    (current_callback, [ str(i) for i in Scheduler().get_classinst_calls(self).keys() ])
            remaining_ticks = max(calls.values()[0], 1)  # save a number > 0

        db("INSERT INTO collector(rowid, state, remaining_ticks, start_hidden) VALUES(?, ?, ?, ?)", \
           self.worldid, self.state.index, remaining_ticks, self.start_hidden)

        # save the job
        if self.job is not None:
            obj_id = -1 if self.job.object is None else self.job.object.worldid
            db("INSERT INTO collector_job(rowid, object, resource, amount) VALUES(?, ?, ?, ?)", \
               self.worldid, obj_id, self.job.res, self.job.amount)

    def load(self, db, worldid):
        super(Collector, self).load(db, worldid)

        # load collector properties
        state_id, remaining_ticks, start_hidden = \
                db("SELECT state, remaining_ticks, start_hidden FROM collector \
		            WHERE rowid = ?"                                    , worldid)[0]
        self.__init(self.states[state_id], start_hidden)

        # load job
        job_db = db(
            "SELECT object, resource, amount FROM collector_job WHERE rowid = ?",
            worldid)
        if (len(job_db) > 0):
            job_db = job_db[0]
            # create job with worldid of object as object. This is used to defer the target resolution,
            # which might not have been loaded
            self.job = Job(job_db[0], job_db[1], job_db[2])

        # apply state when job object is loaded for sure
        Scheduler().add_new_object(Callback(self.apply_state, self.state,
                                            remaining_ticks),
                                   self,
                                   run_in=0)

    def apply_state(self, state, remaining_ticks=None):
        """Takes actions to set collector to a state. Useful after loading.
		@param state: EnumValue from states
		@param remaining_ticks: ticks after which current state is finished
		"""
        if state == self.states.idle:
            # we do nothing, so schedule a new search for a job
            Scheduler().add_new_object(self.search_job, self, remaining_ticks)
        elif state == self.states.moving_to_target:
            # we are on the way to target, so save the job
            self.setup_new_job()
            # and notify us, when we're at target
            self.add_move_callback(self.begin_working)
            self.show()
        elif state == self.states.working:
            # we are at the target and work
            # register the new job
            self.setup_new_job()
            # job finishes in remaining_ticks ticks
            Scheduler().add_new_object(self.finish_working, self,
                                       remaining_ticks)

    # GETTER

    def get_home_inventory(self):
        """Returns inventory where collected res will be stored.
		This could be the inventory of a home_building, or it's own.
		"""
        raise NotImplementedError

    def get_colleague_collectors(self):
        """Returns a list of collectors, that work for the same "inventory"."""
        return []

    def get_job(self):
        """Returns the next job or None"""
        raise NotImplementedError

    # BEHAVIOUR
    def search_job(self):
        """Search for a job, only called if the collector does not have a job.
		If no job is found, a new search will be scheduled in a few ticks."""
        self.job = self.get_job()
        if self.job is None:
            self.handle_no_possible_job()
        else:
            self.begin_current_job()

    def handle_no_possible_job(self):
        """Called when we can't find a job. default is to wait and try again in a few secs"""
        self.log.debug("%s: found no possible job, retry in %s ticks", \
                       (self, COLLECTORS.DEFAULT_WAIT_TICKS) )
        Scheduler().add_new_object(self.search_job, self,
                                   COLLECTORS.DEFAULT_WAIT_TICKS)

    def setup_new_job(self):
        """Executes the necessary actions to begin a new job"""
        self.job.object.add_incoming_collector(self)

    @decorators.cachedmethod
    def check_possible_job_target(self, target):
        """Checks our if we "are allowed" and able to pick up from the target"""
        # Discard building if it works for same inventory (happens when both are storage buildings
        # or home_building is checked out)
        if target.inventory == self.get_home_inventory():
            #self.log.debug("nojob: same inventory")
            return False

        # check if we're allowed to pick up there
        if self.is_restricted and target.id not in self.possible_target_classes:
            #self.log.debug("nojob: %s is restricted", target.id)
            return False

        # pathfinding would fit in here, but it's too expensive,
        # we just do that at targets where we are sure to get a lot of res later on.

        return True

    @decorators.make_constants()
    def check_possible_job_target_for(self, target, res):
        """Checks out if we could get res from target.
		Does _not_ check for anything else (e.g. if we are able to walk there).
		@param target: possible target. buildings are supported, support for more can be added.
		@param res: resource id
		@return: instance of Job or None, if we can't collect anything
		"""
        res_amount = target.get_available_pickup_amount(res, self)
        if res_amount <= 0:
            #self.log.debug("nojob: no pickup amount")
            return None

        # check if other collectors get this resource, because our inventory could
        # get full if they arrive.
        total_registered_amount_consumer = sum([ collector.job.amount for collector in \
                                                 self.get_colleague_collectors() if \
                                                 collector.job is not None and \
                                                 collector.job.res == res ])

        inventory = self.get_home_inventory()

        # check if there are resources left to pickup
        home_inventory_free_space = inventory.get_limit(res) - \
                                (total_registered_amount_consumer + inventory[res])
        if home_inventory_free_space <= 0:
            #self.log.debug("nojob: no home inventory space")
            return None

        collector_inventory_free_space = self.inventory.get_free_space_for(res)
        if collector_inventory_free_space <= 0:
            #self.log.debug("nojob: no collector inventory space")
            return None

        possible_res_amount = min(res_amount, home_inventory_free_space, \
                                  collector_inventory_free_space)

        target_inventory_full = (target.inventory.get_free_space_for(res) == 0)

        # create a new job.
        return Job(target, res, possible_res_amount, target_inventory_full)

    def get_best_possible_job(self, jobs):
        """Return best possible job from jobs.
		"Best" means that the job is highest when the job list was sorted.
		"Possible" means that we can find a path there.
		@param jobs: unsorted JobList instance
		@return: selected Job instance from list or None if no jobs are possible."""
        jobs.sort_jobs()
        # check if we can move to that targets
        for job in jobs:
            if self.check_move(job.object.loading_area):
                return job

        return None

    def begin_current_job(self, job_location=None):
        """Starts executing the current job by registering itself and moving to target.
		@param job_location: Where collector should work. default: job.object.loading_area"""
        self.log.debug("%s prepares job %s", self, self.job)
        self.setup_new_job()
        self.show()
        if job_location is None:
            job_location = self.job.object.loading_area
        self.move(job_location, self.begin_working, \
                  destination_in_building = self.destination_always_in_building)
        self.state = self.states.moving_to_target

    def begin_working(self):
        """Pretends that the collector works by waiting some time. finish_working is
		called after that time."""
        self.log.debug("%s begins working", self)
        assert self.job is not None, '%s job is non in begin_working' % self
        Scheduler().add_new_object(self.finish_working, self,
                                   self.work_duration)
        # play working sound
        if self.soundfiles:
            self.play_ambient(self.soundfiles[0], looping=False)
        self.state = self.states.working

    def finish_working(self):
        """Called when collector has stayed at the target for a while.
		Picks up the resources.
		Should be overridden to specify what the collector should do after this."""
        self.log.debug("%s finished working", self)
        self.act("idle", self._instance.getFacingLocation(), True)
        # transfer res
        self.transfer_res_from_target()
        # deregister at the target we're at
        self.job.object.remove_incoming_collector(self)
        # stop playing ambient sound if any
        if self.soundfiles:
            self.stop_sound()

    def transfer_res_from_target(self):
        """Transfers resources from target to collector inventory"""
        res_amount = self.job.object.pickup_resources(self.job.res,
                                                      self.job.amount, self)
        if res_amount != self.job.amount:
            self.job.amount = res_amount  # update job amount
        remnant = self.inventory.alter(self.job.res, res_amount)
        assert remnant == 0, "%s couldn't take all of res %s; remnant: %s; planned: %s; acctual %s" % \
               (self, self.job.res, remnant, self.job.amount, res_amount)

    def transfer_res_to_home(self, res, amount):
        """Transfer resources from collector to the home inventory"""
        self.log.debug("%s brought home %s of %s", self, amount, res)
        remnant = self.get_home_inventory().alter(res, amount)
        #assert remnant == 0, "Home building could not take all resources from collector."
        remnant = self.inventory.alter(res, -amount)
        assert remnant == 0, "%s couldn't give all of res %s; remnant: %s; inventory: %s" % \
               (self, res, remnant, self.inventory)

    """ unused for now
	def reroute(self):
		""Reroutes the collector to a different job.
		Can be called the current job can't be executed any more""
		raise NotImplementedError
	"""

    def end_job(self):
        """Contrary to setup_new_job"""
        # he finished the job now
        # before the new job can begin this will be executed
        self.log.debug("%s end_job - waiting for new search_job", self)
        if self.start_hidden:
            self.hide()
        self.job = None
        Scheduler().add_new_object(self.search_job, self,
                                   COLLECTORS.DEFAULT_WAIT_TICKS)
        self.state = self.states.idle

    def cancel(self, continue_action):
        """Aborts the current job.
		@param continue_action: Callback, gets called after cancel. Specifies what collector
			                      is supposed to now.
		"""
        self.log.debug("%s was cancel, continue action is %s", self,
                       continue_action)
        if self.job is not None:
            # remove us as incoming collector at target
            if self.state != self.states.moving_home:
                # in the moving_home state, the job object still exists,
                # but the collector is already deregistered
                self.job.object.remove_incoming_collector(self)
            # clean up depending on state
            if self.state == self.states.working:
                removed_calls = Scheduler().rem_call(self, self.finish_working)
                assert removed_calls == 1, 'removed %s calls instead of one' % removed_calls
            self.job = None
            self.state = self.states.idle
        continue_action()

    def __str__(self):
        try:
            return super(Collector, self).__str__() + "(state=%s)" % self.state
        except AttributeError:  # state has not been set
            return super(Collector, self).__str__()
Пример #15
0
class FoundSettlement(ShipMission):
	"""
	Given a ship with the required resources and the coordinates of the future warehouse
	the ship is taken near the end location and a warehouse is built.
	"""

	missionStates = Enum('created', 'moving')

	def __init__(self, success_callback, failure_callback, land_manager, ship, coords):
		super(FoundSettlement, self).__init__(success_callback, failure_callback, ship)
		self.land_manager = land_manager
		self.coords = coords
		self.warehouse = None
		self.state = self.missionStates.created

	def save(self, db):
		super(FoundSettlement, self).save(db)
		db("INSERT INTO ai_mission_found_settlement(rowid, land_manager, ship, x, y, state) VALUES(?, ?, ?, ?, ?, ?)",
			self.worldid, self.land_manager.worldid, self.ship.worldid, self.coords[0], self.coords[1], self.state.index)

	@classmethod
	def load(cls, db, worldid, success_callback, failure_callback):
		self = cls.__new__(cls)
		self._load(db, worldid, success_callback, failure_callback)
		return self

	def _load(self, db, worldid, success_callback, failure_callback):
		db_result = db("SELECT land_manager, ship, x, y, state FROM ai_mission_found_settlement WHERE rowid = ?", worldid)[0]
		self.land_manager = WorldObject.get_object_by_id(db_result[0])
		self.coords = (int(db_result[2]), int(db_result[3]))
		self.warehouse = None
		self.state = self.missionStates[db_result[4]]
		super(FoundSettlement, self).load(db, worldid, success_callback, failure_callback, WorldObject.get_object_by_id(db_result[1]))

		if self.state == self.missionStates.moving:
			self.ship.add_move_callback(Callback(self._reached_destination_area))
			self.ship.add_blocked_callback(Callback(self._move_to_destination_area))
		else:
			assert False, 'invalid state'

	def start(self):
		self.state = self.missionStates.moving
		self._move_to_destination_area()

	def _move_to_destination_area(self):
		if self.coords is None:
			self.report_failure('No possible warehouse location')
			return

		self._move_to_warehouse_area(Point(*self.coords), Callback(self._reached_destination_area),
			Callback(self._move_to_destination_area), 'Move not possible')

	def _reached_destination_area(self):
		self.log.info('%s reached BO area', self)

		builder = BasicBuilder(BUILDINGS.WAREHOUSE, self.coords, 0)
		self.warehouse = builder.execute(self.land_manager, ship=self.ship)
		if not self.warehouse:
			self.report_failure('Unable to build the warehouse')
			return

		self.land_manager.settlement = self.warehouse.settlement
		self.log.info('%s built the warehouse', self)

		self._unload_all_resources(self.land_manager.settlement)
		self.report_success('Built the warehouse, transferred resources')

	@classmethod
	def find_warehouse_location(cls, ship, land_manager):
		"""Return the coordinates of a location for the warehouse on the given island."""
		warehouse_class = Entities.buildings[BUILDINGS.WAREHOUSE]
		pos_offsets = []
		for dx in xrange(warehouse_class.width):
			for dy in xrange(warehouse_class.height):
				pos_offsets.append((dx, dy))

		island = land_manager.island
		personality = land_manager.owner.personality_manager.get('FoundSettlement')
		too_close_penalty_threshold_sq = personality.too_close_penalty_threshold * personality.too_close_penalty_threshold

		available_spots_list = list(sorted(island.terrain_cache.cache[warehouse_class.terrain_type][warehouse_class.size].intersection(island.available_land_cache.cache[warehouse_class.size])))
		if not available_spots_list:
			return None

		options = []
		limited_spots = island.session.random.sample(available_spots_list, min(len(available_spots_list), personality.max_options))
		for (x, y) in limited_spots:
			cost = 0
			for (x2, y2) in land_manager.village:
				dx = x2 - x
				dy = y2 - y
				distance = (dx * dx + dy * dy) ** 0.5
				if distance < personality.too_close_penalty_threshold:
					cost += personality.too_close_constant_penalty + personality.too_close_linear_penalty / (distance + 1.0)
				else:
					cost += distance

			for settlement_manager in land_manager.owner.settlement_managers:
				cost += settlement_manager.settlement.warehouse.position.distance((x, y)) * personality.linear_warehouse_penalty
			options.append((cost, x, y))

		for _, x, y in sorted(options):
			if ship.check_move(Circle(Point(x + warehouse_class.width // 2, y + warehouse_class.height // 2), BUILDINGS.BUILD.MAX_BUILDING_SHIP_DISTANCE)):
				return (x, y)
		return None

	@classmethod
	def create(cls, ship, land_manager, success_callback, failure_callback):
		coords = cls.find_warehouse_location(ship, land_manager)
		return FoundSettlement(success_callback, failure_callback, land_manager, ship, coords)
Пример #16
0
class Trader(AIPlayer):
	"""A trader represents the free trader that travels around the map with his trading ship(s) and
	sells resources to players and buys resources from them. This is a very simple form of AI, as it
	doesn't do any more then drive to a place on water or a branchoffice randomly and then buys and
	sells resources. A game should not have more then one free trader (it could though)
	@param id: int - player id, every Player needs a unique id, as the freetrader is a Player instance, he also does.
	@param name: Traders name, also needed for the Player class.
	@param color: util.Color instance with the traders banner color, also needed for the Player class"""

	shipStates = Enum.get_extended(AIPlayer.shipStates, 'moving_to_branch', 'reached_branch')

	log = logging.getLogger("ai.trader")

	def __init__(self, session, id, name, color, **kwargs):
		super(Trader, self).__init__(session, id, name, color, **kwargs)
		self.__init()
		self.create_ship()

	def create_ship(self):
		"""Create a ship and place it randomly"""
		self.log.debug("Trader %s: creating new ship", self.worldid)
		point = self.session.world.get_random_possible_ship_position()
		ship = CreateUnit(self.worldid, UNITS.TRADER_SHIP_CLASS, point.x, point.y)(issuer=self)
		self.ships[ship] = self.shipStates.reached_branch
		Scheduler().add_new_object(Callback(self.ship_idle, ship), self, run_in=0)

	def __init(self):
		self.office = {} # { ship.worldid : branch }. stores the branch the ship is currently heading to
		self.allured_by_signal_fire = {} # bool, used to get away from a signal fire (and not be allured again immediately)

	def save(self, db):
		super(Trader, self).save(db)

		# mark self as a trader
		db("UPDATE player SET is_trader = 1 WHERE rowid = ?", self.worldid)

		for ship in self.ships:
			# prepare values
			ship_state = self.ships[ship]

			remaining_ticks = None
			# get current callback in scheduler, according to ship state, to retrieve
			# the number of ticks, when the call will actually be done
			current_callback = None
			if ship_state == self.shipStates.reached_branch:
				current_callback = Callback(self.ship_idle, ship)
			if current_callback is not None:
				# current state has a callback
				calls = Scheduler().get_classinst_calls(self, current_callback)
				assert len(calls) == 1, "got %s calls for saving %s: %s" %(len(calls), current_callback, calls)
				remaining_ticks = max(calls.values()[0], 1)

			targeted_branch = None if ship.worldid not in self.office else self.office[ship.worldid].worldid

			# put them in the database
			db("INSERT INTO trader_ships(rowid, state, remaining_ticks, targeted_branch) \
			   VALUES(?, ?, ?, ?)", ship.worldid, ship_state.index, remaining_ticks, targeted_branch)

	def _load(self, db, worldid):
		super(Trader, self)._load(db, worldid)
		self.__init()

	def load_ship_states(self, db):
		# load ships one by one from db (ship instances themselves are loaded already, but
		# we have to use them here)
		for ship_id, state_id, remaining_ticks, targeted_branch in \
				db("SELECT rowid, state, remaining_ticks, targeted_branch FROM trader_ships"):
			state = self.shipStates[state_id]
			ship = WorldObject.get_object_by_id(ship_id)

			self.ships[ship] = state

			if state == self.shipStates.moving_random:
				ship.add_move_callback(Callback(self.ship_idle, ship))
			elif state == self.shipStates.moving_to_branch:
				ship.add_move_callback(Callback(self.reached_branch, ship))
				assert targeted_branch is not None
				self.office[ship.worldid] = WorldObject.get_object_by_id(targeted_branch)
			elif state == self.shipStates.reached_branch:
				assert remaining_ticks is not None
				Scheduler().add_new_object( \
					Callback(self.ship_idle, ship), self, remaining_ticks)

	def get_ship_count(self):
		"""Returns number of ships"""
		return len(self.ships)

	def send_ship_random(self, ship):
		"""Sends a ship to a random position on the map.
		@param ship: Ship instance that is to be used"""
		super(Trader, self).send_ship_random(ship)
		ship.add_conditional_callback(Callback(self._check_for_signal_fire_in_ship_range, ship), \
		                              callback=Callback(self._ship_found_signal_fire, ship))

	def _check_for_signal_fire_in_ship_range(self, ship):
		"""Returns the signal fire instance, if there is one in the ships range, else False"""
		if ship in self.allured_by_signal_fire and self.allured_by_signal_fire[ship]:
			return False # don't visit signal fire again
		for tile in self.session.world.get_tiles_in_radius(ship.position, ship.radius):
			try:
				if tile.object.id == BUILDINGS.SIGNAL_FIRE_CLASS:
					return tile.object
			except AttributeError:
				pass # tile has no object or object has no id
		return False

	def _ship_found_signal_fire(self, ship):
		signal_fire = self._check_for_signal_fire_in_ship_range(ship)
		self.log.debug("Trader %s ship %s found signal fire %s", self.worldid, ship.worldid, signal_fire)
		# search a branch office in the range of the signal fire and move to it
		branch_offices = self.session.world.get_branch_offices()
		for bo in branch_offices:
			if bo.position.distance(signal_fire.position) <= signal_fire.radius and \
			   bo.owner == signal_fire.owner:
				self.log.debug("Trader %s moving to bo %s", self.worldid, bo)
				self.allured_by_signal_fire[ship] = True
				# HACK: remove allured flag in a few ticks
				def rem_allured(self, ship): self.allured_by_signal_fire[ship] = False
				Scheduler().add_new_object(Callback(rem_allured, self, ship), self, Scheduler().get_ticks(60))
				self.send_ship_random_branch(ship, bo)
				return
		self.log.debug("Trader can't find bo in range of the signal fire")

	def send_ship_random_branch(self, ship, branch_office=None):
		"""Sends a ship to a random branch office on the map
		@param ship: Ship instance that is to be used
		@param branch_office: Branch Office instance to move to. Random one is selected on None."""
		self.log.debug("Trader %s ship %s moving to bo (random=%s)", self.worldid, ship.worldid, \
		               (branch_office is None))
		# maybe this kind of list should be saved somewhere, as this is pretty performance intense
		branchoffices = self.session.world.get_branch_offices()
		if len(branchoffices) == 0:
			# there aren't any branch offices, so move randomly
			self.send_ship_random(ship)
		else:
			# select a branch office
			if branch_office is None:
				rand = self.session.random.randint(0, len(branchoffices)-1)
				self.office[ship.worldid] = branchoffices[rand]
			else:
				self.office[ship.worldid] = branch_office
			# try to find a possible position near the bo
			try:
				ship.move(Circle(self.office[ship.worldid].position.center(), ship.radius), Callback(self.reached_branch, ship))
				self.ships[ship] = self.shipStates.moving_to_branch
			except MoveNotPossible:
				self.send_ship_random(ship)

	def reached_branch(self, ship):
		"""Actions that need to be taken when reaching a branch office
		@param ship: ship instance"""
		self.log.debug("Trader %s ship %s: reached bo", self.worldid, ship.worldid)
		settlement = self.office[ship.worldid].settlement
		# NOTE: must be sorted for mp games (same order everywhere)
		for res in sorted(settlement.buy_list.iterkeys()): # check for resources that the settlement wants to buy
			amount = self.session.random.randint(*TRADER.SELL_AMOUNT) # select a random amount to sell
			if amount == 0:
				continue
			price = int(self.session.db.get_res_value(res) * TRADER.PRICE_MODIFIER_SELL * amount)
			settlement.buy(res, amount, price)
			# don't care if he bought it. the trader just offers.
			self.log.debug("Trader %s: offered sell %s tons of res %s", self.worldid, amount, res)

		# NOTE: must be sorted for mp games (same order everywhere)
		for res in sorted(settlement.sell_list.iterkeys()):
			# select a random amount to buy from the settlement
			amount = self.session.random.randint(*TRADER.BUY_AMOUNT)
			if amount == 0:
				continue
			price = int(self.session.db.get_res_value(res) * TRADER.PRICE_MODIFIER_BUY * amount)
			settlement.sell(res, amount, price)
			self.log.debug("Trader %s: offered buy %s tons of res %s", self.worldid, amount, res)

		del self.office[ship.worldid]
		# wait 2 seconds before going on to the next island
		Scheduler().add_new_object(Callback(self.ship_idle, ship), self, \
		                           Scheduler().get_ticks(TRADER.TRADING_DURATION))
		self.ships[ship] = self.shipStates.reached_branch

	def ship_idle(self, ship):
		"""Called if a ship is idle. Sends ship to a random place or  branch office (which target
		to use is decided by chance, probability for branch office (BUSINESS_S.) is 2/3 by default)
		@param ship: ship instance"""
		if self.session.random.randint(0, 100) < TRADER.BUSINESS_SENSE:
			# delay one tick, to allow old movement calls to completely finish
			self.log.debug("Trader %s ship %s: idle, moving to random bo", self.worldid, ship.worldid)
			Scheduler().add_new_object(Callback(self.send_ship_random_branch, ship), self, run_in=0)
		else:
			self.log.debug("Trader %s ship %s: idle, moving to random location", self.worldid, ship.worldid)
			Scheduler().add_new_object(Callback(self.send_ship_random, ship), self, run_in=0)
class PrepareFoundationShip(ShipMission):
	"""
	Given a ship and a settlement manager it moves the ship to the warehouse and loads
	it with the resources required to start another settlement.
	"""

	missionStates = Enum('created', 'moving')

	def __init__(self, settlement_manager, ship, feeder_island, success_callback, failure_callback):
		super(PrepareFoundationShip, self).__init__(success_callback, failure_callback, ship)
		self.settlement_manager = settlement_manager
		self.feeder_island = feeder_island
		self.warehouse = self.settlement_manager.settlement.warehouse
		self.state = self.missionStates.created

	def save(self, db):
		super(PrepareFoundationShip, self).save(db)
		db("INSERT INTO ai_mission_prepare_foundation_ship(rowid, settlement_manager, ship, feeder_island, state) VALUES(?, ?, ?, ?, ?)",
			self.worldid, self.settlement_manager.worldid, self.ship.worldid, self.feeder_island, self.state.index)

	@classmethod
	def load(cls, db, worldid, success_callback, failure_callback):
		self = cls.__new__(cls)
		self._load(db, worldid, success_callback, failure_callback)
		return self

	def _load(self, db, worldid, success_callback, failure_callback):
		db_result = db("SELECT settlement_manager, ship, feeder_island, state FROM ai_mission_prepare_foundation_ship WHERE rowid = ?", worldid)[0]
		self.settlement_manager = WorldObject.get_object_by_id(db_result[0])
		self.warehouse = self.settlement_manager.settlement.warehouse
		self.feeder_island = db_result[2]
		self.state = self.missionStates[db_result[3]]
		super(PrepareFoundationShip, self).load(db, worldid, success_callback, failure_callback,
			WorldObject.get_object_by_id(db_result[1]))

		if self.state == self.missionStates.moving:
			self.ship.add_move_callback(Callback(self._reached_destination_area))
			self.ship.add_blocked_callback(Callback(self._move_to_destination_area))
		else:
			assert False, 'invalid state'

	def start(self):
		self.state = self.missionStates.moving
		self._move_to_destination_area()

	def _move_to_destination_area(self):
		self._move_to_warehouse_area(self.warehouse.position, Callback(self._reached_destination_area),
			Callback(self._move_to_destination_area), 'Move not possible')

	def _load_foundation_resources(self):
		personality = self.owner.personality_manager.get('SettlementFounder')
		if self.feeder_island:
			max_amounts = {RES.BOARDS: personality.max_new_feeder_island_boards, RES.TOOLS: personality.max_new_feeder_island_tools}
		else:
			max_amounts = {RES.BOARDS: personality.max_new_island_boards, RES.FOOD: personality.max_new_island_food, RES.TOOLS: personality.max_new_island_tools}

		for resource_id, max_amount in max_amounts.iteritems():
			self.move_resource(self.ship, self.settlement_manager.settlement, resource_id, self.ship.get_component(StorageComponent).inventory[resource_id] - max_amount)

	def _reached_destination_area(self):
		self.log.info('%s reached BO area', self)
		self._load_foundation_resources()

		success = False
		if self.feeder_island:
			success = self.settlement_manager.owner.settlement_founder.have_feeder_island_starting_resources(self.ship, None)
			if success:
				self.report_success('Transferred enough feeder island foundation resources to the ship')
		else:
			success = self.settlement_manager.owner.settlement_founder.have_starting_resources(self.ship, None)
			if success:
				self.report_success('Transferred enough foundation resources to the ship')
		if not success:
			self.report_failure('Not enough foundation resources available')

	def cancel(self):
		self.ship.stop()
		super(PrepareFoundationShip, self).cancel()
Пример #18
0
class BehaviorManager(object):
    """
	BehaviorManager holds BehaviorComponents.
	Entities such as CombatManager or StrategyManager ask BehaviorManager to perform
	and action, or create a mission object. BehaviorManager does these based on
	behavior probability and likelihood of success.
	"""
    action_types = Enum('offensive', 'defensive', 'idle')
    strategy_types = Enum('offensive', 'idle', 'diplomatic')

    log = logging.getLogger("ai.aiplayer.behavior.behaviormanager")

    def __init__(self, owner):
        super(BehaviorManager, self).__init__()
        self.__init(owner)

        self.profile_token = self.get_profile_token()
        self.profile = owner.get_random_profile(self.profile_token)

    def __init(self, owner):
        self.owner = owner
        self.world = owner.world
        self.session = owner.session

    def save(self, db):
        db(
            "INSERT INTO ai_behavior_manager (owner_id, profile_token) VALUES(?, ?)",
            self.owner.worldid, self.profile_token)

    @classmethod
    def load(cls, db, owner):
        self = cls.__new__(cls)
        super(BehaviorManager, self).__init__()
        self.__init(owner)
        self._load(db, owner)
        return self

    def _load(self, db, owner):
        (profile_token, ) = db(
            "SELECT profile_token FROM ai_behavior_manager WHERE owner_id = ?",
            self.owner.worldid)[0]
        self.profile_token = profile_token

        # this time they will be loaded with a correct token
        self.profile = owner.get_random_profile(self.profile_token)

    def request_behavior(self, type, action_name, behavior_list,
                         **environment):
        possible_behaviors = []
        for behavior, probability in behavior_list[type].iteritems():
            if hasattr(behavior, action_name):
                certainty = behavior.certainty(action_name, **environment)
                # final probability is the one defined in profile multiplied by it's certainty
                self.log.info(
                    "Player:%s Behavior:%s Function:%s (p: %s ,c: %s ,f: %s)",
                    self.owner.name, behavior.__class__.__name__, action_name,
                    probability, certainty, probability * certainty)
                possible_behaviors.append((behavior, probability * certainty))

        # get the best action possible if any is available
        final_action = self.get_best_behavior(possible_behaviors)
        if final_action:
            return getattr(final_action, action_name)(**environment)

    def request_action(self, type, action_name, **environment):
        return self.request_behavior(type, action_name, self.profile.actions,
                                     **environment)

    def request_strategy(self, type, strategy_name, **environment):
        return self.request_behavior(type, strategy_name,
                                     self.profile.strategies, **environment)

    def get_conditions(self):
        return self.profile.conditions

    def get_best_behavior(self, behavior_iterable):
        """
		Get best behavior from behavior_iterable (linear time).
		"""
        total, random_value = 0.0, self.session.random.random()

        # instead of scaling every value to make 1.0, we scale random_value to sum of probabilities
        sum_probs = sum([item[1] for item in behavior_iterable])

        if abs(sum_probs) < 1e-7:
            return None

        random_value *= sum_probs

        for behavior, probability in behavior_iterable:
            if (total + probability) > random_value:
                return behavior
            total += probability

    def get_profile_token(self):
        """
		Returns a random token for player profile. Token is used when requesting for a random behavior profile.
		Because it is guaranteed to get exactly the same player profile for given token, instead of storing
		whole Profile in database, we store a single number (token) which on load() generates same set of actions.
		"""
        return self.session.random.randint(0, 10000)
Пример #19
0
class ScoutingMission(FleetMission):
	"""
	This is an example of a scouting mission.
	Send ship from point A to point B, and then to point A again.
	1. Send fleet to a point on the map
	2. Fleet returns to starting position of ships[0] (first ship)
	"""
	missionStates = Enum.get_extended(FleetMission.missionStates, 'sailing_to_target', 'going_back')

	def __init__(self, success_callback, failure_callback, ships, target_point):
		super(ScoutingMission, self).__init__(success_callback, failure_callback, ships)
		self.__init(target_point, ships[0].position.copy())

	def __init(self, target_point, starting_point):
		self.target_point = target_point
		self.starting_point = starting_point

		self.combatIntermissions = {
			self.missionStates.sailing_to_target: (self.sail_to_target, self.flee_home),
			self.missionStates.going_back: (self.go_back, self.flee_home),
			self.missionStates.fleeing_home: (self.flee_home, self.flee_home),
		}

		self._state_fleet_callbacks = {
			self.missionStates.sailing_to_target: Callback(self.go_back),
			self.missionStates.going_back: Callback(self.report_success, "Ships arrived at the target"),
			self.missionStates.fleeing_home: Callback(self.report_failure, "Combat was lost, ships fled home successfully"),
		}

	def start(self):
		self.sail_to_target()

	def save(self, db):
		super(ScoutingMission, self).save(db)
		db("INSERT INTO ai_scouting_mission (rowid, starting_point_x, starting_point_y, target_point_x, target_point_y) VALUES(?, ?, ?, ?, ?)",
			self.worldid, self.starting_point.x, self.starting_point.y, self.target_point.x, self.target_point.y)

	def _load(self, worldid, owner, db, success_callback, failure_callback):
		super(ScoutingMission, self)._load(db, worldid, success_callback, failure_callback, owner)
		db_result = db("SELECT target_point_x, target_point_y, starting_point_x, starting_point_y FROM ai_scouting_mission WHERE rowid = ?", worldid)[0]
		self.__init(Point(*db_result[:2]), Point(*db_result[2:]))

	def go_back(self):
		"""
		Going back home after successfully reaching the target point.
		"""
		try:
			self.fleet.move(self.starting_point, self._state_fleet_callbacks[self.missionStates.going_back])
			self.state = self.missionStates.going_back
		except MoveNotPossible:
			self.report_failure("Move was not possible when going back")

	def flee_home(self):
		"""
		Fleeing home after severe casualties.
		"""
		if self.fleet.size() > 0:
			try:
				self.fleet.move(self.starting_point, self._state_fleet_callbacks[self.missionStates.fleeing_home])
				self.state = self.missionStates.fleeing_home
			except MoveNotPossible:
				self.report_failure("Combat was lost, ships couldn't flee home")
		else:
			self.report_failure("Combat was lost, all ships were wiped out")

	def sail_to_target(self):
		if not self.target_point:
			self.target_point = self.owner.session.world.get_random_possible_ship_position()
		try:
			self.fleet.move(self.target_point, self._state_fleet_callbacks[self.missionStates.sailing_to_target])
			self.state = self.missionStates.sailing_to_target
		except MoveNotPossible:
			self.report_failure("Move was not possible when moving to target")

	@classmethod
	def create(cls, success_callback, failure_callback, ships, target_point=None):
		return ScoutingMission(success_callback, failure_callback, ships, target_point)
Пример #20
0
class BuildTab(TabInterface):
    """
	Layout data is defined in image_data and text_data.
	Columns in the tabs are enumerated as follows:
	  01  11  21  31
	  02  12  22  32
	  03  13  23  33
	  04  14  24  34
	Boxes and Labels have the same number as their left upper icon.
	Check buildtab.xml for details. Icons without image are transparent.
	"""
    lazy_loading = True
    widget = 'buildtab.xml'

    MAX_ROWS = 4
    MAX_COLS = 4

    build_menus = [
        "content/objects/gui_buildmenu/build_menu_per_tier.yaml",
        "content/objects/gui_buildmenu/build_menu_per_type.yaml"
    ]

    build_menu_config_per_tier = build_menus[0]
    build_menu_config_per_type = build_menus[1]

    default_build_menu_config = build_menu_config_per_tier

    cur_build_menu_config = default_build_menu_config

    # NOTE: check for occurrences of this when adding one, you might want to
    #       add respective code there as well
    unlocking_strategies = Enum(
        "tab_per_tier",  # 1 tab per tier
        "single_per_tier"  # each single building unlocked if tier is unlocked
    )

    last_active_build_tab = None  # type: int

    def __init__(self, session, tabindex, data, build_callback,
                 unlocking_strategy, build_menu_config):
        """
		@param tabindex: position of tab
		@param data: data directly from yaml specifying the contents of this tab
		@param build_callback: called on clicks
		@param unlocking_strategy: element of unlocking_strategies
		@param build_menu_config: entry of build_menus where this definition originates from
		"""
        icon_path = None
        helptext = None
        headline = None
        rows = []
        for entry in data:
            if isinstance(entry, dict):
                # this is one key-value pair, e.g. "- icon: img/foo.png"
                if len(entry) != 1:
                    raise InvalidBuildMenuFileFormat(
                        "Invalid entry in buildmenuconfig: {}".format(entry))
                key, value = list(entry.items())[0]
                if key == "icon":
                    icon_path = value
                elif key == "helptext":
                    helptext = value[2:] if value.startswith('_ ') else value
                elif key == "headline":
                    headline = value[2:] if value.startswith('_ ') else value
                else:
                    raise InvalidBuildMenuFileFormat(
                        "Invalid key: {}\nMust be either icon, helptext or headline."
                        .format(key))
            elif isinstance(entry, list):
                # this is a line of data
                rows.append(entry)  # parse later on demand
            else:
                raise InvalidBuildMenuFileFormat(
                    "Invalid entry: {}".format(entry))

        if not icon_path:
            raise InvalidBuildMenuFileFormat(
                "icon_path definition is missing.")

        self.session = session
        self.tabindex = tabindex
        self.build_callback = build_callback
        self.unlocking_strategy = unlocking_strategy
        if self.unlocking_strategy != self.__class__.unlocking_strategies.tab_per_tier:
            if not helptext and not headline:
                raise InvalidBuildMenuFileFormat(
                    "helptext definition is missing.")
        self.row_definitions = rows
        self.headline = T(
            headline) if headline else headline  # don't translate None
        self.helptext = T(helptext) if helptext else self.headline

        #get build style
        saved_build_style = horizons.globals.fife.get_uh_setting("Buildstyle")
        self.cur_build_menu_config = self.__class__.build_menus[
            saved_build_style]

        super(BuildTab, self).__init__(icon_path=icon_path)

    @classmethod
    def get_saved_buildstyle(cls):
        saved_build_style = horizons.globals.fife.get_uh_setting("Buildstyle")
        return cls.build_menus[saved_build_style]

    def init_widget(self):
        self.__current_settlement = None
        headline_lbl = self.widget.child_finder('headline')
        if self.headline:  # prefer specific headline
            headline_lbl.text = self.headline
        elif self.unlocking_strategy == self.__class__.unlocking_strategies.tab_per_tier:
            headline_lbl.text = T(
                self.session.db.get_settler_name(self.tabindex))

    def set_content(self):
        """Parses self.row_definitions and sets the content accordingly"""
        settlement = LastActivePlayerSettlementManager().get()

        def _set_entry(button, icon, building_id):
            """Configure a single build menu button"""
            if self.unlocking_strategy == self.__class__.unlocking_strategies.single_per_tier and \
               self.get_building_tiers()[building_id] > self.session.world.player.settler_level:
                return

            building = Entities.buildings[building_id]
            button.helptext = building.get_tooltip()

            # Add necessary resources to tooltip text.
            # tooltip.py will then place icons from this information.
            required_resources = ''
            for resource_id, amount_needed in sorted(building.costs.items()):
                required_resources += ' {}:{}'.format(resource_id,
                                                      amount_needed)
            required_text = '[[Buildmenu{}]]'.format(required_resources)
            button.helptext = required_text + button.helptext

            enough_res = False  # don't show building by default
            if settlement is not None:  # settlement is None when the mouse has left the settlement
                res_overview = self.session.ingame_gui.resource_overview
                show_costs = Callback(res_overview.set_construction_mode,
                                      settlement, building.costs)
                button.mapEvents({
                    button.name + "/mouseEntered/buildtab":
                    show_costs,
                    button.name + "/mouseExited/buildtab":
                    res_overview.close_construction_mode
                })

                (enough_res,
                 missing_res) = Build.check_resources({}, building.costs,
                                                      settlement.owner,
                                                      [settlement])
            # Check whether to disable build menu icon (not enough res available).
            if enough_res:
                icon.image = "content/gui/images/buttons/buildmenu_button_bg.png"
                button.path = "icons/buildmenu/{id:03d}".format(id=building_id)
            else:
                icon.image = "content/gui/images/buttons/buildmenu_button_bg_bw.png"
                button.path = "icons/buildmenu/greyscale/{id:03d}".format(
                    id=building_id)

            button.capture(Callback(self.build_callback, building_id))

        for row_num, row in enumerate(self.row_definitions):
            # we have integers for building types, strings for headlines above slots and None as empty slots
            column = -1  # can't use enumerate, not always incremented
            for entry in row:
                column += 1
                position = (10 * column) + (
                    row_num + 1)  # legacy code, first row is 1, 11, 21
                if entry is None:
                    continue
                elif (column + 1) > self.MAX_COLS:
                    # out of 4x4 bounds
                    err = "Invalid entry '{}': column {} does not exist.".format(
                        entry, column + 1)
                    err += " Max. column amount in current layout is {}.".format(
                        self.MAX_COLS)
                    raise InvalidBuildMenuFileFormat(err)
                elif row_num > self.MAX_ROWS:
                    # out of 4x4 bounds
                    err = "Invalid entry '{}': row {} does not exist.".format(
                        entry, row_num)
                    err += " Max. row amount in current layout is {}.".format(
                        self.MAX_ROWS)
                    raise InvalidBuildMenuFileFormat(err)
                elif isinstance(entry, str):
                    column -= 1  # a headline does not take away a slot
                    lbl = self.widget.child_finder(
                        'label_{position:02d}'.format(position=position))
                    lbl.text = T(
                        entry[2:]) if entry.startswith('_ ') else entry
                elif isinstance(entry, int):
                    button = self.widget.child_finder(
                        'button_{position:02d}'.format(position=position))
                    icon = self.widget.child_finder(
                        'icon_{position:02d}'.format(position=position))
                    _set_entry(button, icon, entry)
                else:
                    raise InvalidBuildMenuFileFormat(
                        "Invalid entry: {}".format(entry))

    def refresh(self):
        self.set_content()

    def on_settlement_change(self, message):
        if message.settlement is not None:
            # only react to new actual settlements, else we have no res source
            self.refresh()

    def __remove_changelisteners(self):
        NewPlayerSettlementHovered.discard(self.on_settlement_change)
        if self.__current_settlement is not None:
            inventory = self.__current_settlement.get_component(
                StorageComponent).inventory
            inventory.discard_change_listener(self.refresh)

    def __add_changelisteners(self):
        NewPlayerSettlementHovered.subscribe(self.on_settlement_change)
        if self.__current_settlement is not None:
            inventory = self.__current_settlement.get_component(
                StorageComponent).inventory
            if not inventory.has_change_listener(self.refresh):
                inventory.add_change_listener(self.refresh)

    def show(self):
        self.__remove_changelisteners()
        self.__current_settlement = LastActivePlayerSettlementManager().get()
        self.__add_changelisteners()
        self.__class__.last_active_build_tab = self.tabindex
        super(BuildTab, self).show()

        button = self.widget.child_finder("switch_build_menu_config_button")
        self._set_switch_layout_button_image(button)
        button.capture(self._switch_build_menu_config)

    def hide(self):
        self.__remove_changelisteners()
        super(BuildTab, self).hide()

    def _set_switch_layout_button_image(self, button):
        image_path = "content/gui/icons/tabwidget/buildmenu/"
        if self.__class__.cur_build_menu_config is self.build_menu_config_per_type:
            button.up_image = image_path + "tier.png"
        else:
            button.up_image = image_path + "class.png"
        self.switch_layout_button_needs_update = False

    def _switch_build_menu_config(self):
        """Sets next build menu config and recreates the gui"""
        cur_index = self.__class__.build_menus.index(
            self.cur_build_menu_config)
        new_index = (cur_index + 1) % len(self.__class__.build_menus)
        self.__class__.cur_build_menu_config = self.__class__.build_menus[
            new_index]

        # after switch set active tab to first
        self.__class__.last_active_build_tab = 0
        self.session.ingame_gui.show_build_menu(update=True)

        #save build style
        horizons.globals.fife.set_uh_setting("Buildstyle", new_index)
        horizons.globals.fife.save_settings()

    @classmethod
    def create_tabs(cls, session, build_callback):
        """Create according to current build menu config
		@param build_callback: function to call to enable build mode, has to take building type parameter
		"""
        source = cls.get_saved_buildstyle()
        # parse
        data = YamlCache.get_file(source, game_data=True)
        if 'meta' not in data:
            raise InvalidBuildMenuFileFormat(
                'File does not contain "meta" section')
        metadata = data['meta']
        if 'unlocking_strategy' not in metadata:
            raise InvalidBuildMenuFileFormat(
                '"meta" section does not contain "unlocking_strategy"')
        try:
            unlocking_strategy = cls.unlocking_strategies.get_item_for_string(
                metadata['unlocking_strategy'])
        except KeyError:
            raise InvalidBuildMenuFileFormat(
                'Invalid entry for "unlocking_strategy"')

        # create tab instances
        tabs = []
        for tab, tabdata in sorted(data.items()):
            if tab == "meta":
                continue  # not a tab

            if unlocking_strategy == cls.unlocking_strategies.tab_per_tier and len(
                    tabs) > session.world.player.settler_level:
                break

            try:
                tab = BuildTab(session, len(tabs), tabdata, build_callback,
                               unlocking_strategy, source)
                tabs.append(tab)
            except Exception as e:
                to_add = "\nThis error happened in {} of {} .".format(
                    tab, source)
                e.args = (e.args[0] + to_add, ) + e.args[1:]
                e.message = (e.message + to_add)
                raise

        return tabs

    @classmethod
    @decorators.cachedfunction
    def get_building_tiers(cls):
        """Returns a dictionary mapping building type ids to their tiers
		@return cached dictionary (don't modify)"""
        building_tiers = {}
        data = YamlCache.get_file(cls.build_menu_config_per_tier,
                                  game_data=True)
        tier = -1
        for tab, tabdata in sorted(data.items()):
            if tab == "meta":
                continue  # not a tab

            tier += 1

            for row in tabdata:
                if isinstance(row, list):  # actual content
                    for entry in row:
                        if isinstance(entry, int):  # actual building button
                            building_tiers[entry] = tier
        return building_tiers
Пример #21
0
class SurpriseAttack(FleetMission):
    """
	This is a basic attack mission.
	1. Send fleet to a Point (or Circle) A
	2. Break diplomacy with enemy player P if he is not hostile,
	3. Begin combat phase
	4. Return home (point B).
	"""

    missionStates = Enum.get_extended(FleetMission.missionStates,
                                      'sailing_to_target', 'in_combat',
                                      'breaking_diplomacy', 'going_back')

    def __init__(self, success_callback, failure_callback, ships, target_point,
                 return_point, enemy_player):
        super(SurpriseAttack, self).__init__(success_callback,
                                             failure_callback, ships)
        self.__init(target_point, return_point, enemy_player)

    def __init(self, target_point, return_point, enemy_player):
        self.target_point = target_point
        self.return_point = return_point
        self.enemy_player = enemy_player

        self.combatIntermissions = {
            self.missionStates.sailing_to_target:
            (self.sail_to_target, self.flee_home),
            self.missionStates.in_combat: (self.go_back, self.flee_home),
            self.missionStates.going_back: (self.go_back, self.flee_home),
            self.missionStates.breaking_diplomacy:
            (self.break_diplomacy, self.flee_home),
            self.missionStates.fleeing_home: (self.flee_home, self.flee_home),
        }

        # Fleet callbacks corresponding to given state
        self._state_fleet_callbacks = {
            self.missionStates.sailing_to_target:
            Callback(self.break_diplomacy),
            self.missionStates.going_back:
            Callback(self.report_success, "Ships arrived at return point"),
            self.missionStates.fleeing_home:
            Callback(self.report_failure,
                     "Combat was lost, ships fled home successfully"),
        }

    def save(self, db):
        super(SurpriseAttack, self).save(db)
        db(
            "INSERT INTO ai_mission_surprise_attack (rowid, enemy_player_id, target_point_x, target_point_y, target_point_radius, "
            "return_point_x, return_point_y) VALUES(?, ?, ?, ?, ?, ?, ?)",
            self.worldid, self.enemy_player.worldid,
            self.target_point.center.x, self.target_point.center.y,
            self.target_point.radius, self.return_point.x, self.return_point.y)

    def _load(self, worldid, owner, db, success_callback, failure_callback):
        super(SurpriseAttack, self)._load(db, worldid, success_callback,
                                          failure_callback, owner)
        db_result = db(
            "SELECT enemy_player_id, target_point_x, target_point_y, target_point_radius, return_point_x, return_point_y "
            "FROM ai_mission_surprise_attack WHERE rowid = ?", worldid)[0]
        enemy_player_id, target_point_x, target_point_y, target_point_radius, return_point_x, return_point_y = db_result

        target_point = Circle(Point(target_point_x, target_point_y),
                              target_point_radius)
        return_point = Point(return_point_x, return_point_y)
        enemy_player = WorldObject.get_object_by_id(enemy_player_id)
        self.__init(target_point, return_point, enemy_player)

    def start(self):
        self.sail_to_target()

    def sail_to_target(self):
        self.log.debug(
            "Player %s, Mission %s, 1/4 set off from point %s to point %s",
            self.owner.name, self.__class__.__name__, self.return_point,
            self.target_point)
        try:
            self.fleet.move(
                self.target_point, self._state_fleet_callbacks[
                    self.missionStates.sailing_to_target])
            self.state = self.missionStates.sailing_to_target
        except MoveNotPossible:
            self.report_failure("Move was not possible when moving to target")

    def break_diplomacy(self):
        self.state = self.missionStates.breaking_diplomacy
        self.log.debug(
            "Player %s, Mission %s, 2/4 breaking diplomacy with Player %s",
            self.owner.name, self.__class__.__name__, self.enemy_player.name)
        if not self.session.world.diplomacy.are_enemies(
                self.owner, self.enemy_player):
            AddEnemyPair(self.owner, self.enemy_player).execute(self.session)
        self.in_combat()

    def in_combat(self):
        self.combat_phase = True
        self.log.debug("Player %s, Mission %s, 3/4 in combat", self.owner.name,
                       self.__class__.__name__)
        self.state = self.missionStates.in_combat

    def go_back(self):
        self.log.debug(
            "Player %s, Mission %s, 4/4 going back after combat to point %s",
            self.owner.name, self.__class__.__name__, self.return_point)
        try:
            self.fleet.move(
                self.return_point,
                self._state_fleet_callbacks[self.missionStates.going_back])
            self.state = self.missionStates.going_back
        except MoveNotPossible:
            self.report_failure("Move was not possible when going back")

    def flee_home(self):
        if self.fleet.size() > 0:
            try:
                self.fleet.move(
                    self.return_point, self._state_fleet_callbacks[
                        self.missionStates.fleeing_home])
                self.state = self.missionStates.fleeing_home
            except MoveNotPossible:
                self.report_failure(
                    "Combat was lost, ships couldn't flee home")
        else:
            self.report_failure("Combat was lost, all ships were wiped out")

    @classmethod
    def create(cls, success_callback, failure_callback, fleet, target_point,
               return_point, enemy_player):
        return SurpriseAttack(success_callback, failure_callback, fleet,
                              target_point, return_point, enemy_player)
Пример #22
0
class DomesticTrade(ShipMission):
    """
	Given a ship and two settlement managers the ship is taken to the first one,
	loaded with resources, and then moved to the other one to unload the resources.
	"""

    missionStates = Enum('created', 'moving_to_source_warehouse',
                         'moving_to_destination_warehouse')

    def __init__(self, source_settlement_manager,
                 destination_settlement_manager, ship, success_callback,
                 failure_callback):
        super(DomesticTrade, self).__init__(success_callback, failure_callback,
                                            ship)
        self.source_settlement_manager = source_settlement_manager
        self.destination_settlement_manager = destination_settlement_manager
        self.state = self.missionStates.created

    def save(self, db):
        super(DomesticTrade, self).save(db)
        db(
            "INSERT INTO ai_mission_domestic_trade(rowid, source_settlement_manager, destination_settlement_manager, ship, state) VALUES(?, ?, ?, ?, ?)",
            self.worldid, self.source_settlement_manager.worldid,
            self.destination_settlement_manager.worldid, self.ship.worldid,
            self.state.index)

    @classmethod
    def load(cls, db, worldid, success_callback, failure_callback):
        self = cls.__new__(cls)
        self._load(db, worldid, success_callback, failure_callback)
        return self

    def _load(self, db, worldid, success_callback, failure_callback):
        db_result = db(
            "SELECT source_settlement_manager, destination_settlement_manager, ship, state FROM ai_mission_domestic_trade WHERE rowid = ?",
            worldid)[0]
        self.source_settlement_manager = WorldObject.get_object_by_id(
            db_result[0])
        self.destination_settlement_manager = WorldObject.get_object_by_id(
            db_result[1])
        self.state = self.missionStates[db_result[3]]
        super(DomesticTrade,
              self).load(db, worldid, success_callback, failure_callback,
                         WorldObject.get_object_by_id(db_result[2]))

        if self.state == self.missionStates.moving_to_source_warehouse:
            self.ship.add_move_callback(
                Callback(self._reached_source_warehouse_area))
            self.ship.add_blocked_callback(
                Callback(self._move_to_source_warehouse_area))
        elif self.state == self.missionStates.moving_to_destination_warehouse:
            self.ship.add_move_callback(
                Callback(self._reached_destination_warehouse_area))
            self.ship.add_blocked_callback(
                Callback(self._move_to_destination_warehouse_area))
        else:
            assert False, 'invalid state'

    def start(self):
        self.state = self.missionStates.moving_to_source_warehouse
        self._move_to_source_warehouse_area()

    def _move_to_source_warehouse_area(self):
        self._move_to_warehouse_area(
            self.source_settlement_manager.settlement.warehouse.position,
            Callback(self._reached_source_warehouse_area),
            Callback(self._move_to_source_warehouse_area),
            'First move not possible')

    def _reached_source_warehouse_area(self):
        self.log.info('%s reached the source warehouse area', self)
        if self.source_settlement_manager.trade_manager.load_resources(self):
            self.log.info('%s loaded resources', self)
            self.state = self.missionStates.moving_to_destination_warehouse
            self._move_to_destination_warehouse_area()
        else:
            self.report_failure('No need for the ship at the source warehouse')

    def _move_to_destination_warehouse_area(self):
        self._move_to_warehouse_area(
            self.destination_settlement_manager.settlement.warehouse.position,
            Callback(self._reached_destination_warehouse_area),
            Callback(self._move_to_destination_warehouse_area),
            'Second move not possible')

    def _reached_destination_warehouse_area(self):
        self.log.info('%s reached destination warehouse area', self)
        self._unload_all_resources(
            self.destination_settlement_manager.settlement)
        self.report_success('Unloaded resources')
Пример #23
0
class KeyConfig(object, metaclass=Singleton):
    """Class for storing key bindings.
	The central function is translate().
	"""

    _Actions = Enum('LEFT', 'RIGHT', 'UP', 'DOWN', 'ROTATE_LEFT',
                    'ROTATE_RIGHT', 'SPEED_UP', 'SPEED_DOWN', 'PAUSE',
                    'ZOOM_IN', 'ZOOM_OUT', 'BUILD_TOOL', 'DESTROY_TOOL',
                    'ROAD_TOOL', 'PIPETTE', 'PLAYERS_OVERVIEW',
                    'SETTLEMENTS_OVERVIEW', 'SHIPS_OVERVIEW', 'LOGBOOK',
                    'CHAT', 'QUICKSAVE', 'QUICKLOAD', 'ESCAPE', 'TRANSLUCENCY',
                    'TILE_OWNER_HIGHLIGHT', 'HEALTH_BAR', 'SHOW_SELECTED',
                    'REMOVE_SELECTED', 'HELP', 'SCREENSHOT', 'DEBUG',
                    'CONSOLE', 'GRID', 'COORD_TOOLTIP')

    def __init__(self):
        _Actions = self._Actions
        self.log = logging.getLogger("gui.keys")

        self.all_keys = self.get_keys_by_name()
        # map key ID (int) to action it triggers (int)
        self.keyval_action_mappings = {}
        self.loadKeyConfiguration()

        self.requires_shift = {_Actions.DEBUG}

    def loadKeyConfiguration(self):
        self.keyval_action_mappings = {}
        custom_key_actions = horizons.globals.fife.get_hotkey_settings()
        for action in custom_key_actions:
            action_id = getattr(self._Actions, action, None)
            if action_id is None:
                self.log.warning('Unknown hotkey in settings: %s', action)
                continue

            keys_for_action = horizons.globals.fife.get_keys_for_action(action)
            for key in keys_for_action:
                key_id = self.get_key_by_name(key.upper())
                self.keyval_action_mappings[key_id] = action_id

    def translate(self, evt):
        """
		@param evt: fife.Event
		@return pseudo-enum _Action
		"""
        keyval = evt.getKey().getValue()

        if keyval in self.keyval_action_mappings:
            action = self.keyval_action_mappings[keyval]
        else:
            return None

        if action in self.requires_shift and not evt.isShiftPressed():
            return None
        else:
            return action  # all checks passed

    def get_key_by_name(self, keyname):
        return self.all_keys.get(keyname)

    def get_keys_by_name(self):
        def is_available(key):
            special_keys = ('WORLD_', 'ENTER', 'ALT', 'COMPOSE', 'LEFT_',
                            'RIGHT_', 'POWER', 'INVALID_KEY')
            return (key.startswith(tuple(ascii_uppercase))
                    and not key.startswith(special_keys))

        return {k: v for k, v in fife.Key.__dict__.items() if is_available(k)}

    def get_keys_by_value(self):
        def is_available(key):
            special_keys = ('WORLD_', 'ENTER', 'ALT', 'COMPOSE', 'LEFT_',
                            'RIGHT_', 'POWER', 'INVALID_KEY')
            return (key.startswith(tuple(ascii_uppercase))
                    and not key.startswith(special_keys))

        return {v: k for k, v in fife.Key.__dict__.items() if is_available(k)}

    def get_keyval_to_actionid_map(self):
        return self.keyval_action_mappings

    def get_current_keys(self, action):
        return horizons.globals.fife.get_keys_for_action(action)

    def get_default_keys(self, action):
        return horizons.globals.fife.get_keys_for_action(action, default=True)

    def get_actions_by_name(self):
        """Returns a list of the names of all the actions"""
        return [str(x) for x in self._Actions]

    def get_bindable_actions_by_name(self):
        """Returns a list of the names of the actions which can be binded in the hotkeys interface"""
        actions = [str(x) for x in self._Actions]
        unbindable_actions = ['DEBUG', 'ESCAPE']
        for action in unbindable_actions:
            actions.remove(action)
        return actions
Пример #24
0
class ChaseShipsAndAttack(FleetMission):
	"""
	This is one of the basic attack missions.
	1. Sail to given ship on the map until the fleet is in close range
	2. Begin combat phase
	3. go to 1 if ship is still alive

	This mission may work the best for 2 ships fleet
	"""

	missionStates = Enum.get_extended(FleetMission.missionStates, 'sailing_to_target', 'in_combat')
	target_range = 5

	def __init__(self, success_callback, failure_callback, ships, target_ship):
		super(ChaseShipsAndAttack, self).__init__(success_callback, failure_callback, ships)
		self.__init(target_ship)

	def __init(self, target_ship):
		self.target_ship = target_ship

		self.combatIntermissions = {
			self.missionStates.sailing_to_target: (self.sail_to_target, self.flee_home),
			self.missionStates.in_combat: (self.check_ship_alive, self.flee_home),
			self.missionStates.fleeing_home: (self.flee_home, self.flee_home),
		}

		self._state_fleet_callbacks = {
			self.missionStates.sailing_to_target: Callback(self.was_reached),
			self.missionStates.fleeing_home: Callback(self.report_failure, "Combat was lost, ships fled home successfully"),
		}

		ShipDestroyed.subscribe(self._on_ship_destroyed)

	def save(self, db):
		super(ChaseShipsAndAttack, self).save(db)
		db("INSERT INTO ai_mission_chase_ships_and_attack (rowid, target_ship_id) VALUES(?, ?)", self.worldid, self.target_ship.worldid)

	def _load(self, worldid, owner, db, success_callback, failure_callback):
		super(ChaseShipsAndAttack, self)._load(db, worldid, success_callback, failure_callback, owner)
		(target_ship_id,) = db("SELECT target_ship_id FROM ai_mission_chase_ships_and_attack WHERE rowid = ?", worldid)[0]

		target_ship = WorldObject.get_object_by_id(target_ship_id)
		self.__init(target_ship)

	def start(self):
		self.sail_to_target()

	def sail_to_target(self):
		self.log.debug("Player %s, Mission %s, 1/2 set off to ship %s at %s", self.owner.name, self.__class__.__name__,
			self.target_ship.get_component(NamedComponent).name, self.target_ship.position)
		try:
			self.fleet.move(Circle(self.target_ship.position, self.target_range), self._state_fleet_callbacks[self.missionStates.sailing_to_target])
			self.state = self.missionStates.sailing_to_target
		except MoveNotPossible:
			self.report_failure("Move was not possible when moving to target")

	def was_reached(self):
		if self.target_ship.in_ship_map:
			if any((ship.position.distance(self.target_ship.position) <= self.target_range + 1 for ship in self.fleet.get_ships())):
				# target ship reached: execute combat
				self.state = self.missionStates.in_combat
				self.in_combat()
			else:
				# target ship was not reached: sail again
				self.state = self.missionStates.sailing_to_target
				self.sail_to_target()
		else:
			# ship was destroyed
			self.report_success("Ship was destroyed")

	def check_ship_alive(self):
		if self.target_ship.in_ship_map:
			self.was_reached()
		else:
			self.report_success("Target ship was eliminated")

	def in_combat(self):
		if not self.session.world.diplomacy.are_enemies(self.owner, self.target_ship.owner):
			self.report_failure("Target ship was not hostile. Aborting mission.")
			return
		self.combat_phase = True
		self.log.debug("Player %s, Mission %s, 2/2 in combat", self.owner.name, self.__class__.__name__)
		self.state = self.missionStates.in_combat

	def flee_home(self):
		# check if fleet still exists
		if self.fleet.size() > 0:
			try:
				home_settlement = self.owner.settlements[0]
				return_point = self.unit_manager.get_warehouse_area(home_settlement, 10)
				self.fleet.move(return_point, self._state_fleet_callbacks[self.missionStates.fleeing_home])
				self.state = self.missionStates.fleeing_home
			except MoveNotPossible:
				self.report_failure("Combat was lost, ships couldn't flee home")
		else:
			self.report_failure("Combat was lost, all ships were wiped out")

	@classmethod
	def create(cls, success_callback, failure_callback, fleet, target_ship):
		return ChaseShipsAndAttack(success_callback, failure_callback, fleet, target_ship)

	def _on_ship_destroyed(self, msg):
		if msg.sender is self.target_ship:
			self.check_ship_alive()
			assert not self.target_ship.in_ship_map
			ShipDestroyed.unsubscribe(self._on_ship_destroyed)

	def end(self):
		if self.target_ship.in_ship_map:
			ShipDestroyed.unsubscribe(self._on_ship_destroyed)
		super(ChaseShipsAndAttack, self).end()
Пример #25
0
class AIPlayer(GenericAI):
    """This is the AI that builds settlements."""

    shipStates = Enum.get_extended(
        GenericAI.shipStates,
        'on_a_mission',
    )

    log = logging.getLogger("ai.aiplayer")
    tick_interval = 32
    tick_long_interval = 128

    def __init__(self, session, id, name, color, clientid, difficulty_level,
                 **kwargs):
        super(AIPlayer, self).__init__(session, id, name, color, clientid,
                                       difficulty_level, **kwargs)
        self.need_more_ships = False
        self.need_more_combat_ships = True
        self.need_feeder_island = False
        self.personality_manager = PersonalityManager(self)
        self.__init()
        Scheduler().add_new_object(Callback(self.finish_init), self, run_in=0)

    def start(self):
        """ Start the AI tick process. Try to space out their ticks evenly. """
        ai_players = 0
        position = None
        for player in self.world.players:
            if isinstance(player, AIPlayer):
                if player is self:
                    position = ai_players
                ai_players += 1
        run_in = self.tick_interval * position // ai_players + 1
        Scheduler().add_new_object(Callback(self.tick), self, run_in=run_in)
        run_in = self.tick_long_interval * position // ai_players + 1
        Scheduler().add_new_object(Callback(self.tick_long),
                                   self,
                                   run_in=run_in)

    def finish_init(self):
        # initialize the things that couldn't be initialized before because of the loading order
        self.refresh_ships()
        self.start()

    def refresh_ships(self):
        """ called when a new ship is added to the fleet """
        for ship in self.world.ships:
            if ship.owner == self and ship.has_component(
                    SelectableComponent) and ship not in self.ships:
                self.log.info('%s Added %s to the fleet', self, ship)
                self.ships[ship] = self.shipStates.idle
                if isinstance(ship, MovingWeaponHolder):
                    ship.stance = NoneStance
                if isinstance(ship, FightingShip):
                    self.combat_manager.add_new_unit(ship)
        self.need_more_ships = False
        self.need_more_combat_ships = False

    def __init(self):
        self._enabled = True  # whether this player is enabled (currently disabled at the end of the game)
        self.world = self.session.world
        self.islands = {}
        self.settlement_managers = []
        self._settlement_manager_by_settlement_id = {}
        self.missions = set()
        self.fishers = []
        self.settlement_founder = SettlementFounder(self)
        self.unit_builder = UnitBuilder(self)
        self.unit_manager = UnitManager(self)
        self.combat_manager = CombatManager(self)
        self.strategy_manager = StrategyManager(self)
        self.behavior_manager = BehaviorManager(self)
        self.settlement_expansions = []  # [(coords, settlement)]
        self.goals = [DoNothingGoal(self)]
        self.special_domestic_trade_manager = SpecialDomesticTradeManager(self)
        self.international_trade_manager = InternationalTradeManager(self)
        SettlementRangeChanged.subscribe(self._on_settlement_range_changed)
        NewDisaster.subscribe(self.notify_new_disaster)
        MineEmpty.subscribe(self.notify_mine_empty)

    def get_random_profile(self, token):
        return BehaviorProfileManager.get_random_player_profile(self, token)

    def start_mission(self, mission):
        self.ships[mission.ship] = self.shipStates.on_a_mission
        self.missions.add(mission)
        mission.start()

    def report_success(self, mission, msg):
        if not self._enabled:
            return

        self.missions.remove(mission)
        if mission.ship and mission.ship in self.ships:
            self.ships[mission.ship] = self.shipStates.idle
        if isinstance(mission, FoundSettlement):
            settlement_manager = SettlementManager(self, mission.land_manager)
            self.settlement_managers.append(settlement_manager)
            self._settlement_manager_by_settlement_id[
                settlement_manager.settlement.worldid] = settlement_manager
            self.add_building(settlement_manager.settlement.warehouse)
            if settlement_manager.feeder_island:
                self.need_feeder_island = False
        elif isinstance(mission, PrepareFoundationShip):
            self.settlement_founder.tick()

    def report_failure(self, mission, msg):
        if not self._enabled:
            return

        self.missions.remove(mission)
        if mission.ship and mission.ship in self.ships:
            self.ships[mission.ship] = self.shipStates.idle
        if isinstance(mission, FoundSettlement):
            del self.islands[mission.land_manager.island.worldid]

    def save(self, db):
        super(AIPlayer, self).save(db)

        # save the player
        db("UPDATE player SET client_id = 'AIPlayer' WHERE rowid = ?",
           self.worldid)

        current_callback = Callback(self.tick)
        calls = Scheduler().get_classinst_calls(self, current_callback)
        assert len(calls) == 1, "got %s calls for saving %s: %s" % (
            len(calls), current_callback, calls)
        remaining_ticks = max(calls.values()[0], 1)

        current_callback_long = Callback(self.tick_long)
        calls = Scheduler().get_classinst_calls(self, current_callback_long)
        assert len(calls) == 1, "got %s calls for saving %s: %s" % (
            len(calls), current_callback_long, calls)
        remaining_ticks_long = max(calls.values()[0], 1)

        db(
            "INSERT INTO ai_player(rowid, need_more_ships, need_more_combat_ships, need_feeder_island, remaining_ticks, remaining_ticks_long) VALUES(?, ?, ?, ?, ?, ?)",
            self.worldid, self.need_more_ships, self.need_more_combat_ships,
            self.need_feeder_island, remaining_ticks, remaining_ticks_long)

        # save the ships
        for ship, state in self.ships.iteritems():
            db("INSERT INTO ai_ship(rowid, owner, state) VALUES(?, ?, ?)",
               ship.worldid, self.worldid, state.index)

        # save the land managers
        for land_manager in self.islands.itervalues():
            land_manager.save(db)

        # save the settlement managers
        for settlement_manager in self.settlement_managers:
            settlement_manager.save(db)

        # save the missions
        for mission in self.missions:
            mission.save(db)

        # save the personality manager
        self.personality_manager.save(db)

        # save the unit manager
        self.unit_manager.save(db)

        # save the combat manager
        self.combat_manager.save(db)

        # save the strategy manager
        self.strategy_manager.save(db)

        # save the behavior manager
        self.behavior_manager.save(db)

    def _load(self, db, worldid):
        super(AIPlayer, self)._load(db, worldid)
        self.personality_manager = PersonalityManager.load(db, self)
        self.__init()

        self.need_more_ships, self.need_more_combat_ships, self.need_feeder_island, remaining_ticks, remaining_ticks_long = \
         db("SELECT need_more_ships, need_more_combat_ships, need_feeder_island, remaining_ticks, remaining_ticks_long FROM ai_player WHERE rowid = ?", worldid)[0]
        Scheduler().add_new_object(Callback(self.tick),
                                   self,
                                   run_in=remaining_ticks)
        Scheduler().add_new_object(Callback(self.tick_long),
                                   self,
                                   run_in=remaining_ticks_long)

    def finish_loading(self, db):
        """ This is called separately because most objects are loaded after the player. """

        # load the ships

        for ship_id, state_id in db(
                "SELECT rowid, state FROM ai_ship WHERE owner = ?",
                self.worldid):
            ship = WorldObject.get_object_by_id(ship_id)
            self.ships[ship] = self.shipStates[state_id]

        # load unit manager
        self.unit_manager = UnitManager.load(db, self)

        # load combat manager
        self.combat_manager = CombatManager.load(db, self)

        # load strategy manager
        self.strategy_manager = StrategyManager.load(db, self)

        # load BehaviorManager
        self.behavior_manager = BehaviorManager.load(db, self)

        # load the land managers
        for (worldid, ) in db(
                "SELECT rowid FROM ai_land_manager WHERE owner = ?",
                self.worldid):
            land_manager = LandManager.load(db, self, worldid)
            self.islands[land_manager.island.worldid] = land_manager

        # load the settlement managers and settlement foundation missions
        for land_manager in self.islands.itervalues():
            db_result = db(
                "SELECT rowid FROM ai_settlement_manager WHERE land_manager = ?",
                land_manager.worldid)
            if db_result:
                settlement_manager = SettlementManager.load(
                    db, self, db_result[0][0])
                self.settlement_managers.append(settlement_manager)
                self._settlement_manager_by_settlement_id[
                    settlement_manager.settlement.worldid] = settlement_manager

                # load the foundation ship preparing missions
                db_result = db(
                    "SELECT rowid FROM ai_mission_prepare_foundation_ship WHERE settlement_manager = ?",
                    settlement_manager.worldid)
                for (mission_id, ) in db_result:
                    self.missions.add(
                        PrepareFoundationShip.load(db, mission_id,
                                                   self.report_success,
                                                   self.report_failure))
            else:
                mission_id = db(
                    "SELECT rowid FROM ai_mission_found_settlement WHERE land_manager = ?",
                    land_manager.worldid)[0][0]
                self.missions.add(
                    FoundSettlement.load(db, mission_id, self.report_success,
                                         self.report_failure))

        for settlement_manager in self.settlement_managers:
            # load the domestic trade missions
            db_result = db(
                "SELECT rowid FROM ai_mission_domestic_trade WHERE source_settlement_manager = ?",
                settlement_manager.worldid)
            for (mission_id, ) in db_result:
                self.missions.add(
                    DomesticTrade.load(db, mission_id, self.report_success,
                                       self.report_failure))

            # load the special domestic trade missions
            db_result = db(
                "SELECT rowid FROM ai_mission_special_domestic_trade WHERE source_settlement_manager = ?",
                settlement_manager.worldid)
            for (mission_id, ) in db_result:
                self.missions.add(
                    SpecialDomesticTrade.load(db, mission_id,
                                              self.report_success,
                                              self.report_failure))

            # load the international trade missions
            db_result = db(
                "SELECT rowid FROM ai_mission_international_trade WHERE settlement_manager = ?",
                settlement_manager.worldid)
            for (mission_id, ) in db_result:
                self.missions.add(
                    InternationalTrade.load(db, mission_id,
                                            self.report_success,
                                            self.report_failure))

    def tick(self):
        Scheduler().add_new_object(Callback(self.tick),
                                   self,
                                   run_in=self.tick_interval)
        self.settlement_founder.tick()
        self.handle_enemy_expansions()
        self.handle_settlements()
        self.special_domestic_trade_manager.tick()
        self.international_trade_manager.tick()
        self.unit_manager.tick()
        self.combat_manager.tick()

    def tick_long(self):
        """
		Same as above but used for reasoning that is not required to be called as often (such as diplomacy, strategy etc.)
		"""
        Scheduler().add_new_object(Callback(self.tick_long),
                                   self,
                                   run_in=self.tick_long_interval)
        self.strategy_manager.tick()

    def handle_settlements(self):
        goals = []
        for goal in self.goals:
            if goal.can_be_activated:
                goal.update()
                goals.append(goal)
        for settlement_manager in self.settlement_managers:
            settlement_manager.tick(goals)
        goals.sort(reverse=True)

        settlements_blocked = set()  # set([settlement_manager_id, ...])
        for goal in goals:
            if not goal.active:
                continue
            if isinstance(
                    goal, SettlementGoal
            ) and goal.settlement_manager.worldid in settlements_blocked:
                continue  # can't build anything in this settlement
            result = goal.execute()
            if result == GOAL_RESULT.SKIP:
                self.log.info('%s, skipped goal %s', self, goal)
            elif result == GOAL_RESULT.BLOCK_SETTLEMENT_RESOURCE_USAGE:
                self.log.info(
                    '%s blocked further settlement resource usage by goal %s',
                    self, goal)
                settlements_blocked.add(goal.settlement_manager.worldid)
                goal.settlement_manager.need_materials = True
            else:
                self.log.info(
                    '%s all further goals during this tick blocked by goal %s',
                    self, goal)
                break  # built something; stop because otherwise the AI could look too fast

        self.log.info('%s had %d active goals', self,
                      sum(goal.active for goal in goals))
        for goal in goals:
            if goal.active:
                self.log.info('%s %s', self, goal)

        # refresh taxes and upgrade permissions
        for settlement_manager in self.settlement_managers:
            settlement_manager.refresh_taxes_and_upgrade_permissions()

    def request_ship(self):
        self.log.info('%s received request for more ships', self)
        self.need_more_ships = True

    def request_combat_ship(self):
        self.log.info('%s received request for combat ships', self)
        self.need_more_combat_ships = True

    def add_building(self, building):
        assert self._enabled
        # if the settlement id is not present then this is a new settlement that has to be handled separately
        if building.settlement.worldid in self._settlement_manager_by_settlement_id:
            self._settlement_manager_by_settlement_id[
                building.settlement.worldid].add_building(building)

    def remove_building(self, building):
        if not self._enabled:
            return

        self._settlement_manager_by_settlement_id[
            building.settlement.worldid].remove_building(building)

    def remove_unit(self, unit):
        if not self._enabled:
            return

        if unit in self.ships:
            del self.ships[unit]
        self.combat_manager.remove_unit(unit)
        self.unit_manager.remove_unit(unit)

    def count_buildings(self, building_id):
        return sum(
            settlement_manager.settlement.count_buildings(building_id)
            for settlement_manager in self.settlement_managers)

    def notify_mine_empty(self, message):
        """The Mine calls this function to let the player know that the mine is empty."""
        settlement = message.mine.settlement
        if settlement.owner is self:
            self._settlement_manager_by_settlement_id[
                settlement.worldid].production_builder.handle_mine_empty(
                    message.mine)

    def notify_new_disaster(self, message):
        settlement = message.building.settlement
        if settlement.owner is self:
            Scheduler().add_new_object(Callback(
                self._settlement_manager_by_settlement_id[
                    settlement.worldid].handle_disaster, message),
                                       self,
                                       run_in=0)

    def _on_settlement_range_changed(self, message):
        """Stores the ownership changes in a list for later processing."""
        our_new_coords_list = []
        settlement = message.sender

        for tile in message.changed_tiles:
            coords = (tile.x, tile.y)
            if settlement.owner is self:
                our_new_coords_list.append(coords)
            else:
                self.settlement_expansions.append((coords, settlement))

        if our_new_coords_list and settlement.worldid in self._settlement_manager_by_settlement_id:
            self._settlement_manager_by_settlement_id[
                settlement.
                worldid].production_builder.road_connectivity_cache.modify_area(
                    our_new_coords_list)

    def handle_enemy_expansions(self):
        if not self.settlement_expansions:
            return  # no changes in land ownership

        change_lists = defaultdict(lambda: [])
        for coords, settlement in self.settlement_expansions:
            if settlement.island.worldid not in self.islands:
                continue  # we don't have a settlement there and have no current plans to create one
            change_lists[settlement.island.worldid].append(coords)
        self.settlement_expansions = []
        if not change_lists:
            return  # no changes in land ownership on islands we care about

        for island_id, changed_coords in change_lists.iteritems():
            affects_us = False
            land_manager = self.islands[island_id]
            for coords in changed_coords:
                if coords in land_manager.production or coords in land_manager.village:
                    affects_us = True
                    break
            if not affects_us:
                continue  # we weren't using that land anyway

            settlement_manager = None
            for potential_settlement_manager in self.settlement_managers:
                if potential_settlement_manager.settlement.island.worldid == island_id:
                    settlement_manager = potential_settlement_manager
                    break

            if settlement_manager is None:
                self.handle_enemy_settling_on_our_chosen_island(island_id)
                # we are on the way to found a settlement on that island
            else:
                # we already have a settlement there
                settlement_manager.handle_lost_area(changed_coords)

    def handle_enemy_settling_on_our_chosen_island(self, island_id):
        mission = None
        for a_mission in self.missions:
            if isinstance(
                    a_mission, FoundSettlement
            ) and a_mission.land_manager.island.worldid == island_id:
                mission = a_mission
                break
        assert mission
        mission.cancel()
        self.settlement_founder.tick()

    @classmethod
    def load_abstract_buildings(cls, db):
        AbstractBuilding.load_all(db)

    @classmethod
    def clear_caches(cls):
        BasicBuilder.clear_cache()
        AbstractFarm.clear_cache()

    def __str__(self):
        return 'AI(%s/%s)' % (
            self.name if hasattr(self, 'name') else 'unknown',
            self.worldid if hasattr(self, 'worldid') else 'none')

    def early_end(self):
        """Called to speed up session destruction."""
        assert self._enabled
        self._enabled = False
        SettlementRangeChanged.unsubscribe(self._on_settlement_range_changed)
        NewDisaster.unsubscribe(self.notify_new_disaster)
        MineEmpty.unsubscribe(self.notify_mine_empty)

    def end(self):
        assert not self._enabled
        self.personality_manager = None
        self.world = None
        self.islands = None
        self.settlement_managers = None
        self._settlement_manager_by_settlement_id = None
        self.missions = None
        self.fishers = None
        self.settlement_founder = None
        self.unit_builder = None
        self.unit_manager = None
        self.behavior_manager = None
        self.combat_manager = None
        self.settlement_expansions = None
        self.goals = None
        self.special_domestic_trade_manager = None
        self.international_trade_manager = None
        self.strategy_manager.end()
        self.strategy_manager = None
        super(AIPlayer, self).end()
Пример #26
0
class Fleet(WorldObject):
	"""
	Fleet object is responsible for moving a group of ship around the map in an ordered manner, that is:
	1. provide a single move callback for a fleet as a whole,
	2. resolve self-blocks in a group of ships
	3. resolve MoveNotPossible exceptions.
	"""

	log = logging.getLogger("ai.aiplayer.fleet")

	# ship states inside a fleet, fleet doesn't care about AIPlayer.shipStates since it doesn't do any reasoning.
	# all fleet cares about is to move ships from A to B.
	shipStates = Enum('idle', 'moving', 'blocked', 'reached')

	RETRY_BLOCKED_TICKS = 16

	# state for a fleet as a whole
	fleetStates = Enum('idle', 'moving')

	def __init__(self, ships, destroy_callback=None):
		super(Fleet, self).__init__()

		assert ships, "request to create a fleet from  %s ships" % (len(ships))
		self.__init(ships, destroy_callback)

	def __init(self, ships, destroy_callback=None):
		self.owner = ships[0].owner

		# dictionary of ship => state
		self._ships = WeakKeyDictionary()
		for ship in ships:
			self._ships[ship] = self.shipStates.idle
			#TODO: @below, this caused errors on one occasion but I was not able to reproduce it.
			ship.add_remove_listener(Callback(self._lost_ship, ship))
		self.state = self.fleetStates.idle
		self.destroy_callback = destroy_callback

	def save(self, db):
		super(Fleet, self).save(db)
		# save the fleet
		# save destination if fleet is moving somewhere
		db("INSERT INTO fleet (fleet_id, owner_id, state_id) VALUES(?, ?, ?)", self.worldid, self.owner.worldid, self.state.index)

		if self.state == self.fleetStates.moving and hasattr(self, 'destination'):
			if isinstance(self.destination, Point):
				x, y = self.destination.x, self.destination.y
				db("UPDATE fleet SET dest_x = ?, dest_y = ? WHERE fleet_id = ?", x, y, self.worldid)
			elif isinstance(self.destination, Circle):
				x, y, radius = self.destination.center.x, self.destination.center.y, self.destination.radius
				db("UPDATE fleet SET dest_x = ?, dest_y = ?, radius = ? WHERE fleet_id = ?", x, y, radius, self.worldid)
			else:
				assert False, "destination is neither a Circle nor a Point: %s" % self.destination.__class__.__name__

		if hasattr(self, "ratio"):
			db("UPDATE fleet SET ratio = ? WHERE fleet_id = ?", self.ratio, self.worldid)

		# save ships
		for ship in self.get_ships():
			db("INSERT INTO fleet_ship (ship_id, fleet_id, state_id) VALUES(?, ?, ?)",
			   ship.worldid, self.worldid, self._ships[ship].index)

	def _load(self, worldid, owner, db, destroy_callback):
		super(Fleet, self).load(db, worldid)
		self.owner = owner
		state_id, dest_x, dest_y, radius, ratio = db("SELECT state_id, dest_x, dest_y, radius, ratio FROM fleet WHERE fleet_id = ?", worldid)[0]

		if radius:  # Circle
			self.destination = Circle(Point(dest_x, dest_y), radius)
		elif dest_x and dest_y:  # Point
			self.destination = Point(dest_x, dest_y)
		else:  # No destination
			pass

		if ratio:
			self.ratio = ratio

		ships_states = [(WorldObject.get_object_by_id(ship_id), self.shipStates[ship_state_id])
		                for ship_id, ship_state_id
		                in db("SELECT ship_id, state_id FROM fleet_ship WHERE fleet_id = ?", worldid)]
		ships = [item[0] for item in ships_states]

		self.__init(ships, destroy_callback)
		self.state = self.fleetStates[state_id]

		for ship, state in ships_states:
			self._ships[ship] = state

		if self.state == self.fleetStates.moving:
			for ship in self.get_ships():
				if self._ships[ship] == self.shipStates.moving:
					ship.add_move_callback(Callback(self._ship_reached, ship))

		if destroy_callback:
			self.destroy_callback = destroy_callback

	@classmethod
	def load(cls, worldid, owner, db, destroy_callback=None):
		self = cls.__new__(cls)
		self._load(worldid, owner, db, destroy_callback)
		return self

	def get_ships(self):
		return self._ships.keys()

	def destroy(self):
		for ship in self._ships.keys():
			ship.remove_remove_listener(self._lost_ship)
		if self.destroy_callback:
			self.destroy_callback()

	def _lost_ship(self, ship):
		"""
		Used when fleet was on the move and one of the ships was killed during that.
		This way fleet has to check whether the target point was reached.
		"""
		if ship in self._ships:
			del self._ships[ship]
		if self.size() == 0:
			self.destroy()
		elif self._was_target_reached():
			self._fleet_reached()

	def _get_ship_states_count(self):
		"""
		Returns Counter about how many ships are in state idle, moving, reached.
		"""
		counter = defaultdict(int)
		for value in self._ships.values():
			counter[value] += 1
		return counter

	def _was_target_reached(self):
		"""
		Checks whether required ratio of ships reached the target.
		"""
		state_counts = self._get_ship_states_count()

		# below: include blocked ships as "reached" as well since there's not much more left to do,
		# and it's better than freezing the whole fleet
		reached = state_counts[self.shipStates.reached] + state_counts[self.shipStates.blocked]
		total = len(self._ships)
		return self.ratio <= float(reached) / total

	def _ship_reached(self, ship):
		"""
		Called when a single ship reaches destination.
		"""
		self.log.debug("Fleet %s, Ship %s reached the destination", self.worldid, ship.get_component(NamedComponent).name)
		self._ships[ship] = self.shipStates.reached
		if self._was_target_reached():
			self._fleet_reached()

	def _fleet_reached(self):
		"""
		Called when whole fleet reaches destination.
		"""
		self.log.debug("Fleet %s reached the destination", self.worldid)
		self.state = self.fleetStates.idle
		for ship in self._ships.keys():
			self._ships[ship] = self.shipStates.idle

		if self.callback:
			self.callback()

	def _move_ship(self, ship, destination, callback):
		# retry ad infinitum. Not the most elegant solution but will do for a while.
		# Idea: mark ship as "blocked" through state and check whether they all are near the destination anyway
		# 1. If they don't make them sail again.
		# 2. If they do, assume they reached the spot.
		try:
			ship.move(destination, callback=callback, blocked_callback=Callback(self._move_ship, ship, destination, callback))
			self._ships[ship] = self.shipStates.moving
		except MoveNotPossible:
			self._ships[ship] = self.shipStates.blocked
			if not self._was_target_reached():
				Scheduler().add_new_object(Callback(self._retry_moving_blocked_ships), self, run_in=self.RETRY_BLOCKED_TICKS)

	def _get_circle_size(self):
		"""
		Destination circle size for movement calls that involve more than one ship.
		"""
		return 10
		#return min(self.size(), 5)

	def _retry_moving_blocked_ships(self):
		if self.state != self.fleetStates.moving:
			return

		for ship in filter(lambda ship: self._ships[ship] == self.shipStates.blocked, self.get_ships()):
			self._move_ship(ship, self.destination, Callback(self._ship_reached, ship))

	def move(self, destination, callback=None, ratio=1.0):
		"""
		Move fleet to a destination.
		@param ratio: what percentage of ships has to reach destination in order for the move to be considered done:
			0.0 - None (not really useful, executes the callback right away)
			0.0001 - effectively ANY ship
			1.0 - ALL of the ships
			0.5 - at least half of the ships
			etc.
		"""
		assert self.size() > 0, "ordered to move a fleet consisting of 0 ships"

		# it's ok to specify single point for a destination only when there's only one ship in a fleet
		if isinstance(destination, Point) and self.size() > 1:
			destination = Circle(destination, self._get_circle_size())

		self.destination = destination
		self.state = self.fleetStates.moving
		self.ratio = ratio

		self.callback = callback

		# This is a good place to do something fancier later like preserving ship formation instead sailing to the same point
		for ship in self._ships.keys():
			self._move_ship(ship, destination, Callback(self._ship_reached, ship))

	def size(self):
		return len(self._ships)

	def __str__(self):
		if hasattr(self, '_ships'):
			ships_str = "\n   " + "\n   ".join(["%s (fleet state:%s)" % (ship.get_component(NamedComponent).name, self._ships[ship]) for ship in self._ships.keys()])
		else:
			ships_str = 'N/A'
		return "Fleet: %s , state: %s, ships:%s" % (self.worldid, (self.state if hasattr(self, 'state') else 'unknown state'), ships_str)
Пример #27
0
class JobList(list):
    """Data structure for evaluating best jobs.
	It's a list extended by special sort functions.
	"""
    order_by = Enum('rating', 'amount', 'random', 'fewest_available',
                    'fewest_available_and_distance', 'for_storage_collector',
                    'distance')

    def __init__(self, collector, job_order):
        """
		@param collector: collector instance
		@param job_order: instance of order_by-Enum
		"""
        super(JobList, self).__init__()
        self.collector = collector
        # choose actual function by name of enum value
        sort_fun_name = '_sort_jobs_' + str(job_order)
        if not hasattr(self, sort_fun_name):
            self._selected_sort_jobs = self._sort_jobs_amount
            print('WARNING: invalid job order: ', job_order)
        else:
            self._selected_sort_jobs = getattr(self, sort_fun_name)

    def sort_jobs(self):
        """Call this to sort jobs.

		The function to call is decided in `__init__`.
		"""
        self._selected_sort_jobs()

    def _sort_jobs_random(self):
        """Sorts jobs randomly"""
        self.collector.session.random.shuffle(self)

    def _sort_jobs_amount(self):
        """Sorts the jobs by the amount of resources available"""
        self.sort(key=operator.attrgetter('amount_sum'), reverse=True)

    def _sort_jobs_fewest_available(self, shuffle_first=True):
        """Prefer jobs where least amount is available in obj's inventory.
		Only considers resource of resource list with minimum amount available.
		This is supposed to fix urgent shortages."""
        # shuffle list before sorting, so that jobs with same value have equal chance
        if shuffle_first:
            self.collector.session.random.shuffle(self)
        inventory = self.collector.get_home_inventory()
        self.sort(key=lambda job: min(inventory[res] for res in job.resources),
                  reverse=False)

    def _sort_jobs_fewest_available_and_distance(self):
        """Sort jobs by distance, but secondarily also consider fewest available resources"""
        # python sort is stable, so two sequenced sorts work.
        self._sort_jobs_fewest_available(shuffle_first=False)
        self._sort_jobs_distance()

    def _sort_jobs_for_storage_collector(self):
        """Special sophisticated sorting routing for storage collectors.
		Same as fewest_available_and_distance_, but also considers whether target inv is full."""
        self._sort_jobs_fewest_available_and_distance()
        self._sort_target_inventory_full()

    def _sort_jobs_distance(self):
        """Prefer targets that are nearer"""
        collector_point = self.collector.position
        self.sort(
            key=lambda job: collector_point.distance(job.object.loading_area))

    def _sort_target_inventory_full(self):
        """Prefer targets with full inventory"""
        self.sort(key=operator.attrgetter('target_inventory_full_num'),
                  reverse=True)

    def __str__(self):
        return str([str(i) for i in self])
Пример #28
0
class Pirate(GenericAI):
    """A pirate ship moving randomly around. If another ship comes into the reach
	of it, it will be followed for a short time."""

    # TODO: Move on_a_mission to GenericAI
    shipStates = Enum.get_extended(GenericAI.shipStates, 'on_a_mission',
                                   'chasing_ship', 'going_home')

    log = logging.getLogger("ai.pirate")
    regular_player = False

    caught_ship_radius = 5
    home_radius = 2

    ship_count = 1

    tick_interval = 32
    tick_long_interval = 128

    def __init__(self, session, id, name, color, **kwargs):
        super(Pirate, self).__init__(session, id, name, color, **kwargs)

        # choose a random water tile on the coast and call it home
        self.home_point = self.session.world.get_random_possible_coastal_ship_position(
        )
        # random sea tile if costal tile not found. Empty map?
        if not self.home_point:
            self.home_point = self.session.world.get_random_possible_ship_position(
            )
        self.log.debug("Pirate: home at (%d, %d), radius %d",
                       self.home_point.x, self.home_point.y, self.home_radius)
        self.__init()

        # create a ship and place it randomly (temporary hack)
        for i in range(self.ship_count):
            self.create_ship_at_random_position()

        Scheduler().add_new_object(Callback(self.tick), self, 1, -1,
                                   self.tick_interval)
        Scheduler().add_new_object(Callback(self.tick_long), self, 1, -1,
                                   self.tick_long_interval)

    def __init(self):
        self.world = self.session.world
        self.unit_manager = UnitManager(self)
        self.combat_manager = PirateCombatManager(self)
        self.strategy_manager = PirateStrategyManager(self)
        self.behavior_manager = BehaviorManager(self)

    @staticmethod
    def get_nearest_player_ship(base_ship):
        lowest_distance = None
        nearest_ship = None
        for ship in base_ship.find_nearby_ships():
            if isinstance(
                    ship,
                (PirateShip,
                 TradeShip)) or not ship.has_component(SelectableComponent):
                continue  # don't attack these ships
            distance = base_ship.position.distance(ship.position)
            if lowest_distance is None or distance < lowest_distance:
                lowest_distance = distance
                nearest_ship = ship
        return nearest_ship

    def tick(self):
        self.combat_manager.tick()

        # Temporary function for pirates respawn
        self.maintain_ship_count()

    def tick_long(self):
        self.strategy_manager.tick()

    def get_random_profile(self, token):
        return BehaviorProfileManager.get_random_pirate_profile(self, token)

    def create_ship_at_random_position(self):
        point = self.session.world.get_random_possible_ship_position()
        ship = CreateUnit(self.worldid, UNITS.PIRATE_SHIP, point.x,
                          point.y)(issuer=self.session.world.player)
        self.ships[ship] = self.shipStates.idle
        self.combat_manager.add_new_unit(ship)

    def maintain_ship_count(self):
        if len(list(self.ships.keys())) < self.ship_count:
            self.create_ship_at_random_position()

    def save(self, db):
        super(Pirate, self).save(db)
        db("UPDATE player SET is_pirate = 1 WHERE rowid = ?", self.worldid)
        db("INSERT INTO pirate_home_point(x, y) VALUES(?, ?)",
           self.home_point.x, self.home_point.y)

        current_callback = Callback(self.tick)
        calls = Scheduler().get_classinst_calls(self, current_callback)
        assert len(calls) == 1, "got %s calls for saving %s: %s" % (
            len(calls), current_callback, calls)
        remaining_ticks = max(list(calls.values())[0], 1)

        current_callback_long = Callback(self.tick_long)
        calls = Scheduler().get_classinst_calls(self, current_callback_long)
        assert len(calls) == 1, "got %s calls for saving %s: %s" % (
            len(calls), current_callback_long, calls)
        remaining_ticks_long = max(list(calls.values())[0], 1)

        db(
            "INSERT INTO ai_pirate(rowid, remaining_ticks, remaining_ticks_long) VALUES(?, ?, ?)",
            self.worldid, remaining_ticks, remaining_ticks_long)

        for ship in self.ships:
            ship_state = self.ships[ship]
            db("INSERT INTO pirate_ships(rowid, state) VALUES(?, ?)",
               ship.worldid, ship_state.index)

        # save unit manager
        self.unit_manager.save(db)

        # save combat manager
        self.combat_manager.save(db)

        # save strategy manager
        self.strategy_manager.save(db)

        # save behavior manager
        self.behavior_manager.save(db)

    def _load(self, db, worldid):
        super(Pirate, self)._load(db, worldid)
        self.__init()

        remaining_ticks, = db(
            "SELECT remaining_ticks FROM ai_pirate WHERE rowid = ?",
            worldid)[0]
        Scheduler().add_new_object(Callback(self.tick), self, remaining_ticks,
                                   -1, self.tick_interval)

        remaining_ticks_long, = db(
            "SELECT remaining_ticks_long FROM ai_pirate WHERE rowid = ?",
            worldid)[0]
        Scheduler().add_new_object(Callback(self.tick_long), self,
                                   remaining_ticks_long, -1,
                                   self.tick_interval)

        home = db("SELECT x, y FROM pirate_home_point")[0]
        self.home_point = Point(home[0], home[1])

        self.log.debug("Pirate: home at (%d, %d), radius %d",
                       self.home_point.x, self.home_point.y, self.home_radius)

    def finish_loading(self, db):
        # load ships one by one from db (ship instances themselves are loaded already, but
        # we have to use them here)

        for ship_id, state_id in db("SELECT rowid, state FROM pirate_ships"):
            state = self.shipStates[state_id]
            ship = WorldObject.get_object_by_id(ship_id)
            self.ships[ship] = state

        # load unit manager
        self.unit_manager = UnitManager.load(db, self)

        # load combat manager
        self.combat_manager = PirateCombatManager.load(db, self)

        # load strategy manager
        self.strategy_manager = PirateStrategyManager.load(db, self)

        # load BehaviorManager
        self.behavior_manager = BehaviorManager.load(db, self)

    def remove_unit(self, unit):
        """Called when a ship which is owned by the pirate is removed or killed."""
        del self.ships[unit]
        self.combat_manager.remove_unit(unit)
        self.unit_manager.remove_unit(unit)

    def end(self):
        self.strategy_manager.end()
        super(Pirate, self).end()
class BehaviorDiplomatic(BehaviorComponent):
    """
	Behaviors that handle diplomacy.
	"""

    # value to which each function is related, so even when relationship_score is at the peek somewhere (e.g. it's value is 1.0)
    # probability to actually choose given action is peek/upper_boundary (0.2 in case of upper_boundary = 5.0)
    upper_boundary = DiplomacySettings.upper_boundary

    # possible actions behavior can take
    actions = Enum('wait', 'war', 'peace', 'neutral')

    def calculate_relationship_score(self, balance, weights):
        """
		Calculate total relationship_score based on balances and their weights.
		Count only balances that have weight defined (this way "default" weight is 0)
		"""
        return sum((getattr(balance, key) * value
                    for key, value in weights.iteritems()))

    @classmethod
    def _move_f(cls, f, v_x, v_y):
        """
		Return function f moved by vector (v_x, v_y)
		"""
        return lambda x: f(x - v_x) + v_y

    def handle_diplomacy(self, parameters, **environment):
        """
		Main function responsible for handling diplomacy.
		"""
        player = environment['player']
        balance = self.owner.strategy_manager.calculate_player_balance(player)
        relationship_score = self.calculate_relationship_score(
            balance, self.weights)
        action = self._get_action(relationship_score, **parameters)
        self.log.debug(
            "%s vs %s | Dipomacy: balance:%s, relationship_score:%s, action:%s",
            self.owner.name, player.name, balance, relationship_score, action)
        self._perform_action(action, **environment)

    def _perform_action(self, action, **environment):
        """
		Execute action from actions Enum.
		"""
        player = environment['player']

        # ideally this shouldn't automatically change diplomacy for both players (i.e. add_pair) but do it for one side only.

        if action == self.actions.war:
            self.session.world.diplomacy.add_enemy_pair(self.owner, player)
        elif action == self.actions.peace:
            self.session.world.diplomacy.add_ally_pair(self.owner, player)
        elif action == self.actions.neutral:
            self.session.world.diplomacy.add_neutral_pair(self.owner, player)

    @classmethod
    def _get_quadratic_function(cls, mid, root, peek=1.0):
        """
		Functions for border distributions such as enemy or ally (left or right parabola).
		@param mid: value on axis X that is to be center of the parabola
		@type mid: float
		@param root: value on axis X which is a crossing point of axis OX and the function itself
		@type root: float
		@param peek: value on axis Y which is a peek of a function
		@type peek: float
		@return: quadratic function
		@rtype: lambda(x)
		"""

        # base function is upside-down parabola, stretched in X in order to have roots at exactly 'root' value.
        # (-1. / (abs(mid - root) ** 2)) part is for stretching the parabola in X axis and flipping it upside down, we have to use
        # abs(mid - root) because it's later moved by mid
        # Note: Multiply by 1./abs(mid-root) to scale function in X (e.g. if mid is 1.0 and root is 1.5 -> make original x^2 function 2 times narrower
        base = lambda x: (-1. / (abs(mid - root)**2)) * (x**2)

        # we move the function so it looks like "distribution", i.e. move it far left(or right), and assume the peek is 1.0
        moved = cls._move_f(base, mid, 1.0)

        # in case of negative values of f(x) we want to have 0.0 instead
        # we multiply by peek here in order to scale function in Y
        final_function = lambda x: max(0.0, moved(x) * peek)

        return final_function

    @classmethod
    def get_enemy_function(cls, root, peek=1.0):
        return cls._get_quadratic_function(-10.0, root, peek)

    @classmethod
    def get_ally_function(cls, root, peek=1.0):
        return cls._get_quadratic_function(10.0, root, peek)

    @classmethod
    def get_neutral_function(cls, mid, root, peek=1.0):
        return cls._get_quadratic_function(mid, root, peek)

    def _choose_random_from_tuple(self, tuple):
        """
		Choose random action from tuple of (name, value)
		"""
        total_probability = sum((item[1] for item in tuple))
        random_value = self.session.random.random() * total_probability
        counter = 0.0
        for item in tuple:
            if item[1] + counter >= random_value:
                return item[0]
            else:
                counter += item[1]

    def _get_action(self, relationship_score, **parameters):
        possible_actions = []
        if 'enemy' in parameters:
            enemy_params = parameters['enemy']
            possible_actions.append((
                self.actions.war,
                self.get_enemy_function(**enemy_params)(relationship_score),
            ))

        if 'ally' in parameters:
            ally_params = parameters['ally']
            possible_actions.append((
                self.actions.peace,
                self.get_ally_function(**ally_params)(relationship_score),
            ))

        if 'neutral' in parameters:
            neutral_params = parameters['neutral']
            possible_actions.append((
                self.actions.neutral,
                self.get_neutral_function(
                    **neutral_params)(relationship_score),
            ))

        max_probability = max((item[1] for item in possible_actions))
        random_value = self.session.random.random() * self.upper_boundary
        if random_value < max_probability:  #do something
            return self._choose_random_from_tuple(possible_actions)
        else:
            return self.actions.wait

    def hostile_player(self, **environment):
        """
		Calculate balance, and change diplomacy towards a player to neutral or ally.
		This has a very small chance though, since BehaviorEvil enjoys to be in a war.
		"""

        # Parameters are crucial in determining how AI should behave:
        # 'ally' and 'enemy' parameters are tuples of 1 or 2 values that set width or width and height of the parabola.
        # By default parabola peek is fixed at 1.0, but could be changed (by providing second parameter)
        # to manipulate the chance with which given actions is called
        # 'neutral' parameter is a tuple up to three values, first one determining where the center of the parabola is

        self.handle_diplomacy(self.parameters_hostile, **environment)

    def neutral_player(self, **environment):
        self.handle_diplomacy(self.parameters_neutral, **environment)

    def allied_player(self, **environment):
        self.handle_diplomacy(self.parameters_allied, **environment)
Пример #30
0
class FleetMission(Mission):

    missionStates = Enum('created', 'fleeing_home')

    # db_table name has to be overwritten by the concrete mission

    log = logging.getLogger("ai.aiplayer.fleetmission")

    def __init__(self, success_callback, failure_callback, ships):
        assert ships, "Attempt to create a fleet mission out of 0 ships"
        super(FleetMission, self).__init__(success_callback, failure_callback,
                                           ships[0].owner)
        self.__init()
        self._init_fleet(ships)

    def __init(self):
        self.unit_manager = self.owner.unit_manager
        self.session = self.owner.session
        self.strategy_manager = self.owner.strategy_manager
        self.combat_phase = False
        self.state = self.missionStates.created
        self._setup_state_callbacks()

    def _setup_state_callbacks(self):
        """This function can be overwritten to setup mission specific callbacks"""
        # combatIntermissions states which function should be called after combat phase was finished (winning or losing).
        # each combatIntermission entry should provide both, It is the CombatManager that decides which function to call
        # Dictionary of type: missionState => (won_function, lost_function)
        self.combatIntermissions = {}

        # _state_fleet_callbacks states which callback is supposed to be called by the fleet when it reaches the target point
        # based on given mission state. Used when changing mission (initiating new mission phase) states and loading game (restarting mission from given state)
        # Dictionary of type: missionState => Callback object
        self._state_fleet_callbacks = {}

    def _init_fleet(self, ships):
        self.fleet = self.unit_manager.create_fleet(
            ships=ships,
            destroy_callback=Callback(self.cancel, "All ships were destroyed"))
        for ship in self.fleet.get_ships():
            self.owner.ships[ship] = self.owner.shipStates.on_a_mission

    def save(self, db):
        super(FleetMission, self).save(db)
        db(
            "INSERT INTO ai_fleet_mission (rowid, owner_id, fleet_id, state_id, combat_phase) VALUES(?, ?, ?, ?, ?)",
            self.worldid, self.owner.worldid, self.fleet.worldid,
            self.state.index, self.combat_phase)

    @classmethod
    def load(cls, worldid, owner, db, success_callback, failure_callback):
        self = cls.__new__(cls)
        self._load(worldid, owner, db, success_callback, failure_callback)
        self._initialize_mission()
        return self

    def _load(self, db, worldid, success_callback, failure_callback, owner):
        super(FleetMission, self).load(db, worldid, success_callback,
                                       failure_callback, owner)
        self.__init()

        fleet_id, state_id, combat_phase = db(
            "SELECT fleet_id, state_id, combat_phase FROM ai_fleet_mission WHERE rowid = ?",
            worldid)[0]

        self.fleet = WorldObject.get_object_by_id(fleet_id)
        self.state = self.missionStates[state_id]
        self.combat_phase = bool(combat_phase)

    def _initialize_mission(self):
        """
		Initializes mission after loading is finished.
		"""

        # Add move callback for fleet, dependent on loaded fleet state
        if self.state in self._state_fleet_callbacks:
            self.fleet.callback = self._state_fleet_callbacks[self.state]

        # Add destroy callback, the same for every case of fleet being destroyed
        self.fleet.destroy_callback = Callback(self.cancel,
                                               "All ships were destroyed")

    def _dismiss_fleet(self):
        for ship in self.fleet.get_ships():
            self.owner.ships[ship] = self.owner.shipStates.idle
            ship.stop()
        self.unit_manager.destroy_fleet(self.fleet)

    def report_success(self, msg):
        self._dismiss_fleet()
        self.success_callback(self, msg)

    def report_failure(self, msg):
        self._dismiss_fleet()
        self.failure_callback(self, msg)

    def lost_ship(self):
        if self.fleet.size() == 0:
            self.cancel('Lost all of the ships')

    def pause_mission(self):
        self.log.debug("Player %s, Mission %s, pausing mission at state %s",
                       self.owner.name, self.__class__.__name__, self.state)
        self.combat_phase = True
        for ship in self.fleet.get_ships():
            ship.stop()

    # continue / abort methods are called by CombatManager after it handles combat.
    # CombatManager decides whether the battle was successful (and if the mission should be continued) or unsuccessful (mission should be aborted)
    def continue_mission(self):
        assert self.combat_phase, "request to continue mission without it being in combat_phase in the first place"
        assert self.state in self.combatIntermissions, "request to continue mission from not defined state: {}".format(
            self.state)
        self.log.debug("Player %s, Mission %s, continuing mission at state %s",
                       self.owner.name, self.__class__.__name__, self.state)
        self.combat_phase = False
        self.combatIntermissions[self.state][0]()

    def abort_mission(self, msg):
        assert self.combat_phase, "request to abort mission without it being in combat_phase in the first place"
        assert self.state in self.combatIntermissions, "request to abort mission from not defined state: {}".format(
            self.state)
        self.log.debug("Player %s, Mission %s, aborting mission at state %s",
                       self.owner.name, self.__class__.__name__, self.state)
        self.combat_phase = False
        self.combatIntermissions[self.state][1]()

    def cancel(self, msg):
        self.report_failure(msg)

    def __str__(self):
        return super(FleetMission, self).__str__() + \
        (' using {}'.format(self.fleet if hasattr(self, 'fleet') else 'unknown fleet')) + \
        ('(mission state:{},'.format(self.state if hasattr(self, 'state') else 'unknown state')) + \
        ('combat_phase:{})'.format(self.combat_phase if hasattr(self, 'combat_phase') else 'N/A'))