def __init__(self, cfgobj): self.cfg = cfgobj self.__started = False self.__created = False # Load Config self.__autostart = cfgobj.getboolean("Game", "auto_start") self.__allowafterstart = cfgobj.getboolean("Game", "allow_after_start") self.__allowreentry = cfgobj.getboolean("Server", "allow_re-entry") self.__resetworldround = self.cfg.getboolean("Tournament", "reset_world_each_round") self.__roundtime = cfgobj.getint("Tournament", "round_time") self._disconnect_on_death = cfgobj.getboolean("Game", "disconnect_on_death") self._reset_score_on_death = cfgobj.getboolean("Game", "reset_score_on_death") self._points_lost_on_death = cfgobj.getint("Game", "points_lost_on_death") self._points_initial = cfgobj.getint("Game", "points_initial") self._primary_victory = cfgobj.get("Game", "primary_victory_attr") self._primary_victory_high = cfgobj.getboolean( "Game", "primary_victory_highest") self._secondary_victory = cfgobj.get("Game", "secondary_victory_attr") self._secondary_victory_high = cfgobj.getboolean( "Game", "secondary_victory_highest") self._tournament = cfgobj.getboolean("Tournament", "tournament") self._tmanager = eval(cfgobj.get("Tournament", "manager"))( cfgobj, self.game_get_current_leader_list) if self._tournament: self.__autostart = False else: self.__roundtime = 0 # don't want to return a big round time set in a config, if it's not used. self.laststats = None self._highscore = 0 # highest score achieved during game self._leader = None # player object of who's in lead self.server = None # set when server created self.__teams = ThreadSafeDict() self._players = ThreadSafeDict() self.__aiships = ThreadSafeDict() self.__timer = None self.__leaderboard_cache = self.game_get_current_player_list() # Load World self.world = GameWorld(self, (cfgobj.getint( "World", "width"), cfgobj.getint("World", "height")), self.world_add_remove_object) self.spawnmanager = SpawnManager(cfgobj, self.world) if self.__autostart: self.round_start()
def ConfiguredWorld(game, cfg, pys=True, empty=False): """Generates a World from a Configuration Object. Args: game: Game object. cfg: ConfigParser object. pys: Physics flag. empty: if you want to populate the world with objects (used for testing) Returns: new World object. """ world = GameWorld(game, (cfg.getint("World","width"), cfg.getint("World","height")), pys) if not empty: av_sizes = eval(cfg.get("Nebula", "sizes")) for neb in xrange(cfg.getint("Nebula", "number")): world.append(Nebula(getPositionAwayFromOtherObjects(world, 30, 30), random.choice(av_sizes), cfg_rand_min_max(cfg, "Nebula", "pull"))) #next ast for p in xrange(cfg.getint("Planet", "number")): world.append(Planet(getPositionAwayFromOtherObjects(world, 200, 100), cfg_rand_min_max(cfg, "Planet", "range"), cfg_rand_min_max(cfg, "Planet", "pull"))) #next p for bh in xrange(cfg.getint("BlackHole", "number")): world.append(BlackHole(getPositionAwayFromOtherObjects(world, 250, 100), cfg_rand_min_max(cfg, "BlackHole", "range"), cfg_rand_min_max(cfg, "BlackHole", "pull"))) #next bh for ast in xrange(cfg.getint("Asteroid", "number")): world.append(Asteroid(getPositionAwayFromOtherObjects(world, 100, 30))) #next ast #eif return world
def SimpleWorld(game, size, numplanets=1, numblackholes=0, numasteroids=2): """Returns a simple world object with the number of objects specified. Args: game: Game object. size: (width, height) tuple. numplanets: integer Number of Planets. numblackholes: integer Number of BlackHoles. numasteroids: integer Number of Asteroids. Returns: new World object. """ world = GameWorld(game, size) for p in xrange(numplanets): world.append(Planet(getPositionAwayFromOtherObjects(world, 200, 100))) #next p for bh in xrange(numblackholes): world.append(BlackHole(getPositionAwayFromOtherObjects(world, 250, 100))) #next bh for ast in xrange(numasteroids): world.append(Asteroid(getPositionAwayFromOtherObjects(world, 100, 30))) #next ast #world.append(Planet((100, 100))) return world
def __init__(self, cfgobj): self.cfg = cfgobj self.__started = False self.__created = False # Load Config self.__autostart = cfgobj.getboolean("Game", "auto_start") self.__allowafterstart = cfgobj.getboolean("Game", "allow_after_start") self.__allowreentry = cfgobj.getboolean("Server", "allow_re-entry") self.__resetworldround = self.cfg.getboolean("Tournament", "reset_world_each_round") self.__roundtime = cfgobj.getint("Tournament", "round_time") self._disconnect_on_death = cfgobj.getboolean("Game", "disconnect_on_death") self._reset_score_on_death = cfgobj.getboolean("Game", "reset_score_on_death") self._points_lost_on_death = cfgobj.getint("Game", "points_lost_on_death") self._points_initial = cfgobj.getint("Game", "points_initial") self._primary_victory = cfgobj.get("Game", "primary_victory_attr") self._primary_victory_high = cfgobj.getboolean("Game", "primary_victory_highest") self._secondary_victory = cfgobj.get("Game", "secondary_victory_attr") self._secondary_victory_high = cfgobj.getboolean("Game", "secondary_victory_highest") self._tournament = cfgobj.getboolean("Tournament", "tournament") self._tmanager = eval(cfgobj.get("Tournament", "manager"))(cfgobj, self.game_get_current_leader_list) if self._tournament: self.__autostart = False else: self.__roundtime = 0 # don't want to return a big round time set in a config, if it's not used. self.laststats = None self._highscore = 0 # highest score achieved during game self._leader = None # player object of who's in lead self.server = None # set when server created self.__teams = ThreadSafeDict() self._players = ThreadSafeDict() self.__aiships = ThreadSafeDict() self.__timer = None self.__leaderboard_cache = self.game_get_current_player_list() # Load World self.world = GameWorld(self, (cfgobj.getint("World","width"), cfgobj.getint("World","height")), self.world_add_remove_object) self.spawnmanager = SpawnManager(cfgobj, self.world) if self.__autostart: self.round_start()
class BasicGame(object): """ The BasicGame defines all notions of the basic mechanics of Space Battle as well as an infrastructure to build upon. The BasicGame provides information about a player's 'score', 'bestscore', 'deaths' by default. It also keeps track of the 'highscore' in a variety of manners and can run a tournament to advance players to a final round. Public Attributes: world: world object server: server object laststats: previous rounds scores Protected Attributes: _players: dictionary of players keyed by their network id (player.netid) _tournament: boolean True if playing tournament _tournamentinitialized: boolean True if tournament has been setup _highscore: current highscore of the game based on primary_victory_attr _leader: current leader of the game """ _ADD_REASON_REGISTER_ = 0 _ADD_REASON_START_ = 1 _ADD_REASON_RESPAWN_ = 2 def __init__(self, cfgobj): self.cfg = cfgobj self.__started = False self.__created = False # Load Config self.__autostart = cfgobj.getboolean("Game", "auto_start") self.__allowafterstart = cfgobj.getboolean("Game", "allow_after_start") self.__allowreentry = cfgobj.getboolean("Server", "allow_re-entry") self.__resetworldround = self.cfg.getboolean("Tournament", "reset_world_each_round") self.__roundtime = cfgobj.getint("Tournament", "round_time") self._disconnect_on_death = cfgobj.getboolean("Game", "disconnect_on_death") self._reset_score_on_death = cfgobj.getboolean("Game", "reset_score_on_death") self._points_lost_on_death = cfgobj.getint("Game", "points_lost_on_death") self._points_initial = cfgobj.getint("Game", "points_initial") self._primary_victory = cfgobj.get("Game", "primary_victory_attr") self._primary_victory_high = cfgobj.getboolean("Game", "primary_victory_highest") self._secondary_victory = cfgobj.get("Game", "secondary_victory_attr") self._secondary_victory_high = cfgobj.getboolean("Game", "secondary_victory_highest") self._tournament = cfgobj.getboolean("Tournament", "tournament") self._tmanager = eval(cfgobj.get("Tournament", "manager"))(cfgobj, self.game_get_current_leader_list) if self._tournament: self.__autostart = False else: self.__roundtime = 0 # don't want to return a big round time set in a config, if it's not used. self.laststats = None self._highscore = 0 # highest score achieved during game self._leader = None # player object of who's in lead self.server = None # set when server created self.__teams = ThreadSafeDict() self._players = ThreadSafeDict() self.__aiships = ThreadSafeDict() self.__timer = None self.__leaderboard_cache = self.game_get_current_player_list() # Load World self.world = GameWorld(self, (cfgobj.getint("World","width"), cfgobj.getint("World","height")), self.world_add_remove_object) self.spawnmanager = SpawnManager(cfgobj, self.world) if self.__autostart: self.round_start() def end(self): """ Called when exiting GUI to terminate game. """ logging.info("Ending Game") self._tournament = True # force it to not restart timers again self.round_over() logging.info("Ending World") self.world.endGameLoop() #region Round/Timing Functions def _round_start_timer(self): """ Called automatically by round_start. """ if self.__roundtime > 0 and self._tournament: self.__timer = CallbackTimer(self.__roundtime, self.round_over) self.__timer.start() def round_get_time_remaining(self): """ Returns the amount of time remaining in a tournament round or zero """ if self.__timer == None: return 0 return self.__timer.time_left def round_start(self): """ Called by GUI client to start a synchronized game start, or automatically when the game is initialized if auto_start = true Called every time a new round starts or once if a tournament is not being played. """ if not self.__started and not self.world.gameerror: logging.info("Starting Game") self.__started = True self.__autostart = self.__allowafterstart if self._tournament: if not self._tmanager.is_initialized(): self._tmanager.initialize(self._players.values()) else: self._tmanager.next_round() if self._tmanager.is_final_round(): logging.info("[Tournament] Final Round") #eif #eif # we'll reset the world here so it's "fresh" and ready as soon as the game starts if self.__resetworldround or not self.__created: logging.info("Resetting World.") self.world_create() #eif if not self.__created and not self.world.gameerror: self.world.start() self.__created = True self.spawnmanager.start() # want spawn manager here so it can spawn items on players for player in self.game_get_current_player_list(): self._game_add_ship_for_player(player.netid, roundstart=True) #next self._round_start_timer() def player_added(self, player, reason): """ player_added is called in three separate cases 0) When the server first registers a ship on the server and adds them to the game 1) When a round starts and a player is added to the game round 2) When a player dies and is respawned in the world. cases 0 and 1 may occur one after another if auto_start is set to true. The ship object will have been created, but not added to the physics world yet, so no callback to world_add_remove_object may have occured yet. This is also before any commands are requested for the player (i.e. before the first environment is sent). You should add and initialize any new properties that you want on a player here. Reason values: _ADD_REASON_REGISTER_ = 0 _ADD_REASON_START_ = 1 _ADD_REASON_RESPAWN_ = 2 Analog to player_died """ logging.info("Player %s Added(%d)", player.name, reason) if reason == 1: player.score = self._points_initial player.bestscore = self._points_initial player.deaths = 0 self.spawnmanager.player_added(reason) def player_get_start_position(self): """ Should return a position for a player to start at in the world. """ pos = (random.randint(100, self.world.width - 100), random.randint(100, self.world.height - 100)) x = 0 while len(self.world.getObjectsInArea(pos, 150)) > 0 and x < 15: x += 1 pos = (random.randint(100, self.world.width - 100), random.randint(100, self.world.height - 100)) return pos def round_over(self): """ Called in a tournament when the round time expires. Will automatically advance the top players to the next round. """ if self.__timer != None: self.__timer.cancel() # if we get a round-over prematurely or do to some other condition, we should cancel this current timer self.__timer = None self.spawnmanager.stop() logging.debug("[Game] Round Over") self.game_update(0) # make sure we do one last update to sync and refresh the leaderboard cache # Get Stats for only the players in the round self.laststats = self.gui_get_player_stats() logging.info("[Game] Stats: %s", repr(self.laststats)) if len(self.laststats) > 0: self._tmanager.check_results(self.__leaderboard_cache, self.laststats) #eif for player in self._players: player.roundover = True if player.object != None: player.object.destroyed = True self.world.remove(player.object) # here we're forcibly removing them as we're clearing the game if self.__resetworldround: logging.info("Destroying World") self.world.destroy_all() # overwritten by allowafterstart self.__autostart = self.cfg.getboolean("Game", "auto_start") # TODO: figure out a better way to tie these together if self._tournament: self.__autostart = False # If not autostart, wait for next round to start self.__started = False if self.__autostart: self.round_start() def world_create(self): """ Called by game at start of round to create world and when world reset, defaults to the standard world configuration from the config file definitions. """ self.spawnmanager.spawn_initial() def round_get_has_started(self): """ Returns True when game has started. """ return self.__started def game_update(self, t): """ Called by the World to notify the passage of time The default game uses this update to cache the current leaderboard to be used by the GUI and the game to determine the leader info to send out. Note: This is called from the core game loop, best not to delay much here """ self.__leaderboard_cache = sorted(sorted(self.game_get_current_player_list(), key=attrgetter(self._secondary_victory), reverse=self._secondary_victory_high), key=attrgetter(self._primary_victory), reverse=self._primary_victory_high) if len(self.__leaderboard_cache) > 0: self._leader = self.__leaderboard_cache[0] self._highscore = getattr(self._leader, self._primary_victory) #eif #endregion #region Network driven functions def server_register_player(self, name, color, imgindex, netid, aiship=None): """ Called by the server when a new client registration message is received Has all information pertaining to a player entity Parameters: ship: Ship object - used to register AI Ships to the game world """ if not self._players.has_key(netid): if (self.__started and self.__allowafterstart) or not self.__started: # create new player # p = Player(name, color, imgindex, netid, self._primary_victory_high) self.player_added(p, BasicGame._ADD_REASON_REGISTER_) self._players[netid] = p if aiship != None: self.__aiships[netid] = aiship if self.__autostart: self._game_add_ship_for_player(netid, roundstart=True) logging.info("Registering Player: %s %d", name, netid) return True return False def server_process_network_message(self, ship, cmdname, cmddict={}): """ Called by the server when it doesn't know how to convert a network message into a Ship Command. This function is used to create commands custom to the game itself. Parameters: ship: Ship object which will process the Command cmdname: string network shortcode for command, set overriding getName in ShipCommand on Client cmddict: dictionary of properties of a command, set using private fields on ShipCommand class Return: return a server 'Command' object, a string indicating an error, or None """ pass def server_process_command(self, ship, command): """ Called by the server after a Command object has been formed from a network message. This includes the command processed by a game in server_process_network_message. This function would allow the game to interact with the built-in commands. Parameters: ship: Ship object to process this command command: Command object that will be added to the ship's computer by the server Return: This method should return the command or a string indicating an error. Returning None would cause a silent failure on the client. In the case of a string, the message would be printed out in the client console. In both error cases, another standard request for a command is made on the client. """ return command def _game_add_ship_for_player(self, netid, roundstart=False): """ Called internally when a player registers if autostart is true, or when round_start is called Also called when a player respawns. Look to override player_added instead. Also, sends initial environment to start RPC loop with client """ if netid < -2: # AI Ship self._players[netid].object = self.__aiships[netid] # reset AI Ship to look like new ship, though reuse object # TODO: this doesn't work, need 'new' object to add to world, or queue the add to occur after the remove...boo self.__aiships[netid].health.full() self.__aiships[netid].energy.full() self.__aiships[netid].shield.full() self.__aiships[netid].destroyed = False self.__aiships[netid].killed = False self.__aiships[netid].timealive = 0 self.__aiships[netid].body.velocity = Vec2d(0, 0) if not roundstart: # ai ship will be initialized with a starting position for round entry, but if killed, will randomize self.__aiships[netid].body.position = self.player_get_start_position() self._players[netid].object.ship_added() # tell AI ship to start else: self._players[netid].object = Ship(self.player_get_start_position(), self.world) logging.info("Adding Ship for Player %d id #%d with Name %s", netid, self._players[netid].object.id, self._players[netid].name) self._players[netid].object.player = self._players[netid] self._players[netid].object.owner = self._players[netid].object # Make ships owners of themselves for easier logic with ownership? self._players[netid].roundover = False if not self.cfg.getboolean("World", "collisions"): self._players[netid].object.shape.group = 1 if roundstart: self.player_added(self._players[netid], BasicGame._ADD_REASON_START_) else: self.player_added(self._players[netid], BasicGame._ADD_REASON_RESPAWN_) #eif self.world.append(self._players[netid].object) logging.info("Sending New Ship #%d Environment Info", self._players[netid].object.id) if netid >= 0 and not self._players[netid].waiting: self.server.sendEnvironment(self._players[netid].object, netid) return self._players[netid].object def game_get_info(self): """ Called when a client is connected and appends this data to the default image number, world width and world height Should at least return a key "GAMENAME" with the name of the game module. Your game name should be "Basic" if you don't want to return anything in game_get_extra_environment to the player besides Score, Bestscore, Deaths, Highscore, Time left, and Round time. Note: The client doesn't expose a way to get anything besides the game name right now. """ return {"GAMENAME": "Basic"} def game_get_extra_environment(self, player): """ Called by World to return extra environment info to a player when they need to return a command (for just that player). If you desire to return more information to the client, you can create an extended BasicGameInfo class there and repackage the jar. See info in the server docs on adding a subgame. """ return {"SCORE": player.score, "BESTSCORE": player.bestscore, "DEATHS": player.deaths, "HIGHSCORE": self._highscore, "TIMELEFT": int(self.round_get_time_remaining()), "ROUNDTIME": self.__roundtime, "LSTDSTRBY": player.lastkilledby} def game_get_extra_radar_info(self, obj, objdata, player): """ Called by the World when the obj is being radared, should add new properties to the objdata. Note: it is not supported to pick up these values directly by the client, it is best to use the existing properties that make sense. """ if hasattr(obj, "player") and self.cfg.getboolean("World", "radar_include_name"): objdata["NAME"] = obj.player.name def server_disconnect_player(self, netid): """ Called by the server when a disconnect message is received by a player's client player_died should also be called, so it is recommended to override that method instead. """ for player in self._players: if player.netid == netid and player.object != None: logging.debug("Player %s Disconnected", player.name) player.disconnected = True player.object.killed = True # terminate self.world.remove(player.object) player.object = None if self.__allowreentry: if self._players.has_key(player.netid): del self._players[player.netid] # should be handled by world part return True logging.debug("Couldn't find player with netid %d to disconnect", netid); return False def player_get_by_name(self, name): """ Retrieves a player by their name. """ for player in self._players.values(): if player.name == name: return player return None def player_get_by_network_id(self, netid): """ Used by the server to retrieve a player by their network id. """ if self._players.has_key(netid): return self._players[netid] return None #endregion #region Player Scoring / Leaderboard Functions def player_died(self, player, gone): """ Will be called when a player dies, will adjust score appropriately based on game rules. Called before player object removed from player. gone will be True if the player is disconnected or killed (they won't respawn) Analog to player_added """ if not player.roundover: # only count deaths during the round! logging.info("Player %s Died", player.name) player.deaths += 1 if self._points_lost_on_death > 0: player.update_score(-self._points_lost_on_death) if self._primary_victory == "bestscore": # we need to subtract/add to bestscore in stead if self._primary_victory_high: player.bestscore -= self._points_lost_on_death if player.bestscore < 0: player.bestscore = 0 else: player.bestscore += self._points_lost_on_death if player.bestscore > self._points_initial: player.bestscore = self._points_initial if self._reset_score_on_death: self._player_reset_score(player) def _player_reset_score(self, player): """ Used to reset a players score and determine the new leader. Will be called by the default implementation of player_died if the reset_score_on_death configuration property is true. """ player.score = self._points_initial def game_get_current_leader_list(self, all=False): """ Gets the list of players sorted by their score (highest first) (or all players) """ # secondary victory first, primary second if all: return sorted(sorted(self.game_get_current_player_list(all), key=attrgetter(self._secondary_victory), reverse=self._secondary_victory_high), key=attrgetter(self._primary_victory), reverse=self._primary_victory_high) # TODO: Cache this and regen, update leader and highscore value there too, should I do this once every game update? return self.__leaderboard_cache def game_get_current_player_list(self, all=False): """ Returns a list of player objects for players in the current round Returns all players if no tournament running or requested """ if all or not self._tournament: return self._players else: return self._tmanager.get_players_in_round() #eif def player_get_stat_string(self, player): """ Should return a string with information about the player and their current score. Defaults to: primary_score secondary_score : player_name """ return "%.1f" % getattr(player, self._primary_victory) + " " + str(getattr(player, self._secondary_victory)) + " : " + player.name def tournament_is_running(self): """ Returns true if a tournament is running. """ return self._tournament #endregion #region World/Collision Functions def world_add_remove_object(self, wobj, added): """ Called by world when an object is added or destroyed (before added (guaranteed to not have update) and after removed (though may receive last update)) For simple tasks involving players look to the player_died or player_added methods killed ships will not return (used to prevent respawn) """ logging.debug("[Game] Add Object(%s): #%d (%s)", repr(added), wobj.id, friendly_type(wobj)) if not added and isinstance(wobj, SpaceMine) and wobj.active: self.world.causeExplosion(wobj.body.position, SpaceMine.RADIUS, SpaceMine.FORCE, True) # TODO: Cause splash damage? if not added and isinstance(wobj, Ship) and wobj.player.netid in self._players: nid = wobj.player.netid # if we were given an expiration time, means we haven't issued a command, so kill the ship if wobj.has_expired() and self.cfg.getboolean("Server", "disconnect_on_idle"): logging.info("Ship #%d killed due to timeout.", wobj.id) wobj.killed = True if hasattr(wobj, "killedby") and wobj.killedby != None: if isinstance(wobj.killedby, Ship): self._players[nid].lastkilledby = wobj.killedby.player.name else: self._players[nid].lastkilledby = friendly_type(wobj.killedby) + " #" + str(wobj.killedby.id) self.player_died(self._players[nid], (self._players[nid].disconnected or wobj.killed)) self._players[nid].object = None if not self._players[nid].disconnected: if self._disconnect_on_death or wobj.killed: if self.__allowreentry: del self._players[nid] # TODO: disconnect AI? if nid >= 0: self.server.sendDisconnect(nid) else: if not self._players[nid].roundover: # if the round isn't over, then re-add the ship self._game_add_ship_for_player(nid) if not added: self.spawnmanager.check_number(wobj) def world_physics_pre_collision(self, obj1, obj2): """ Called by the physics engine when two objects just touch for the first time return [True/False, [func, obj, para...]... ] use False to not process collision in the physics engine, the function callback will still be called return a list with lists of function callback requests for the function, and object, and extra parameters The default game prevents anything from colliding with (BlackHole, Nebula, or Star) collide returns False. """ logging.debug("Object #%d colliding with #%d", obj1.id, obj2.id) return obj1.collide_start(obj2) and obj2.collide_start(obj1) def world_physics_collision(self, obj1, obj2, damage): """ Called by the physics engine when two objects collide Return [[func, obj, parameter]...] The default game handles inflicting damage on entities in this step. It is best to override world_physics_pre_collision if you want to prevent things from occuring in the first place. """ logging.debug("Object #%d collided with #%d for %f damage", obj1.id, obj2.id, damage) r = [] obj1.take_damage(damage, obj2) obj2.take_damage(damage, obj1) for gobj in (obj1, obj2): # check both objects for callback if gobj.health.maximum > 0 and gobj.health.value <= 0: logging.info("Object #%d destroyed by %s", gobj.id, repr(gobj.killedby)) r.append([self.world_physics_post_collision, gobj, damage]) #eif if r == []: return None return r def world_physics_post_collision(self, dobj, damage): """ Setup and called by world_physics_collision to process objects which have been destroyed as a result of taking too much damage. The default game causes an explosion force based on the strength of the collision in the vicinity of the collision. dobj: the object destroyed para: extra parameters from a previous step, by default collision passes the strength of the collision only """ strength = damage logging.info("Destroying Object: #%d, Force: %d [%d]", dobj.id, strength, thread.get_ident()) dobj.destroyed = True # get rid of object self.world.causeExplosion(dobj.body.position, dobj.radius * 5, strength, True) #Force in physics step def world_physics_end_collision(self, obj1, obj2): """ Called by the physics engine after two objects stop overlapping/colliding. This is still called even if the pre_collision returned 'False' and no actual collision was processed """ logging.debug("Object #%d no longer colliding with #%d", obj1.id, obj2.id) # notify each object of the finalization of the collision obj1.collide_end(obj2) obj2.collide_end(obj1) #endregion #region GUI Drawing def gui_initialize(self): """ Used to initialize GUI resources at the appropriate time after the graphics engine has been initialized. """ self._tmanager.gui_initialize() self._dfont = debugfont() def gui_draw_game_world_info(self, surface, flags, trackplayer): """ Called by GUI to have the game draw additional (optional) info in the world when toggled on 'G'. (coordinates related to the game world) """ pass def gui_draw_game_screen_info(self, screen, flags, trackplayer): """ Called by GUI to have the game draw additional (optional) info on screen when toggled on 'G'. (coordinates related to the screen) """ pass def gui_get_player_stats(self, all=False): """ Called by GUI to get the sorted list of player stats. GUI expects a list of strings, you should usually override player_get_stat_string. """ sstat = [] for player in self.game_get_current_leader_list(all): sstat.append(self.player_get_stat_string(player)) return sstat def gui_draw_tournament_bracket(self, screen, flags, trackplayer): """ Called by GUI to draw info about the round/tournament (optional) when toggled on 'T'. (coordinates related to the screen) """ if self._tournament and self._tmanager.is_initialized(): self._tmanager.gui_draw_tournament_bracket(screen, flags, trackplayer)
class BasicGame(object): """ The BasicGame defines all notions of the basic mechanics of Space Battle as well as an infrastructure to build upon. The BasicGame provides information about a player's 'score', 'bestscore', 'deaths' by default. It also keeps track of the 'highscore' in a variety of manners and can run a tournament to advance players to a final round. Public Attributes: world: world object server: server object laststats: previous rounds scores Protected Attributes: _players: dictionary of players keyed by their network id (player.netid) _tournament: boolean True if playing tournament _tournamentinitialized: boolean True if tournament has been setup _highscore: current highscore of the game based on primary_victory_attr _leader: current leader of the game """ _ADD_REASON_REGISTER_ = 0 _ADD_REASON_START_ = 1 _ADD_REASON_RESPAWN_ = 2 def __init__(self, cfgobj): self.cfg = cfgobj self.__started = False self.__created = False # Load Config self.__autostart = cfgobj.getboolean("Game", "auto_start") self.__allowafterstart = cfgobj.getboolean("Game", "allow_after_start") self.__allowreentry = cfgobj.getboolean("Server", "allow_re-entry") self.__resetworldround = self.cfg.getboolean("Tournament", "reset_world_each_round") self.__roundtime = cfgobj.getint("Tournament", "round_time") self._disconnect_on_death = cfgobj.getboolean("Game", "disconnect_on_death") self._reset_score_on_death = cfgobj.getboolean("Game", "reset_score_on_death") self._points_lost_on_death = cfgobj.getint("Game", "points_lost_on_death") self._points_initial = cfgobj.getint("Game", "points_initial") self._primary_victory = cfgobj.get("Game", "primary_victory_attr") self._primary_victory_high = cfgobj.getboolean( "Game", "primary_victory_highest") self._secondary_victory = cfgobj.get("Game", "secondary_victory_attr") self._secondary_victory_high = cfgobj.getboolean( "Game", "secondary_victory_highest") self._tournament = cfgobj.getboolean("Tournament", "tournament") self._tmanager = eval(cfgobj.get("Tournament", "manager"))( cfgobj, self.game_get_current_leader_list) if self._tournament: self.__autostart = False else: self.__roundtime = 0 # don't want to return a big round time set in a config, if it's not used. self.laststats = None self._highscore = 0 # highest score achieved during game self._leader = None # player object of who's in lead self.server = None # set when server created self.__teams = ThreadSafeDict() self._players = ThreadSafeDict() self.__aiships = ThreadSafeDict() self.__timer = None self.__leaderboard_cache = self.game_get_current_player_list() # Load World self.world = GameWorld(self, (cfgobj.getint( "World", "width"), cfgobj.getint("World", "height")), self.world_add_remove_object) self.spawnmanager = SpawnManager(cfgobj, self.world) if self.__autostart: self.round_start() def end(self): """ Called when exiting GUI to terminate game. """ logging.info("Ending Game") self._tournament = True # force it to not restart timers again self.round_over() logging.info("Ending World") self.world.endGameLoop() #region Round/Timing Functions def _round_start_timer(self): """ Called automatically by round_start. """ if self.__roundtime > 0 and self._tournament: self.__timer = CallbackTimer(self.__roundtime, self.round_over) self.__timer.start() def round_get_time_remaining(self): """ Returns the amount of time remaining in a tournament round or zero """ if self.__timer == None: return 0 return self.__timer.time_left def round_start(self): """ Called by GUI client to start a synchronized game start, or automatically when the game is initialized if auto_start = true Called every time a new round starts or once if a tournament is not being played. """ if not self.__started and not self.world.gameerror: logging.info("Starting Game") self.__started = True self.__autostart = self.__allowafterstart if self._tournament: if not self._tmanager.is_initialized(): self._tmanager.initialize(self._players.values()) else: self._tmanager.next_round() if self._tmanager.is_final_round(): logging.info("[Tournament] Final Round") #eif #eif # we'll reset the world here so it's "fresh" and ready as soon as the game starts if self.__resetworldround or not self.__created: logging.info("Resetting World.") self.world_create() #eif if not self.__created and not self.world.gameerror: self.world.start() self.__created = True self.spawnmanager.start( ) # want spawn manager here so it can spawn items on players for player in self.game_get_current_player_list(): self._game_add_ship_for_player(player.netid, roundstart=True) #next self._round_start_timer() def player_added(self, player, reason): """ player_added is called in three separate cases 0) When the server first registers a ship on the server and adds them to the game 1) When a round starts and a player is added to the game round 2) When a player dies and is respawned in the world. cases 0 and 1 may occur one after another if auto_start is set to true. The ship object will have been created, but not added to the physics world yet, so no callback to world_add_remove_object may have occured yet. This is also before any commands are requested for the player (i.e. before the first environment is sent). You should add and initialize any new properties that you want on a player here. Reason values: _ADD_REASON_REGISTER_ = 0 _ADD_REASON_START_ = 1 _ADD_REASON_RESPAWN_ = 2 Analog to player_died """ logging.info("Player %s Added(%d)", player.name, reason) if reason == 1: player.score = self._points_initial player.bestscore = self._points_initial player.deaths = 0 self.spawnmanager.player_added(reason) def player_get_start_position(self): """ Should return a position for a player to start at in the world. """ pos = (random.randint(100, self.world.width - 100), random.randint(100, self.world.height - 100)) x = 0 while len(self.world.getObjectsInArea(pos, 150)) > 0 and x < 15: x += 1 pos = (random.randint(100, self.world.width - 100), random.randint(100, self.world.height - 100)) return pos def round_over(self): """ Called in a tournament when the round time expires. Will automatically advance the top players to the next round. """ if self.__timer != None: self.__timer.cancel( ) # if we get a round-over prematurely or do to some other condition, we should cancel this current timer self.__timer = None self.spawnmanager.stop() logging.debug("[Game] Round Over") self.game_update( 0 ) # make sure we do one last update to sync and refresh the leaderboard cache # Get Stats for only the players in the round self.laststats = self.gui_get_player_stats() logging.info("[Game] Stats: %s", repr(self.laststats)) if len(self.laststats) > 0: self._tmanager.check_results(self.__leaderboard_cache, self.laststats) #eif for player in self._players: player.roundover = True if player.object != None: player.object.destroyed = True self.world.remove( player.object ) # here we're forcibly removing them as we're clearing the game if self.__resetworldround: logging.info("Destroying World") self.world.destroy_all() # overwritten by allowafterstart self.__autostart = self.cfg.getboolean("Game", "auto_start") # TODO: figure out a better way to tie these together if self._tournament: self.__autostart = False # If not autostart, wait for next round to start self.__started = False if self.__autostart: self.round_start() def world_create(self): """ Called by game at start of round to create world and when world reset, defaults to the standard world configuration from the config file definitions. """ self.spawnmanager.spawn_initial() def round_get_has_started(self): """ Returns True when game has started. """ return self.__started def game_update(self, t): """ Called by the World to notify the passage of time The default game uses this update to cache the current leaderboard to be used by the GUI and the game to determine the leader info to send out. Note: This is called from the core game loop, best not to delay much here """ self.__leaderboard_cache = sorted( sorted(self.game_get_current_player_list(), key=attrgetter(self._secondary_victory), reverse=self._secondary_victory_high), key=attrgetter(self._primary_victory), reverse=self._primary_victory_high) if len(self.__leaderboard_cache) > 0: self._leader = self.__leaderboard_cache[0] self._highscore = getattr(self._leader, self._primary_victory) #eif #endregion #region Network driven functions def server_register_player(self, name, color, imgindex, netid, aiship=None): """ Called by the server when a new client registration message is received Has all information pertaining to a player entity Parameters: ship: Ship object - used to register AI Ships to the game world """ if not self._players.has_key(netid): if (self.__started and self.__allowafterstart) or not self.__started: # create new player # p = Player(name, color, imgindex, netid, self._primary_victory_high) self.player_added(p, BasicGame._ADD_REASON_REGISTER_) self._players[netid] = p if aiship != None: self.__aiships[netid] = aiship if self.__autostart: self._game_add_ship_for_player(netid, roundstart=True) logging.info("Registering Player: %s %d", name, netid) return True return False def server_process_network_message(self, ship, cmdname, cmddict={}): """ Called by the server when it doesn't know how to convert a network message into a Ship Command. This function is used to create commands custom to the game itself. Parameters: ship: Ship object which will process the Command cmdname: string network shortcode for command, set overriding getName in ShipCommand on Client cmddict: dictionary of properties of a command, set using private fields on ShipCommand class Return: return a server 'Command' object, a string indicating an error, or None """ pass def server_process_command(self, ship, command): """ Called by the server after a Command object has been formed from a network message. This includes the command processed by a game in server_process_network_message. This function would allow the game to interact with the built-in commands. Parameters: ship: Ship object to process this command command: Command object that will be added to the ship's computer by the server Return: This method should return the command or a string indicating an error. Returning None would cause a silent failure on the client. In the case of a string, the message would be printed out in the client console. In both error cases, another standard request for a command is made on the client. """ return command def _game_add_ship_for_player(self, netid, roundstart=False): """ Called internally when a player registers if autostart is true, or when round_start is called Also called when a player respawns. Look to override player_added instead. Also, sends initial environment to start RPC loop with client """ if netid < -2: # AI Ship self._players[netid].object = self.__aiships[netid] # reset AI Ship to look like new ship, though reuse object # TODO: this doesn't work, need 'new' object to add to world, or queue the add to occur after the remove...boo self.__aiships[netid].health.full() self.__aiships[netid].energy.full() self.__aiships[netid].shield.full() self.__aiships[netid].destroyed = False self.__aiships[netid].killed = False self.__aiships[netid].timealive = 0 self.__aiships[netid].body.velocity = Vec2d(0, 0) if not roundstart: # ai ship will be initialized with a starting position for round entry, but if killed, will randomize self.__aiships[ netid].body.position = self.player_get_start_position() self._players[netid].object.ship_added() # tell AI ship to start else: self._players[netid].object = Ship( self.player_get_start_position(), self.world) logging.info("Adding Ship for Player %d id #%d with Name %s", netid, self._players[netid].object.id, self._players[netid].name) self._players[netid].object.player = self._players[netid] self._players[netid].object.owner = self._players[ netid].object # Make ships owners of themselves for easier logic with ownership? self._players[netid].roundover = False if not self.cfg.getboolean("World", "collisions"): self._players[netid].object.shape.group = 1 if roundstart: self.player_added(self._players[netid], BasicGame._ADD_REASON_START_) else: self.player_added(self._players[netid], BasicGame._ADD_REASON_RESPAWN_) #eif self.world.append(self._players[netid].object) logging.info("Sending New Ship #%d Environment Info", self._players[netid].object.id) if netid >= 0 and not self._players[netid].waiting: self.server.sendEnvironment(self._players[netid].object, netid) return self._players[netid].object def game_get_info(self): """ Called when a client is connected and appends this data to the default image number, world width and world height Should at least return a key "GAMENAME" with the name of the game module. Your game name should be "Basic" if you don't want to return anything in game_get_extra_environment to the player besides Score, Bestscore, Deaths, Highscore, Time left, and Round time. Note: The client doesn't expose a way to get anything besides the game name right now. """ return {"GAMENAME": "Basic"} def game_get_extra_environment(self, player): """ Called by World to return extra environment info to a player when they need to return a command (for just that player). If you desire to return more information to the client, you can create an extended BasicGameInfo class there and repackage the jar. See info in the server docs on adding a subgame. """ return { "SCORE": player.score, "BESTSCORE": player.bestscore, "DEATHS": player.deaths, "HIGHSCORE": self._highscore, "TIMELEFT": int(self.round_get_time_remaining()), "ROUNDTIME": self.__roundtime, "LSTDSTRBY": player.lastkilledby } def game_get_extra_radar_info(self, obj, objdata, player): """ Called by the World when the obj is being radared, should add new properties to the objdata. Note: it is not supported to pick up these values directly by the client, it is best to use the existing properties that make sense. """ if hasattr(obj, "player") and self.cfg.getboolean( "World", "radar_include_name"): objdata["NAME"] = obj.player.name def server_disconnect_player(self, netid): """ Called by the server when a disconnect message is received by a player's client player_died should also be called, so it is recommended to override that method instead. """ for player in self._players: if player.netid == netid and player.object != None: logging.debug("Player %s Disconnected", player.name) player.disconnected = True player.object.killed = True # terminate self.world.remove(player.object) player.object = None if self.__allowreentry: if self._players.has_key(player.netid): del self._players[ player.netid] # should be handled by world part return True logging.debug("Couldn't find player with netid %d to disconnect", netid) return False def player_get_by_name(self, name): """ Retrieves a player by their name. """ for player in self._players.values(): if player.name == name: return player return None def player_get_by_network_id(self, netid): """ Used by the server to retrieve a player by their network id. """ if self._players.has_key(netid): return self._players[netid] return None #endregion #region Player Scoring / Leaderboard Functions def player_died(self, player, gone): """ Will be called when a player dies, will adjust score appropriately based on game rules. Called before player object removed from player. gone will be True if the player is disconnected or killed (they won't respawn) Analog to player_added """ if not player.roundover: # only count deaths during the round! logging.info("Player %s Died", player.name) player.deaths += 1 if self._points_lost_on_death > 0: player.update_score(-self._points_lost_on_death) if self._primary_victory == "bestscore": # we need to subtract/add to bestscore in stead if self._primary_victory_high: player.bestscore -= self._points_lost_on_death if player.bestscore < 0: player.bestscore = 0 else: player.bestscore += self._points_lost_on_death if player.bestscore > self._points_initial: player.bestscore = self._points_initial if self._reset_score_on_death: self._player_reset_score(player) def _player_reset_score(self, player): """ Used to reset a players score and determine the new leader. Will be called by the default implementation of player_died if the reset_score_on_death configuration property is true. """ player.score = self._points_initial def game_get_current_leader_list(self, all=False): """ Gets the list of players sorted by their score (highest first) (or all players) """ # secondary victory first, primary second if all: return sorted(sorted(self.game_get_current_player_list(all), key=attrgetter(self._secondary_victory), reverse=self._secondary_victory_high), key=attrgetter(self._primary_victory), reverse=self._primary_victory_high) # TODO: Cache this and regen, update leader and highscore value there too, should I do this once every game update? return self.__leaderboard_cache def game_get_current_player_list(self, all=False): """ Returns a list of player objects for players in the current round Returns all players if no tournament running or requested """ if all or not self._tournament: return self._players else: return self._tmanager.get_players_in_round() #eif def player_get_stat_string(self, player): """ Should return a string with information about the player and their current score. Defaults to: primary_score secondary_score : player_name """ return "%.1f" % getattr(player, self._primary_victory) + " " + str( getattr(player, self._secondary_victory)) + " : " + player.name def tournament_is_running(self): """ Returns true if a tournament is running. """ return self._tournament #endregion #region World/Collision Functions def world_add_remove_object(self, wobj, added): """ Called by world when an object is added or destroyed (before added (guaranteed to not have update) and after removed (though may receive last update)) For simple tasks involving players look to the player_died or player_added methods killed ships will not return (used to prevent respawn) """ logging.debug("[Game] Add Object(%s): #%d (%s)", repr(added), wobj.id, friendly_type(wobj)) if not added and isinstance(wobj, SpaceMine) and wobj.active: self.world.causeExplosion(wobj.body.position, SpaceMine.RADIUS, SpaceMine.FORCE, True) # TODO: Cause splash damage? if not added and isinstance( wobj, Ship) and wobj.player.netid in self._players: nid = wobj.player.netid # if we were given an expiration time, means we haven't issued a command, so kill the ship if wobj.has_expired() and self.cfg.getboolean( "Server", "disconnect_on_idle"): logging.info("Ship #%d killed due to timeout.", wobj.id) wobj.killed = True if hasattr(wobj, "killedby") and wobj.killedby != None: if isinstance(wobj.killedby, Ship): self._players[nid].lastkilledby = wobj.killedby.player.name else: self._players[nid].lastkilledby = friendly_type( wobj.killedby) + " #" + str(wobj.killedby.id) self.player_died(self._players[nid], (self._players[nid].disconnected or wobj.killed)) self._players[nid].object = None if not self._players[nid].disconnected: if self._disconnect_on_death or wobj.killed: if self.__allowreentry: del self._players[nid] # TODO: disconnect AI? if nid >= 0: self.server.sendDisconnect(nid) else: if not self._players[nid].roundover: # if the round isn't over, then re-add the ship self._game_add_ship_for_player(nid) if not added: self.spawnmanager.check_number(wobj) def world_physics_pre_collision(self, obj1, obj2): """ Called by the physics engine when two objects just touch for the first time return [True/False, [func, obj, para...]... ] use False to not process collision in the physics engine, the function callback will still be called return a list with lists of function callback requests for the function, and object, and extra parameters The default game prevents anything from colliding with (BlackHole, Nebula, or Star) collide returns False. """ logging.debug("Object #%d colliding with #%d", obj1.id, obj2.id) return obj1.collide_start(obj2) and obj2.collide_start(obj1) def world_physics_collision(self, obj1, obj2, damage): """ Called by the physics engine when two objects collide Return [[func, obj, parameter]...] The default game handles inflicting damage on entities in this step. It is best to override world_physics_pre_collision if you want to prevent things from occuring in the first place. """ logging.debug("Object #%d collided with #%d for %f damage", obj1.id, obj2.id, damage) r = [] obj1.take_damage(damage, obj2) obj2.take_damage(damage, obj1) for gobj in (obj1, obj2): # check both objects for callback if gobj.health.maximum > 0 and gobj.health.value <= 0: logging.info("Object #%d destroyed by %s", gobj.id, repr(gobj.killedby)) r.append([self.world_physics_post_collision, gobj, damage]) #eif if r == []: return None return r def world_physics_post_collision(self, dobj, damage): """ Setup and called by world_physics_collision to process objects which have been destroyed as a result of taking too much damage. The default game causes an explosion force based on the strength of the collision in the vicinity of the collision. dobj: the object destroyed para: extra parameters from a previous step, by default collision passes the strength of the collision only """ strength = damage logging.info("Destroying Object: #%d, Force: %d [%d]", dobj.id, strength, thread.get_ident()) dobj.destroyed = True # get rid of object self.world.causeExplosion(dobj.body.position, dobj.radius * 5, strength, True) #Force in physics step def world_physics_end_collision(self, obj1, obj2): """ Called by the physics engine after two objects stop overlapping/colliding. This is still called even if the pre_collision returned 'False' and no actual collision was processed """ logging.debug("Object #%d no longer colliding with #%d", obj1.id, obj2.id) # notify each object of the finalization of the collision obj1.collide_end(obj2) obj2.collide_end(obj1) #endregion #region GUI Drawing def gui_initialize(self): """ Used to initialize GUI resources at the appropriate time after the graphics engine has been initialized. """ self._tmanager.gui_initialize() self._dfont = debugfont() def gui_draw_game_world_info(self, surface, flags, trackplayer): """ Called by GUI to have the game draw additional (optional) info in the world when toggled on 'G'. (coordinates related to the game world) """ pass def gui_draw_game_screen_info(self, screen, flags, trackplayer): """ Called by GUI to have the game draw additional (optional) info on screen when toggled on 'G'. (coordinates related to the screen) """ pass def gui_get_player_stats(self, all=False): """ Called by GUI to get the sorted list of player stats. GUI expects a list of strings, you should usually override player_get_stat_string. """ sstat = [] for player in self.game_get_current_leader_list(all): sstat.append(self.player_get_stat_string(player)) return sstat def gui_draw_tournament_bracket(self, screen, flags, trackplayer): """ Called by GUI to draw info about the round/tournament (optional) when toggled on 'T'. (coordinates related to the screen) """ if self._tournament and self._tmanager.is_initialized(): self._tmanager.gui_draw_tournament_bracket(screen, flags, trackplayer)