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)
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)
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()
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 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)
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 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()
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)
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 = []
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)