Пример #1
0
class GameWorld(object):
    """
    represents the virtual space world and the objects in it
    Provides hit detection and radar between objects in the world
    """
    def __init__(self, game, worldsize, pys=True):
        """
        Parameters:
            game: game object that manages rules for world
            worldsize: tuple of world width,height
            pys: flag to indicate whether this should be the main world running a physics/game loop or not
        """
        self.__game = game
        self.size = intpos(worldsize)
        self.width = worldsize[0]
        self.height = worldsize[1]
        self.__space = pymunk.Space()
        #self.__space.add_collision_handler(0, 0, post_solve=self.collideShipStellarObject)
        self.__space.set_default_collision_handler(begin=self.__beginCollideObject, post_solve=self.__collideObject)
        self.__objects = ThreadSafeDict()
        self.__addremovesem = threading.Semaphore()
        self.__planets = []
        self.__nebulas = []
        self.__objectListener = []
        self.__toremove = []
        self.__toadd = []        
        self.__active = True
        self.__pys = pys
        self.gameerror = False

        logging.info("Initialized World of size %s", repr(worldsize))
        if pys:
            logging.debug("Started gameloop for World %s", repr(self))
            threading.Thread(None, self.__THREAD__gameloop, "WorldMap_gameloop_" + repr(self)).start()

    def mid_point(self, xoff = 0, yoff = 0):
        return intpos((self.width / 2 + xoff, self.height / 2 + yoff))

    def endGameLoop(self):
        self.__active = False
            
    def newWorld(self, world):
        self.__pys = False
        time.sleep(0.2)
        
        # Remove All Objects
        for obj in self:
            self.pysremove(obj)
            
        # Copy objects from new world to original world
        for obj in world:
            #world.pysremove(obj) # need to remove object from space before adding to another
            self.append(obj, pys=True)
            
        self.__pys = True
        
    def __beginCollideObject(self, space, arbiter):
        if arbiter.is_first_contact:
            r = self.__game.world_physics_pre_collision( arbiter.shapes )
            if r != None:
                for i in r[1]:
                    space.add_post_step_callback(i[0], i[1], i[2:])
                return r[0]  

        return True       
        
    def __collideObject(self, space, arbiter):        
        if arbiter.is_first_contact:
            r = self.__game.world_physics_collision( arbiter.shapes, arbiter.total_impulse.length / 250.0 )
            if r != None:
                for i in r:
                    space.add_post_step_callback(i[0], i[1], i[2:])            
        #eif

    def causeExplosion(self, origin, radius, strength, force=False):
        logging.debug("Start Explosion %s, %d, %d [%d]", origin, radius, strength, thread.get_ident())
        origin = pymunk.Vec2d(origin)
        for obj in self.__objects:
            if obj.explodable:
                for point in wrappos(obj.body.position, radius, self.size):
                    if in_circle(origin, radius, point):                        
                        logging.debug("Applying Explosion Force to #%d", obj.id)
                        obj.body.apply_impulse((point - origin) * strength, (0,0))
                        break        
        logging.debug("Done Explosion") 
        
    def registerObjectListener(self, callback):
        self.__objectListener.append(callback)

    def __len__(self):
        return len(self.__objects)

    def __iter__(self):
        return self.__objects.__iter__()

    def __getitem__(self, key):
        return self.__objects.__getitem__(key)

    def __setitem__(self, key, value):
        return self.__objects.__setitem__(key, value)

    def has_key(self, key):
        return self.__objects.has_key(key)

    def append(self, item, pys=False):
        """
        Call to add an object to the game world from threads outside the game loop
        """
        logging.debug("Append Object to World %s [%d]", repr(item), thread.get_ident())
        for objListener in self.__objectListener:
            objListener(item, True)

        logging.debug("SEMAPHORE ACQ append [%d]", thread.get_ident())
        self.__addremovesem.acquire()
        if not self.__objects.has_key(item.id):
            self.__objects[item.id] = item
            if pys:
                item.addToSpace(self.__space)
            elif item in self.__toremove:
                self.__toremove.remove(item)
            else:
                self.__toadd.append(item)

            if isinstance(item, Planet) and not item in self.__planets and item.pull > 0:
                self.__planets.append(item)
            elif isinstance(item, Nebula) and not item in self.__nebulas and item.pull > 0:
                self.__nebulas.append(item)
        self.__addremovesem.release()
        logging.debug("SEMAPHORE REL append [%d]", thread.get_ident())                

    def remove(self, item):
        """
        Call to remove an object from the game world
        """
        del self[item]

    def pysremove(self, item):
        self.__delitem__(item, True)
        
    def __delitem__(self, key, pys=False):
        logging.debug("Removing Object from World %s [%d]", repr(key), thread.get_ident())
        logging.debug("SEMAPHORE ACQ delitem [%d]", thread.get_ident())
        self.__addremovesem.acquire()            
        if self.__objects.has_key(key.id):
            if pys:
                key.removeFromSpace(self.__space)
            elif key in self.__toadd:
                self.__toadd.remove(key)
            else:
                self.__toremove.append(key)
            
            del self.__objects[key.id]
            if key in self.__planets:
                self.__planets.remove(key)
            elif key in self.__nebulas:
                self.__nebulas.remove(key)
        self.__addremovesem.release()           
        logging.debug("SEMAPHORE REL delitem [%d]", thread.get_ident())
        
        # Notify after removed, in case re-add same object
        for objListener in self.__objectListener:
            objListener(key, False)

    def __THREAD__gameloop(self):
        lasttime = MINIMUM_GAMESTEP_TIME
        try:
            while self.__active:
                if self.__pys:
                    self.__space.step(MINIMUM_GAMESTEP_TIME) # Advance Physics Engine
                
                tstamp = time.time()

                # find objects in nebulas
                for neb in self.__nebulas:
                    for shape in self.__space.shape_query(neb.shape):
                        # Set value to two, so that if we're still in the nebula
                        # for another loop, that we don't toggle in/out of nebula between slices
                        # across threads
                        if self.has_key(shape.id):
                            self[shape.id].in_nebula = [2, neb]

                # update all game objects
                for obj in self: # self is dictionary
                    # Wrap Object in World
                    if obj.body.position[0] < 0:
                        obj.body.position[0] += self.width
                    elif obj.body.position[0] > self.width:
                        obj.body.position[0] -= self.width
                    if obj.body.position[1] < 0:
                        obj.body.position[1] += self.height
                    elif obj.body.position[1] > self.height:
                        obj.body.position[1] -= self.height

                    # Apply Gravity for Planets
                    if obj.explodable:
                        for planet in self.__planets:
                            for point in wrappos(obj.body.position, planet.gravityFieldLength, self.size):
                                if in_circle(planet.body.position, planet.gravityFieldLength, point):                        
                                    obj.body.apply_impulse((point - planet.body.position) * -planet.pull * lasttime, (0,0))
                                    break
                    
                    # Update and Run Commands
                    obj.update(lasttime)
                    if obj.in_nebula != None:
                        # slow down objects in Nebula
                        if obj.body.velocity.length > 0.1:
                            obj.body.velocity.length -= (obj.in_nebula[1].pull / obj.mass) * lasttime
                        obj.in_nebula[0] -= 1 # decrement count
                        if obj.in_nebula[0] <= 0:
                            obj.in_nebula = None

                    if obj.has_expired():
                        del self[obj]                        
                #next            

                # game time notification
                self.__game.game_update(lasttime)

                # update time
                lasttime = time.time() - tstamp
                           
                logging.debug("SEMAPHORE ACQ gameloop [%d]", thread.get_ident())
                self.__addremovesem.acquire()
                # NOTE ISSUE #68 - If server is slow, can be adding and removing object in same step... add first, so we can immediately remove instead of crash
                # TODO: just look for common set between two lists and remove... or have each list check the other first before adding to the lists...
                #       probably best to refactor to PyMunk 4.0.0 and be able to have PyMunk handle adding/removing objects during physics step.
                # add objects
                for item in self.__toadd:
                    logging.info("World Adding #%d to Physics Engine %s", item.id, repr(item))
                    item.addToSpace(self.__space)

                self.__toadd = []

                #remove objects
                for item in self.__toremove:
                    logging.info("World Removing #%d from Physics Engine %s", item.id, repr(item))
                    item.removeFromSpace(self.__space)
                    
                self.__toremove = []
                self.__addremovesem.release()
                logging.debug("SEMAPHORE REL gameloop [%d]", thread.get_ident())
                    
                # minimum to conserve resources
                if lasttime < MINIMUM_GAMESTEP_TIME:
                    time.sleep(MINIMUM_GAMESTEP_TIME - lasttime)
                    lasttime = MINIMUM_GAMESTEP_TIME
                #else:
                    #logg#ging.debug("Can't keep at 50fps at %dfps", 1.0 / lasttime)
                #eif
            #wend
        except:
            print "EXCEPTION IN GAMELOOP"
            logging.exception("FATAL Error in game loop!!!")
            logging.error(traceback.format_exc())
            print traceback.format_exc()
            self.gameerror = True
        logging.debug("Gameloop Ended")

    def getObjectsInArea(self, pos, radius, force=False):
        logging.debug("Get Objects In Area %s %d (%s) [%d]", repr(pos), radius, repr(force), thread.get_ident())
        objList = []
        for obj in self.__objects.values():
            for point in wrappos(obj.body.position, radius, self.size):
                if in_circle(pos, radius, point):
                    objList.append(obj)
                    break
                #eif
            #next
        #next
        return objList

    def getObjectData(self, obj):
        objData = {}
        # Convert Type of Object to String
        objData["TYPE"] = friendly_type(obj)
        objData["ID"] = obj.id
        objData["POSITION"] = intpos(obj.body.position)
        objData["SPEED"] = obj.body.velocity.length
        # TODO: deal with -0.0 case OR match physics library coordinates?
        objData["DIRECTION"] = -obj.body.velocity.angle_degrees # 30 = -120?, -80 = -10
        #objData["VELOCITYDIRECTION"] = obj.velocity.direction
        objData["MAXSPEED"] = obj.body.velocity_limit
        objData["CURHEALTH"] = obj.health.value
        objData["MAXHEALTH"] = obj.health.maximum
        objData["CURENERGY"] = obj.energy.value
        objData["MAXENERGY"] = obj.energy.maximum
        objData["ENERGYRECHARGERATE"] = obj.energyRechargeRate
        objData["MASS"] = obj.mass
        objData["HITRADIUS"] = obj.radius #TODO: Move to entites that have this i.e. physicalround?
        objData["TIMEALIVE"] = obj.timealive

        obj.getExtraInfo(objData)

        self.__game.game_get_extra_radar_info(obj, objData)

        return objData

    def getEnvironment(self, ship, radarlevel=0, target=-1, getMessages=False):
        #TODO: abstract radar to game level?
        radardata = None
        if radarlevel > 0:
            objs = self.getObjectsInArea(ship.body.position, ship.radarRange)
            #TODO: Need to wait lock the removing of ships with accessing...???
            if ship in objs: objs.remove(ship) # Get rid of self...

            # remove ships with cloak
            for x in xrange(len(objs) - 1, -1, -1):
                if isinstance(objs[x], Ship) and objs[x].commandQueue.containstype(CloakCommand):
                    del objs[x]
                #eif
            #next

            if radarlevel == 1:
                # number of objects
                radardata = []
                for i in xrange(len(objs)):
                    radardata.append({})
            elif radarlevel == 2:
                # (pos, id) list
                radardata = []
                for e in objs:
                    radardata.append({"POSITION":intpos(e.body.position), "ID":e.id})
                #next
            elif radarlevel == 3:
                for obj in objs:
                    if obj.id == target:
                        radardata = [self.getObjectData(obj)]
                        break
            elif radarlevel == 4:
                # (pos, id, type)
                radardata = []
                for e in objs:
                    radardata.append({"POSITION":intpos(e.body.position), "ID":e.id, "TYPE":friendly_type(e)})
                #next
            elif radarlevel == 5:
                radardata = []
                for e in objs:
                    radardata.append(self.getObjectData(e))
                #next
            #eif
        #eif

        msgQueue = []
        if getMessages:
            msqQueue = list(ship.messageQueue)

        return {"SHIPDATA": self.getObjectData(ship),
                "RADARLEVEL": radarlevel,
                "RADARDATA": radardata,
                "GAMEDATA": self.__game.game_get_extra_environment(ship.player),
                "MESSAGES": msgQueue,
                }
Пример #2
0
class DiscoveryQuestGame(BasicGame):
    """
    Discovery Quest is a game of exploration.

    Ships must 'Scan' various objects in order to accumulate points.  Every type of object in the game is worth a different number of points.
    Scanning is different from Radar, and requires a precise ID and takes more time and energy to perform.  It also has a limited range which you must stay within for the whole duration of the scan.
    You CANNOT scan the same object more than once for points.

    Ships must establish an 'Outpost' by scanning it.  Once they have done so, they will start to receive 'Missions'.  They initially won't score points for scanned objects until an Outpost is established.
    All points for things scanned before making an Outpost will be awarded when an Outpost is established.

    A Mission dictates which objects a ship should go scan.  If a ship goes and scans ONLY those things, bonus points will be awarded when they return to their outpost and Scan it again.

    Scanning their outpost will always give them a new mission.

    Your Ship being destroyed clears/fails your mission, so you must return to your established outpost if you want a new one.
    """
    def __init__(self, cfgobj):
        self._outposts = ThreadSafeDict()

        super(DiscoveryQuestGame, self).__init__(cfgobj)

        self.mustbase = cfgobj.getboolean("DiscoveryQuest",
                                          "establish_homebase")
        self.usemissions = cfgobj.getboolean("DiscoveryQuest", "missions")
        self._missions = cfgobj.get("DiscoveryQuest",
                                    "mission_objectives").split(",")
        self.scantime = cfgobj.getfloat("DiscoveryQuest", "scan_time")
        self.scanrange = cfgobj.getint("DiscoveryQuest", "scan_range")
        self.scanduration = cfgobj.getint("DiscoveryQuest", "scan_duration")
        self.outpostdist = cfgobj.getint("DiscoveryQuest", "ship_spawn_dist")
        self.limitwarp = cfgobj.getboolean("DiscoveryQuest",
                                           "disable_warp_in_nebula")

    def game_get_info(self):
        return {"GAMENAME": "DiscoveryQuest"}

    def player_reset_mission(self, player):
        if self.usemissions:
            # get a number of missions for each thing
            player.mission = []
            player.scanned = []
            player.failed = False
            for i in xrange(
                    cfg_rand_min_max(self.cfg, "DiscoveryQuest",
                                     "mission_num")):
                player.mission.append(
                    random.choice(self._missions)
                )  # TODO: validate that this player can still scan those types of objects

    def player_added(self, player, reason):
        if reason == BasicGame._ADD_REASON_REGISTER_:
            player.buffervalue = 0  # stored points before scanning first output, if outpost is required.
            player.outpost = None  # home base obj
            player.lastids = []  # last ids scanned
            player.scantimes = {}  # timers for scans, id: timestamp

        player.mission = []  # List of strings of items to scan
        player.scanned = []  # ids scanned by player
        player.failed = True  # whether current mission has failed, starts as true as no mission to start

        return super(DiscoveryQuestGame, self).player_added(player, reason)

    def player_get_start_position(self, force=False):
        # pick a random outpost to spawn the player besides
        out = intpos(random.choice(self._outposts.values()).body.position)

        pos = (random.randint(out[0] - self.outpostdist,
                              out[0] + self.outpostdist),
               random.randint(out[1] - self.outpostdist,
                              out[1] + self.outpostdist))
        x = 0
        while len(self.world.getObjectsInArea(
                pos, self.outpostdist / 2)) > 0 and x < 15:
            x += 1
            pos = (random.randint(out[0] - self.outpostdist,
                                  out[0] + self.outpostdist),
                   random.randint(out[1] - self.outpostdist,
                                  out[1] + self.outpostdist))

        return pos

    def player_died(self, player, gone):
        if gone and player.outpost != None:
            player.outpost.home_for.remove(player)
            player.outpost = None

        if gone:
            for obj in self.world:
                if player in obj.scanned_by:
                    obj.scanned_by.remove(player)

        return super(DiscoveryQuestGame, self).player_died(player, gone)

    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
        """
        if player.buffervalue > 0:
            return "%d %s B: %d" % (getattr(
                player, self._primary_victory), player.name,
                                    getattr(player, self._secondary_victory))
        else:
            return "%d %s" % (getattr(player,
                                      self._primary_victory), player.name)

    def world_add_remove_object(self, wobj, added):
        if isinstance(wobj, Outpost):
            if not added:
                del self._outposts[wobj.id]
            else:
                self._outposts[wobj.id] = wobj

        if added:
            wobj.scanned_by = []  # objects keep track of who scans them

            # give object point value
            opt = "points_" + friendly_type(wobj).lower()
            if self.cfg.has_option("DiscoveryQuest", opt):
                wobj.value = self.cfg.getint("DiscoveryQuest", opt)
            else:
                #guard
                wobj.value = 0
        else:
            # clean-up reference to scantime on obj death
            for player in self.game_get_current_player_list():
                if player in wobj.scanned_by:
                    del player.scantimes[wobj.id]

        return super(DiscoveryQuestGame,
                     self).world_add_remove_object(wobj, added)

    def game_get_extra_environment(self, player):
        env = super(DiscoveryQuestGame,
                    self).game_get_extra_environment(player)

        if player.outpost != None:
            env["POSITION"] = intpos(player.outpost.body.position)
        env["FAILED"] = player.failed
        env["MISSION"] = player.mission
        obj = player.object
        if obj != None:
            cur = []
            for cmd in obj.commandQueue:
                if isinstance(cmd, ScanCommand):
                    cur.append(cmd.target)
            env["CURIDS"] = cur

        env["SUCIDS"] = player.lastids
        player.lastids = []

        return env

    def game_get_extra_radar_info(self, obj, objdata, player):
        super(DiscoveryQuestGame,
              self).game_get_extra_radar_info(obj, objdata, player)

        # return object's value for scanning
        if hasattr(obj, "value"):
            objdata["VALUE"] = obj.value

        objdata["SUCCESS"] = player in obj.scanned_by

    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
        """
        if cmdname == GAME_CMD_SCAN:
            if cmddict.has_key("TARGET") and isinstance(
                    cmddict["TARGET"], int) and cmddict["TARGET"] > 0:
                return ScanCommand(ship, self, cmddict["TARGET"])
            else:
                return "Discovery Quest Scan Command requires a target id."

    def server_process_command(self, ship, command):
        # Discovery Quest prevents warp in a nebula
        if self.limitwarp and isinstance(command, WarpCommand):
            for body in ship.in_celestialbody:
                if isinstance(body, Nebula):
                    logging.info(
                        "Preventing Ship #%d From Warping As In Nebula",
                        ship.id)
                    return "Discovery Quest Rule - Can't warp when in Nebula"

        return command

    def dq_finished_scan(self, ship, obj, success):
        if obj != None:
            logging.info(
                "Ship #%d finished scan of object #%d and was successful? [%s]",
                ship.id, obj.id, repr(success))
        else:
            logging.info(
                "Ship #%d finished scan of unknown object (never saw id)",
                ship.id)

        if success:
            ship.player.lastids.append(obj.id)

            if isinstance(obj, Outpost) and (ship.player in obj.home_for
                                             or ship.player.outpost == None):
                # scanned player's own Outpost, do Mission Stuff HERE
                if ship.player.outpost == None:
                    # establish as ship's Outpost
                    ship.player.outpost = obj
                    obj.home_for.append(ship.player)
                    ship.player.update_score(
                        ship.player.buffervalue + obj.value * 2
                    )  # Score initial points + 2*outpost value for establishing base
                    ship.player.buffervalue = 0
                    ship.player.sound = "SUCCESS"

                if self.usemissions and not ship.player.failed and len(
                        ship.player.mission) == 0:  # completed mission exactly
                    points = 0
                    # tally points of scanned objects
                    for obj in ship.player.scanned:
                        points += obj.value
                    ship.player.sound = "MISSION"
                    ship.player.update_score(points * self.cfg.getfloat(
                        "DiscoveryQuest", "mission_bonus_multiplier"))

                self.player_reset_mission(ship.player)

            elif ship.player in obj.scanned_by:
                logging.info("Ship #%d has ALREADY scanned object #%d",
                             ship.id, obj.id)
            else:
                # mission checks
                if friendly_type(obj) in ship.player.mission:
                    ship.player.mission.remove(friendly_type(obj))
                else:
                    ship.player.failed = True
                ship.player.scanned.append(obj)

                # track obj scan
                ship.player.scantimes[obj.id] = 0
                obj.scanned_by.append(ship.player)

                # update scores
                ship.player.sound = "SUCCESS"
                if ship.player.outpost != None or not self.mustbase:  # or we don't require bases for points
                    ship.player.update_score(obj.value)
                else:  #haven't found outpost, need to buffer points
                    ship.player.buffervalue += obj.value

    #end dq_finished_scan

    def game_update(self, t):
        super(DiscoveryQuestGame, self).game_update(t)

        # check timeout of scan duration (if enabled)
        if self.scanduration > 0:
            for player in self.game_get_current_player_list():
                for id in player.scantimes.keys():
                    player.scantimes[id] += t
                    if player.scantimes[id] >= self.scanduration:
                        obj = self.world[id]
                        obj.scanned_by.remove(player)
                        del player.scantimes[id]

    #end game_update

    def gui_draw_game_world_info(self, surface, flags, trackplayer):
        for player in self.game_get_current_player_list():
            obj = player.object
            if obj != None:
                bp = intpos(obj.body.position)
                wrapcircle(surface, (0, 255, 255), bp, self.scanrange,
                           self.world.size, 1)  # Scan Range
                if self.usemissions:
                    text = debugfont().render(
                        "%s [%s]" % (repr(player.mission), player.failed),
                        False, (0, 255, 255))
                    surface.blit(text,
                                 (bp[0] - text.get_width() / 2, bp[1] - 6))

        if trackplayer != None:
            curs = []
            curf = []
            obj = trackplayer.object
            if obj != None:
                # Draw Success/Failure Circles around Current Scan Targets
                for cmd in obj.commandQueue:
                    if isinstance(cmd, ScanCommand):
                        if cmd.success:
                            obj = self.world[cmd.target]
                            wrapcircle(surface, (255, 255, 0),
                                       intpos(obj.body.position),
                                       obj.radius + 6, self.world.size, 2)
                        else:
                            obj = self.world[cmd.target]
                            wrapcircle(surface, (255, 0, 0),
                                       intpos(obj.body.position),
                                       obj.radius + 6, self.world.size, 2)

            # Draw Circles around scanned entities
            for id, scantime in trackplayer.scantimes.iteritems():
                obj = self.world[id]
                #if trackplayer in obj.scanned_by:
                if self.scanduration > 0:
                    c = 160 * (scantime / self.scanduration)
                else:
                    c = 0
                wrapcircle(surface, (0, 255 - c, 255 - c),
                           intpos(obj.body.position), obj.radius + 4,
                           self.world.size, 4)
Пример #3
0
class DiscoveryQuestGame(BasicGame):
    """
    Discovery Quest is a game of exploration.

    Ships must 'Scan' various objects in order to accumulate points.  Every type of object in the game is worth a different number of points.
    Scanning is different from Radar, and requires a precise ID and takes more time and energy to perform.  It also has a limited range which you must stay within for the whole duration of the scan.
    You CANNOT scan the same object more than once for points.

    Ships must establish an 'Outpost' by scanning it.  Once they have done so, they will start to receive 'Missions'.  They initially won't score points for scanned objects until an Outpost is established.
    All points for things scanned before making an Outpost will be awarded when an Outpost is established.

    A Mission dictates which objects a ship should go scan.  If a ship goes and scans ONLY those things, bonus points will be awarded when they return to their outpost and Scan it again.

    Scanning their outpost will always give them a new mission.

    Your Ship being destroyed clears/fails your mission, so you must return to your established outpost if you want a new one.
    """
    def __init__(self, cfgobj):
        self._outposts = ThreadSafeDict()

        super(DiscoveryQuestGame, self).__init__(cfgobj)

        self.mustbase = cfgobj.getboolean("DiscoveryQuest", "establish_homebase")
        self.usemissions = cfgobj.getboolean("DiscoveryQuest", "missions")
        self._missions = cfgobj.get("DiscoveryQuest", "mission_objectives").split(",")
        self.scantime = cfgobj.getfloat("DiscoveryQuest", "scan_time")
        self.scanrange = cfgobj.getint("DiscoveryQuest", "scan_range")
        self.scanduration = cfgobj.getint("DiscoveryQuest", "scan_duration")
        self.outpostdist = cfgobj.getint("DiscoveryQuest", "ship_spawn_dist")
        self.limitwarp = cfgobj.getboolean("DiscoveryQuest", "disable_warp_in_nebula")

    def game_get_info(self):
        return {"GAMENAME": "DiscoveryQuest"}

    def player_reset_mission(self, player):
        if self.usemissions:
            # get a number of missions for each thing
            player.mission = []
            player.scanned = []
            player.failed = False
            for i in xrange(cfg_rand_min_max(self.cfg, "DiscoveryQuest", "mission_num")):
                player.mission.append(random.choice(self._missions)) # TODO: validate that this player can still scan those types of objects

    def player_added(self, player, reason):
        if reason == BasicGame._ADD_REASON_REGISTER_:
            player.buffervalue = 0 # stored points before scanning first output, if outpost is required.
            player.outpost = None # home base obj
            player.lastids = [] # last ids scanned
            player.scantimes = {} # timers for scans, id: timestamp
            
        player.mission = [] # List of strings of items to scan
        player.scanned = [] # ids scanned by player
        player.failed = True # whether current mission has failed, starts as true as no mission to start

        return super(DiscoveryQuestGame, self).player_added(player, reason)

    def player_get_start_position(self, force = False):
        # pick a random outpost to spawn the player besides
        out = intpos(random.choice(self._outposts.values()).body.position)

        pos = (random.randint(out[0]-self.outpostdist, out[0]+self.outpostdist),
               random.randint(out[1]-self.outpostdist, out[1]+self.outpostdist))
        x = 0
        while len(self.world.getObjectsInArea(pos, self.outpostdist / 2)) > 0 and x < 15:
            x += 1
            pos = (random.randint(out[0]-self.outpostdist, out[0]+self.outpostdist),
                   random.randint(out[1]-self.outpostdist, out[1]+self.outpostdist))
        
        return pos

    def player_died(self, player, gone):
        if gone and player.outpost != None:
            player.outpost.home_for.remove(player)
            player.outpost = None

        if gone:
            for obj in self.world:
                if player in obj.scanned_by:
                    obj.scanned_by.remove(player)

        return super(DiscoveryQuestGame, self).player_died(player, gone)

    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
        """
        if player.buffervalue > 0:
            return "%d %s B: %d" % (getattr(player, self._primary_victory), player.name, getattr(player, self._secondary_victory))
        else:
            return "%d %s" % (getattr(player, self._primary_victory), player.name)

    def world_add_remove_object(self, wobj, added):
        if isinstance(wobj, Outpost):
            if not added:
                del self._outposts[wobj.id]
            else:
                self._outposts[wobj.id] = wobj

        if added:
            wobj.scanned_by = [] # objects keep track of who scans them

            # give object point value
            opt = "points_" + friendly_type(wobj).lower()
            if self.cfg.has_option("DiscoveryQuest", opt):
                wobj.value = self.cfg.getint("DiscoveryQuest", opt)
            else:
                #guard
                wobj.value = 0
        else:
            # clean-up reference to scantime on obj death
            for player in self.game_get_current_player_list():
                if player in wobj.scanned_by:
                    del player.scantimes[wobj.id]

        return super(DiscoveryQuestGame, self).world_add_remove_object(wobj, added)

    def game_get_extra_environment(self, player):
        env = super(DiscoveryQuestGame, self).game_get_extra_environment(player)

        if player.outpost != None:
            env["POSITION"] = intpos(player.outpost.body.position)
        env["FAILED"] = player.failed
        env["MISSION"] = player.mission
        obj = player.object
        if obj != None:
            cur = []
            for cmd in obj.commandQueue:
                if isinstance(cmd, ScanCommand):
                    cur.append(cmd.target)
            env["CURIDS"] = cur

        env["SUCIDS"] = player.lastids
        player.lastids = []

        return env

    def game_get_extra_radar_info(self, obj, objdata, player):
        super(DiscoveryQuestGame, self).game_get_extra_radar_info(obj, objdata, player)

        # return object's value for scanning
        if hasattr(obj, "value"):
            objdata["VALUE"] = obj.value

        objdata["SUCCESS"] = player in obj.scanned_by

    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
        """
        if cmdname == GAME_CMD_SCAN:
            if cmddict.has_key("TARGET") and isinstance(cmddict["TARGET"], int) and cmddict["TARGET"] > 0:
                return ScanCommand(ship, self, cmddict["TARGET"])
            else:
                return "Discovery Quest Scan Command requires a target id."

    def server_process_command(self, ship, command):
        # Discovery Quest prevents warp in a nebula
        if self.limitwarp and isinstance(command, WarpCommand):
            for body in ship.in_celestialbody:
                if isinstance(body, Nebula):
                    logging.info("Preventing Ship #%d From Warping As In Nebula", ship.id)
                    return "Discovery Quest Rule - Can't warp when in Nebula"

        return command

    def dq_finished_scan(self, ship, obj, success):
        if obj != None:
            logging.info("Ship #%d finished scan of object #%d and was successful? [%s]", ship.id, obj.id, repr(success))
        else:
            logging.info("Ship #%d finished scan of unknown object (never saw id)", ship.id)

        if success:
            ship.player.lastids.append(obj.id)

            if isinstance(obj, Outpost) and (ship.player in obj.home_for or ship.player.outpost == None):
                # scanned player's own Outpost, do Mission Stuff HERE
                if ship.player.outpost == None:
                    # establish as ship's Outpost
                    ship.player.outpost = obj
                    obj.home_for.append(ship.player)
                    ship.player.update_score(ship.player.buffervalue + obj.value * 2) # Score initial points + 2*outpost value for establishing base
                    ship.player.buffervalue = 0
                    ship.player.sound = "SUCCESS"
                
                if self.usemissions and not ship.player.failed and len(ship.player.mission) == 0: # completed mission exactly
                    points = 0
                    # tally points of scanned objects
                    for obj in ship.player.scanned:
                        points += obj.value
                    ship.player.sound = "MISSION"
                    ship.player.update_score(points * self.cfg.getfloat("DiscoveryQuest", "mission_bonus_multiplier"))

                self.player_reset_mission(ship.player)

            elif ship.player in obj.scanned_by:
                logging.info("Ship #%d has ALREADY scanned object #%d", ship.id, obj.id)
            else:
                # mission checks
                if friendly_type(obj) in ship.player.mission:
                    ship.player.mission.remove(friendly_type(obj))
                else:
                    ship.player.failed = True
                ship.player.scanned.append(obj)

                # track obj scan
                ship.player.scantimes[obj.id] = 0
                obj.scanned_by.append(ship.player)

                # update scores
                ship.player.sound = "SUCCESS"
                if ship.player.outpost != None or not self.mustbase: # or we don't require bases for points
                    ship.player.update_score(obj.value)
                else: #haven't found outpost, need to buffer points
                    ship.player.buffervalue += obj.value
    #end dq_finished_scan

    def game_update(self, t):
        super(DiscoveryQuestGame, self).game_update(t)

        # check timeout of scan duration (if enabled)
        if self.scanduration > 0:
            for player in self.game_get_current_player_list():
                for id in player.scantimes.keys():
                    player.scantimes[id] += t                    
                    if player.scantimes[id] >= self.scanduration:
                        obj = self.world[id]
                        obj.scanned_by.remove(player)
                        del player.scantimes[id]
    #end game_update

    def gui_draw_game_world_info(self, surface, flags, trackplayer):
        for player in self.game_get_current_player_list():
            obj = player.object
            if obj != None:
                bp = intpos(obj.body.position)
                wrapcircle(surface, (0, 255, 255), bp, self.scanrange, self.world.size, 1) # Scan Range
                if self.usemissions:
                    text = debugfont().render("%s [%s]" % (repr(player.mission), player.failed), False, (0, 255, 255))
                    surface.blit(text, (bp[0]-text.get_width()/2, bp[1] - 6))

        if trackplayer != None:
            curs = []
            curf = []
            obj = trackplayer.object
            if obj != None:
                # Draw Success/Failure Circles around Current Scan Targets
                for cmd in obj.commandQueue:
                    if isinstance(cmd, ScanCommand):
                        if cmd.success:
                            obj = self.world[cmd.target]
                            wrapcircle(surface, (255, 255, 0), intpos(obj.body.position), obj.radius + 6, self.world.size, 2)
                        else:
                            obj = self.world[cmd.target]
                            wrapcircle(surface, (255, 0, 0), intpos(obj.body.position), obj.radius + 6, self.world.size, 2)

            # Draw Circles around scanned entities
            for id, scantime in trackplayer.scantimes.iteritems():
                obj = self.world[id]
                #if trackplayer in obj.scanned_by:
                if self.scanduration > 0:
                    c = 160 * (scantime / self.scanduration)
                else:
                    c = 0
                wrapcircle(surface, (0, 255 - c, 255 - c), intpos(obj.body.position), obj.radius + 4, self.world.size, 4)                    
Пример #4
0
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)
Пример #5
0
def startGame(windowcaption, game, fullscreen=True, resolution=None, cfg=None, testcase=None):
    try:
        #region Initialization
        logging.info("Initiating PyGame...")

        world = game.world
        pygame.init()
        fpsClock = pygame.time.Clock()

        logging.debug("Initiating Screen...")

        if cfg.getboolean("Application", "sound"):
            logging.info("Starting Sound...")
            pygame.mixer.init()
            SCache(True)
        else:
            logging.info("Sound Disabled")
            SCache(False)
    
        ipaddress = getIPAddress()

        if resolution == None:
            resolution = detect_resolution(cfg)

        fullscreen = str2bool(fullscreen)
        if fullscreen in [True, False]:
            windowSurface = pygame.display.set_mode(resolution, (pygame.FULLSCREEN * fullscreen) | pygame.HWSURFACE | pygame.DOUBLEBUF)
        elif fullscreen == "window":
            os.environ['SDL_VIDEO_WINDOW_POS'] = '0,0'
            windowSurface = pygame.display.set_mode(resolution, pygame.NOFRAME | pygame.HWSURFACE | pygame.DOUBLEBUF)
            os.environ['SDL_VIDEO_WINDOW_POS'] = '' # Reset back
        else:
            raise ValueError("Invalid Fullscreen Windowing Mode")

        pygame.display.set_caption(windowcaption)

        logging.debug("Game GUI CFG...")
        # Let game initialize any GUI objects independently
        game.gui_initialize()

        colorBlack = pygame.Color(0, 0, 0)
        mt = 0

        #shipw = ShipGUI(Ship((100, 100)))
        #shipw.ship.velocity.magnitude = 5

        #region World Registration
        bgobjects = ThreadSafeDict() # items we want always in the background
        objects = ThreadSafeDict()
        trackplayer = None
        def addorremove(obj, added):
            logging.debug("GUI: Add/Remove Obj %s (%s) [%d]", repr(obj), repr(added), thread.get_ident())
            try:
                if added:
                    # TODO: #18 - Clean-up this by auto creating wrapper on name...
                    if isinstance(obj, Ship):
                        logging.debug("GUI: Adding Ship #%d", obj.id)
                        objects[obj.id] = ShipGUI(obj, world)
                        logging.debug("GUI: Added Ship #%d", obj.id)
                    elif isinstance(obj, Nebula):
                        logging.debug("GUI: Adding Nebula #%d", obj.id)
                        bgobjects[obj.id] = NebulaGUI(obj, world)
                        logging.debug("GUI: Added Nebula #%d", obj.id)
                    elif isinstance(obj, Planet) or isinstance(obj, Constellation):
                        logging.debug("GUI: Adding Planet #%d", obj.id)
                        bgobjects[obj.id] = PlanetGUI(obj, world)
                        logging.debug("GUI: Added Planet #%d", obj.id)
                    elif isinstance(obj, WormHole):
                        logging.debug("GUI: Adding WormHole #%d", obj.id)
                        bgobjects[obj.id] = WormHoleGUI(obj, world)
                        logging.debug("GUI: Added WormHole #%d", obj.id)
                    elif isinstance(obj, Asteroid):
                        logging.debug("GUI: Adding Asteroid #%d", obj.id)
                        objects[obj.id] = AsteroidGUI(obj, world)
                        logging.debug("GUI: Added Asteroid #%d", obj.id)
                    elif isinstance(obj, Dragon):
                        logging.debug("GUI: Adding Dragon #%d", obj.id)
                        objects[obj.id] = DragonGUI(obj, world)
                        logging.debug("GUI: Added Dragon #%d", obj.id)
                    elif isinstance(obj, Torpedo):
                        logging.debug("GUI: Adding Torpedo #%d", obj.id)
                        objects[obj.id] = TorpedoGUI(obj, world)
                        logging.debug("GUI: Added Torpedo #%d", obj.id)
                    elif isinstance(obj, SpaceMine):
                        logging.debug("GUI: Adding SpaceMine #%d", obj.id)
                        objects[obj.id] = SpaceMineGUI(obj, world)
                        logging.debug("GUI: Added SpaceMine #%d", obj.id)
                    else:
                        logging.debug("GUI: Adding %s #%d", repr(obj), obj.id)
                        objects[obj.id] = obj.WRAPPERCLASS(obj, world)
                        logging.debug("GUI: Added %s %d", repr(obj), obj.id)
                    #eif
                else:
                    if isinstance(obj, Ship):
                        logging.debug("GUI: Ship #%d Dying", obj.id)
                        ship = objects.get(obj.id, None)
                        if ship != None:
                            ship.dying = True
                            logging.debug("GUI: Ship #%d Set Dying", obj.id)
                    elif obj.id in objects:
                        logging.debug("GUI: Removing Object #%d", obj.id)
                        del objects[obj.id]
                        logging.debug("GUI: Removed Object #%d", obj.id)
                    elif obj.id in bgobjects:
                        logging.debug("GUI: Removing BG Object #%d", obj.id)
                        del bgobjects[obj.id]
                        logging.debug("GUI: Removed BG Object #%d", obj.id)
                    #eif
                #eif
            except:
                logging.error("GUI ERROR")
                logging.info(traceback.format_exc())
                logging.error(traceback.format_exc())
                print traceback.format_exc()
            #logging.debug("GUI: Add/Remove Done [%d]", thread.get_ident())
        #endregion

        logging.debug("Register World Listener...")
        world.registerObjectListener(addorremove)

        logging.debug("Add Objects to GUI...")
        # Create objects for all items in world
        for obj in world:
            addorremove(obj, True)
        #efor

        # Create Stars
        #stars = []
        #for i in xrange((world.width / STAR_DENSITY * world.height / STAR_DENSITY)):
        #    stars.append(Star((random.randint(4, world.width - 4), random.randint(4, world.height - 4))))

        logging.debug("Create Fonts...")
        # Start Main Loop
        font = pygame.font.Font("freesansbold.ttf", 12)
        bigfont = pygame.font.Font("freesansbold.ttf", 44)

        logging.debug("Load Textures...")
        spacetexture = Cache().getImage("Backgrounds/space")
        scalefactorx = float(world.size[0]) / resolution[0]
        scalefactory = float(world.size[1]) / resolution[1]
        offsetx = 0
        maxoffsetx = -world.width + resolution[0]
        offsety = 0
        maxoffsety = -world.height + resolution[1]

        def centerViewOnWorld(pos):
            return -pos[0] + resolution[0]/2, -pos[1] + resolution[1]/2
    
        #keyboard repeat rate
        pygame.key.set_repeat(75, 25)

        zoomout = True
        mouselock = True
        MOUSELOCK_RANGE = resolution[0] / 8
        logintercept = False
        mlh = MessageLogHandler(50)
    
        dynamiccamera = False

        mousemode = None
        prevzoom = zoomout
        showip = cfg.getboolean("Application", "showip")
        showplayerlist = cfg.getboolean("Application", "showstats")
        showroundtime = cfg.getboolean("Tournament", "tournament")
        tournamentinfo = cfg.getboolean("Tournament", "tournament")
    
        flags = {"DEBUG":False,
                 "STATS":cfg.getboolean("Application", "showstats"),
                 "NAMES":True,
                 "GAME":cfg.getboolean("Application", "showstats"),
                 "THREADS":False}
    
        logging.info("Create Main Surface...")
        #TODO: Optimize by only drawing objects in viewport...
        worldsurface = pygame.Surface((world.size[0], world.size[1]), flags=HWSURFACE)
    
        logging.debug("Game Start!")
        obj = None
    
        notexit = True

        # In test case we only care about test being done
        if testcase != None:
            notexit = False

        #endregion

        # Get spawnable items in alphabetical order
        SPAWN_TYPES = SpawnManager.ENTITY_TYPES.keys()
        SPAWN_TYPES.sort()

        while notexit or (testcase != None and not testcase.donetest):
            t = pygame.time.get_ticks() / 1000.0

            #for star in stars:
            #    star.draw(windowSurface)

            # TODO: figure out abstraction for pygame or method in order to get objwrapper to figure out if it needs
            # to draw in the screen or be no-op, ideally objwrapper just does what it does now...

            worldsurface.fill(colorBlack)
                
            #for x in xrange(offsetx, resolution[0], 512):
            #    for y in xrange(offsety, resolution[1], 512):
            #        worldsurface.blit(spacetexture, (x, y))

            # draw background items first in specific z-order
            for obj in sorted(bgobjects.values(), key=attrgetter('zorder')):
                obj.draw(worldsurface, flags)
                if obj.dead:
                    del bgobjects[obj._worldobj.id]
       
            # draw active objects
            for obj in objects:
                obj.draw(worldsurface, flags)
                if hasattr(obj._worldobj, "player") and obj._worldobj.player.sound != None:
                    #print "Trying to play sound", obj._worldobj.player.sound
                    SCache().play(obj._worldobj.player.sound)
                    obj._worldobj.player.sound = None
                if obj.dead:
                    del objects[obj._worldobj.id]
                            
            if flags["GAME"]:
                game.gui_draw_game_world_info(worldsurface, flags, trackplayer)

            #region Key/Mouse Event Handling
            for event in pygame.event.get():
                if event.type == QUIT:
                    notexit = False
                elif (event.type == KEYDOWN and event.key == K_a) or (event.type == MOUSEBUTTONDOWN and event.button in (4, 5)):
                    if mousemode == None or not mousemode.startswith("Add"):
                        if event.type == KEYDOWN or event.button == 5:
                            mousemode = "Add" + SPAWN_TYPES[0]
                        else:
                            mousemode = "Add" + SPAWN_TYPES[-1]
                    elif event.type == KEYDOWN or event.button == 5:
                        x = SPAWN_TYPES.index(mousemode[3:])
                        if x < len(SPAWN_TYPES) - 1:
                            mousemode = "Add" + SPAWN_TYPES[x+1]
                        elif event.type == MOUSEBUTTONDOWN:
                            mousemode = "Add" + SPAWN_TYPES[0]
                        else:
                            mousemode = None
                    else:
                        x = SPAWN_TYPES.index(mousemode[3:])
                        if x > 0:
                            mousemode = "Add" + SPAWN_TYPES[x-1]
                        else:
                            mousemode = "Add" + SPAWN_TYPES[-1]
                elif event.type == KEYDOWN:
                    if event.key == K_ESCAPE:
                        pygame.event.post(pygame.event.Event(QUIT))
                    elif event.key == K_SPACE and not game.round_get_has_started():
                        game.round_start()
                    elif event.key == K_d:
                        flags["DEBUG"] = not flags["DEBUG"]
                    elif event.key == K_i:
                        showip = not showip
                    elif event.key == K_p:
                        showplayerlist = (showplayerlist + 1) % (2 + game.tournament_is_running())
                    elif event.key == K_s:
                        flags["STATS"] = not flags["STATS"]
                    elif event.key == K_n:
                        flags["NAMES"] = not flags["NAMES"]
                    elif event.key == K_UP:
                        offsety += 16
                    elif event.key == K_DOWN:
                        offsety -= 16
                    elif event.key == K_RIGHT:
                        offsetx -= 16
                    elif event.key == K_LEFT:
                        offsetx += 16
                    elif event.key == K_g:
                        flags["GAME"] = not flags["GAME"]
                    elif event.key == K_h:
                        flags["THREADS"] = not flags["THREADS"]
                    elif event.key == K_t and game.tournament_is_running():
                        tournamentinfo = not tournamentinfo
                    elif event.key == K_z:
                        zoomout = not zoomout
                    elif event.key == K_m:
                        mouselock = not mouselock
                    elif event.key == K_PAGEUP and len(game.game_get_current_player_list()) > 0:
                        if trackplayer == None:
                            trackplayer = game.game_get_current_leader_list()[-1]
                        else:
                            lst = game.game_get_current_leader_list()
                            for x in xrange(len(lst) - 1, -1, -1):
                                if lst[x] == trackplayer:
                                    if x == 0:
                                        x = len(lst)
                                    trackplayer = lst[x-1]
                                    break
                    elif event.key == K_PAGEDOWN and len(game.game_get_current_player_list()) > 0:
                        if trackplayer == None: 
                            trackplayer = game.game_get_current_leader_list()[0]
                        else:
                            lst = game.game_get_current_leader_list()
                            for x in xrange(len(lst)):
                                if lst[x] == trackplayer:
                                    if x == len(lst) - 1:
                                        x = -1
                                    trackplayer = lst[x+1]
                                    break
                    elif event.key == K_END:
                        trackplayer = None
                        mlh.filter = None
                    elif event.key == K_y:
                        dynamiccamera = not dynamiccamera
                        if not dynamiccamera:
                            trackplayer = None
                    elif event.key == K_l:
                        logintercept = not logintercept
                        logger = logging.getLogger()
                        if logintercept:                        
                            logger.addHandler(mlh)
                        else:
                            logger.removeHandler(mlh)
                            mlh.messages.clear()
                    elif event.key == K_k:
                        if mousemode == "Destroy":
                            mousemode = None
                        else:
                            mousemode = "Destroy"
                    elif event.key == K_e:
                        if mousemode == "Explode":
                            mousemode = None
                        else:
                            mousemode = "Explode"                    
                    elif event.key == K_v:
                        if mousemode == "Move":
                            mousemode = None
                            zoomout = prevzoom
                        else:
                            mousemode = "Move"
                            prevzoom = zoomout
                            zoomout = True
                    elif event.key == K_r:
                        showroundtime = not showroundtime
                elif event.type == MOUSEBUTTONDOWN and event.button in (1, 2): #Left or Middle Click
                    if zoomout:
                        x = event.pos[0]*scalefactorx
                        y = event.pos[1]*scalefactory
                    else:
                        x, y = event.pos[0]-offsetx, event.pos[1]-offsety
                    #print zoomout, x, y

                    if mousemode == "Move" and trackplayer != None:
                        if event.button != 2:
                            mousemode = None
                        zoomout = prevzoom
                        obj = trackplayer.object
                        if obj != None:
                            obj.body.position = (x, y)
                    elif mousemode == "Destroy":
                        if event.button != 2:
                            mousemode = None
                        v = Vec2d(x, y)
                        for lst in objects, bgobjects:
                            for obj in lst:
                                if math.sqrt(obj._worldobj.body.position.get_dist_sqrd(v)) <= 32:
                                    logging.info("[GUI] Destroying Object #%d", obj._worldobj.id)
                                    if isinstance(obj, ShipGUI):
                                        obj._worldobj.killed = True
                                    world.remove(obj._worldobj)
                                    break
                    elif mousemode == "Explode":
                        if event.button != 2:
                            mousemode = None
                        world.causeExplosion((x, y), 256, 1000)
                    elif mousemode != None and mousemode.startswith("Add"):
                        game.spawnmanager.spawn_entity(mousemode[3:], (x, y), False)
                        if event.button != 2:
                            mousemode = None
                    elif trackplayer != None:
                        v = Vec2d(x, y)
                        for obj in objects:
                            if isinstance(obj, ShipGUI) and math.sqrt(obj._worldobj.body.position.get_dist_sqrd(v)) <= 32:
                                logging.info("[GUI] Click Tracking Object #%d", obj._worldobj.id)
                                trackplayer = obj._worldobj.player
                                zoomout = False
                                dynamiccamera = False
                                break
                    elif zoomout and event.button == 1:
                        # zoom in
                        zoomout = False
                        #xoffset = -(((event.pos[0] - resolution[0]/2) % 16)*16 * scalefactorx)
                        #yoffset = -(((event.pos[1] - resolution[1]/2) % 16)*16 * scalefactory)
                        offsetx, offsety = centerViewOnWorld((event.pos[0]*scalefactorx, event.pos[1]*scalefactory))
                    else:
                        # recenter on new click
                        offsetx, offsety = centerViewOnWorld((event.pos[0]*scalefactorx, event.pos[1]*scalefactory))
                elif event.type == MOUSEBUTTONDOWN and event.button == 3: #Right Click
                    mousemode = None
            #endregion 

            # Tracks a ship on screen
            if len(game.game_get_current_player_list()) == 0 or trackplayer not in game.game_get_current_leader_list():
                trackplayer = None
        
            if dynamiccamera and len(game.game_get_current_leader_list()) > 0:
                trackplayer = game.game_get_current_leader_list()[0]
            
        
            if trackplayer != None:
                obj = trackplayer.object
                if obj != None:
                    mlh.filter = "#" + repr(obj.id)
                    offsetx, offsety = centerViewOnWorld(obj.body.position)

            # Pans world with mouse
            if not mouselock:
                # TODO Edge Detect
                pos = pygame.mouse.get_pos()
                if pos[0] < MOUSELOCK_RANGE:
                    offsetx += 48
                elif pos[0] > resolution[0] - MOUSELOCK_RANGE:
                    offsetx -= 48

                if pos[1] < MOUSELOCK_RANGE:
                    offsety += 48
                elif pos[1] > resolution[1] - MOUSELOCK_RANGE:
                    offsety -= 48
            #eif        

            if offsetx > 0: offsetx = 0
            if offsety > 0: offsety = 0
            if offsetx < maxoffsetx: offsetx = maxoffsetx
            if offsety < maxoffsety: offsety = maxoffsety

            if zoomout:
                pygame.transform.scale(worldsurface, resolution, windowSurface)
            else:
                windowSurface.blit(worldsurface, (offsetx, offsety)) 

            if showip:
                ip = bigfont.render(ipaddress, False, (255, 255, 255))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,0))

            if not game.round_get_has_started():
                ip = bigfont.render("Waiting For Start - Press Space", False, (255, 255, 255))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,resolution[1]/2-ip.get_height()/2))
            
                if game.laststats != None:
                    x = 0
                    for i in game.laststats:
                        windowSurface.blit(font.render(i, False, (192, 192, 255)), (resolution[0]/2-300, resolution[1]/2 + 64 + 12*x))
                        x += 1
            
            if showplayerlist > 0:
                x = 0
                for i in game.gui_get_player_stats(all=(showplayerlist == 1)):
                    c = (192, 192, 192)
                    if trackplayer != None and trackplayer.name in i:
                        c = trackplayer.color
                    windowSurface.blit(font.render(i, False, c), (resolution[0]-300, 64 + 12*x))
                    x += 1
                if showplayerlist == 1:
                    windowSurface.blit(font.render(repr(x) + " Players Connected", False, (192, 192, 192)), (resolution[0]-300, 64 + 12 * x))
                else:
                    windowSurface.blit(font.render(repr(x) + " Players In Round", False, (192, 192, 192)), (resolution[0]-300, 64 + 12 * x))

            if showroundtime and testcase == None:
                ip = bigfont.render(str(datetime.timedelta(seconds=game.round_get_time_remaining())), False, (255, 255, 255))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,32))
            elif testcase != None:
                if testcase.flashcolor == False:
                    testcase.flashcolor = (255, 255, 255)
                ip = bigfont.render(str(datetime.timedelta(milliseconds=pygame.time.get_ticks())), False, testcase.flashcolor)
                windowSurface.blit(ip, (32,32))

                testcase.flashcolor = False
            
            if flags["DEBUG"]:
                mpos = pygame.mouse.get_pos()
                if zoomout: # TODO: Need to refactor and centralize these calculations as part of 'Camera' system
                    x = mpos[0]*scalefactorx
                    y = mpos[1]*scalefactory
                else:
                    x, y = mpos[0]-offsetx, mpos[1]-offsety

                windowSurface.blit(font.render("DEBUG MODE", False, (255, 255, 255)), (resolution[0]/2-38,32))
                windowSurface.blit(font.render("FPS: " + repr(fpsClock.get_fps()), False, (255, 255, 255)), (0,0))
                windowSurface.blit(font.render("T: " + repr(len(threading.enumerate())), False, (192, 192, 192)), (0, 12))
                windowSurface.blit(font.render("World Offset: " + repr((-offsetx, -offsety)), False, (255, 255, 255)), (0,24))
                windowSurface.blit(font.render("World Size: " + repr(world.size) + " - " + repr((int(x), int(y))), False, (255, 255, 255)), (0,36))
                windowSurface.blit(font.render("World Objects: " + repr(len(world)), False, (255, 255, 255)), (0,48))            
            
                if mouselock:
                    windowSurface.blit(font.render("MOUSELOCK: ON", False, (255, 255, 255)), (resolution[0]-114,0))
                else:
                    windowSurface.blit(font.render("MOUSELOCK: OFF", False, (255, 255, 255)), (resolution[0]-114,0))
                if "-" in windowcaption:
                    text = font.render(windowcaption.split("-")[1], False, (192, 192, 192))
                    windowSurface.blit(text, (resolution[0]-text.get_width()-2, 12))
                    text = font.render(windowcaption.split("-")[0][18:-1], False, (192, 192, 192))
                    windowSurface.blit(text, (resolution[0]-text.get_width()-2, 24))
                else:
                    text = font.render(windowcaption[18:], False, (192, 192, 192))
                    windowSurface.blit(text, (resolution[0]-text.get_width()-2, 24))

                windowSurface.blit(font.render(repr(flags), False, (255, 255, 255)), (0, resolution[1]-12))            

                if logintercept:
                    for i in xrange(len(mlh.messages)):
                        if i >= len(mlh.messages): break
                        c = 145 + i*2
                        windowSurface.blit(font.render(mlh.messages[i], False, (c, c, c)), (10, 64 + 12*i))

                if trackplayer == None:
                    windowSurface.blit(font.render("Tracking Off", False, (255, 255, 255)), (resolution[0]/2-36,resolution[1]-12))

            if trackplayer != None:
                to = trackplayer.object
                if to != None and objects.has_key(to.id):
                    # if we're tracking a ship, let's print some useful info about it.
                    #windowSurface.blit(font.render(trackplayer.name, False, trackplayer.color), (resolution[0] - 200, resolution[1] - 120))
                    windowSurface.fill((128, 128, 128), Rect(resolution[0] - 310, resolution[1] - 120, 300, 110), pygame.BLEND_ADD)
                    pygame.draw.rect(windowSurface, (192, 192, 192, 128), Rect(resolution[0] - 310, resolution[1] - 120, 300, 110), 1)
                    objects[to.id].draw(windowSurface, 
                                                        {"DEBUG":False,
                                                            "STATS":False,
                                                            "NAMES":True,
                                                            "GAME":False}, Vec2d(resolution[0] - 250, resolution[1] - 64))
                    windowSurface.blit(font.render("%d" % to.body.velocity.length, False, (255, 255, 255)), (resolution[0] - 298, resolution[1] - 86))

                    netenergy = 4
                    x = 0
                    for cmd in to.commandQueue:
                        netenergy -= cmd.energycost
                        windowSurface.blit(font.render("%.1f (%s" % (cmd.energycost, repr(cmd)[14:]), False, trackplayer.color), (resolution[0] - 300, resolution[1] - 28 - x * 12))
                        x += 1
                    x = "+"
                    if netenergy < 0:
                        x = "-"
                    #windowSurface.blit(infofont().render("Energy %d  %s%.1f" % (to.energy.value, x, netenergy), False, trackplayer.color), (resolution[0] - 190, resolution[1] - 110))

                    # Energy Bar
                    windowSurface.fill((0, 64, 0), Rect(resolution[0] - 190, resolution[1] - 114, 170, 18))
                    windowSurface.blit(pygame.transform.scale(Cache().getImage("HUD/Energy"), (166, 14)), (resolution[0] - 188, resolution[1] - 112), pygame.Rect(0, 0, 166 * to.energy.percent, 14))
                    windowSurface.blit(infofont().render("%d %s%.1f" % (to.energy.value, x, netenergy), False, (255, 255, 255)), (resolution[0] - 180, resolution[1] - 112))

                    # Shield Bar
                    windowSurface.fill((0, 0, 96), Rect(resolution[0] - 190, resolution[1] - 94, 170, 8))
                    windowSurface.blit(pygame.transform.scale(Cache().getImage("HUD/Shield"), (166, 14)), (resolution[0] - 188, resolution[1] - 93), pygame.Rect(0, 0, 166 * to.shield.percent, 6))
                    windowSurface.blit(font.render(repr(int(100 * to.shield.percent)), False, (255, 255, 255)), (resolution[0] - 178, resolution[1] - 95))

                    # Health Bar
                    windowSurface.fill((64, 0, 0), Rect(resolution[0] - 190, resolution[1] - 84, 170, 16))
                    windowSurface.blit(pygame.transform.scale(Cache().getImage("HUD/Health"), (166, 14)), (resolution[0] - 188, resolution[1] - 82), pygame.Rect(0, 0, 166 * to.health.percent, 12))
                    windowSurface.blit(infofont().render(repr(int(100 * to.health.percent)), False, (255, 255, 255)), (resolution[0] - 180, resolution[1] - 83))

                    if trackplayer.lastkilledby != None:
                        windowSurface.blit(font.render("LD: " + trackplayer.lastkilledby, False, (255, 255, 255)), (resolution[0] - 298, resolution[1] - 118))

            if flags["GAME"]:
                game.gui_draw_game_screen_info(windowSurface, flags, trackplayer)

            if tournamentinfo:
                game.gui_draw_tournament_bracket(windowSurface, flags, trackplayer)

            if trackplayer != None:
                n = font.render("Tracking: " + trackplayer.name, False, trackplayer.color)
                windowSurface.blit(n, (resolution[0]/2-n.get_width()/2,resolution[1]-12))                          

            if mousemode == "Destroy":
                ip = bigfont.render("DESTROY OBJECT BY CLICK", False, (255, 0, 0))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
            elif mousemode == "Explode":
                ip = bigfont.render("CLICK TO CAUSE EXPLOSION FORCE", False, (255, 0, 0))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
            elif mousemode != None and mousemode[:3] == "Add":
                x = SPAWN_TYPES.index(mousemode[3:])
                for i in xrange(x - 1, -1, -1):
                    c = max(0, 128 + (i - x) * 16)
                    ip = bigfont.render(SPAWN_TYPES[i], False, (c, c, c))
                    windowSurface.blit(ip, (resolution[0]/2 + 35, resolution[1]/2-24-((i - x) * -36)))

                ip = bigfont.render("Click to Add " + mousemode[3:], False, (255, 255, 0))
                windowSurface.blit(ip, (resolution[0]/2-240, resolution[1]/2-ip.get_height()/2))

                for i in xrange(x + 1, len(SPAWN_TYPES)):
                    c = max(0, 128 - (i - x - 1) * 16)
                    ip = bigfont.render(SPAWN_TYPES[i], False, (c, c, c))
                    windowSurface.blit(ip, (resolution[0]/2 + 35, resolution[1]/2+18+((i - x - 1) * 36)))

            if mousemode == "Move" and trackplayer == None:
                ip = bigfont.render("TRACK OBJECT BEFORE MOVING", False, (255, 255, 255))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
            elif mousemode == "Move":
                ip = bigfont.render("CLICK TO MOVE "+trackplayer.name, False, (255, 255, 255))
                windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))        

            if flags["THREADS"]:
                st = mt+1
                rt = st
                mt = 0
                thrs = threading.enumerate()
                thrs.sort(key=lambda x: x.name, reverse=True)
                for thr in thrs:
                    s = repr(thr)
                    c = (192, 192, 192)
                    ind = 30
                    if "Receiving" in s:
                        i = rt
                        rt += 1
                        c = (192, 192, 255)
                        ind = resolution[0]/2 + 30
                    elif "Sending" in s:
                        i = st
                        st += 1
                    else:
                        i = mt
                        mt += 1
                        c = (255, 192, 192)

                    windowSurface.blit(font.render(repr(thr), False, c), (ind, 64 + 12*i))

            pygame.display.update()
            fpsClock.tick(30) # keep in sync with physics engine?

        #TODO: Add Logging???

        # close out a game 
        game.end()

        logging.info("Closing Pygame...")
        print "Closing Pygame"
        pygame.quit()
    except:
        print "EXCEPTION IN GUI"
        logging.exception("FATAL Error in GUI!!!")
        logging.error(traceback.format_exc())
        print traceback.format_exc()
        world.gameerror = True # Flag for Unit Tests

    logging.info("GUI Closed")
class GameWorld(object):
    """
    represents the virtual space world and the objects in it
    Provides hit detection and radar between objects in the world
    """
    def __init__(self, game, worldsize, objlistener=None):
        """
        Parameters:
            game: game object that manages rules for world
            worldsize: tuple of world width,height
            pys: flag to indicate whether this should be the main world running a physics/game loop or not
            objlistener: initial listner callback function
        """
        self.__game = game
        self.size = intpos(worldsize)
        self.width = worldsize[0]
        self.height = worldsize[1]
        self.__space = pymunk.Space()
        #self.__space.add_collision_handler(0, 0, post_solve=self.collideShipStellarObject)
        self.__space.set_default_collision_handler(
            begin=self.__beginCollideObject,
            post_solve=self.__collideObject,
            separate=self.__endCollideObject)
        self.__objects = ThreadSafeDict()
        self.__addremovesem = threading.Semaphore()
        self.__influential = []
        if objlistener == None:
            self.__objectListener = []
        else:
            self.__objectListener = [objlistener]
        self.__toremove = []
        self.__toadd = []
        self.__active = False
        self.__started = False
        self.gameerror = False
        logging.info("Initialized World of size %s", repr(worldsize))

    def start(self):
        if not self.__started:
            self.__started = True
            self.__active = True
            self.__pys = True
            logging.debug("Started gameloop for World %s", repr(self))
            threading.Thread(None, self.__THREAD__gameloop,
                             "WorldMap_gameloop_" + repr(self)).start()

    def mid_point(self, xoff=0, yoff=0):
        return intpos((self.width / 2 + xoff, self.height / 2 + yoff))

    def endGameLoop(self):
        self.__active = False

    def destroy_all(self):
        self.__pys = False  # disable physics step while we add/remove all objects
        time.sleep(0.2)

        # Remove All Objects
        for obj in self:
            self.pysremove(obj)

        self.__pys = True

    def __beginCollideObject(self, space, arbiter):
        if arbiter.is_first_contact:
            r = self.__game.world_physics_pre_collision(
                arbiter.shapes[0].world_object, arbiter.shapes[1].world_object)
            if r != None:
                if r == False or r == True:
                    return r
                for i in r[1:]:
                    space.add_post_step_callback(i[0], i[1], i[2])
                return r[0]

        return True

    def __collideObject(self, space, arbiter):
        if arbiter.is_first_contact:
            r = self.__game.world_physics_collision(
                arbiter.shapes[0].world_object, arbiter.shapes[1].world_object,
                arbiter.total_impulse.length / 250.0)
            if r != None:
                for i in r:
                    space.add_post_step_callback(i[0], i[1], i[2])
        #eif

    def __endCollideObject(self, space, arbiter):
        # when object is destroyed in callback, arbiter may be empty
        if hasattr(arbiter, "shapes"):
            self.__game.world_physics_end_collision(
                arbiter.shapes[0].world_object, arbiter.shapes[1].world_object)

    def causeExplosion(self, origin, radius, strength, force=False):
        logging.debug("Start Explosion %s, %d, %d [%d]", origin, radius,
                      strength, thread.get_ident())
        origin = pymunk.Vec2d(origin)
        for obj in self.__objects:
            if obj.explodable:
                for point in wrappos(obj.body.position, radius, self.size):
                    if in_circle(origin, radius, point):
                        logging.debug(
                            "Applying Explosion Force of strength %d impulse %s to #%d",
                            strength, repr(
                                (point - origin) * strength), obj.id)
                        obj.body.apply_impulse((point - origin) * strength,
                                               (0, 0))
                        break
        logging.debug("Done Explosion")

    def registerObjectListener(self, callback):
        if callback not in self.__objectListener:
            self.__objectListener.append(callback)

    def __len__(self):
        return len(self.__objects)

    def __iter__(self):
        return self.__objects.__iter__()

    def __getitem__(self, key):
        return self.__objects.__getitem__(key)

    def __setitem__(self, key, value):
        return self.__objects.__setitem__(key, value)

    def has_key(self, key):
        return self.__objects.has_key(key)

    def append(self, item, pys=False):
        """
        Call to add an object to the game world from threads outside the game loop
        """
        logging.debug("Append Object to World %s [%d]", repr(item),
                      thread.get_ident())
        for objListener in self.__objectListener:
            objListener(item, True)

        logging.debug("SEMAPHORE ACQ append [%d]", thread.get_ident())
        self.__addremovesem.acquire()
        if not self.__objects.has_key(item.id):
            self.__objects[item.id] = item
            if pys:
                item.addToSpace(self.__space)
            elif item in self.__toremove:
                self.__toremove.remove(item)
            else:
                self.__toadd.append(item)

            if isinstance(item, Influential) and item.influence_range > 0:
                self.__influential.append(item)
        self.__addremovesem.release()
        logging.debug("SEMAPHORE REL append [%d]", thread.get_ident())

    def remove(self, item):
        """
        Call to remove an object from the game world
        """
        del self[item]

    def pysremove(self, item):
        self.__delitem__(item, True)

    def __delitem__(self, key, pys=False):
        logging.debug("Removing Object from World %s [%d]", repr(key),
                      thread.get_ident())
        # Notify each item this may be in that it's no longer colliding
        # HACK: Get around issue with PyMunk not telling us shapes when object removed already before separate callback
        if len(key.in_celestialbody) > 0:
            for item in key.in_celestialbody[:]:
                item.collide_end(key)

        logging.debug("SEMAPHORE ACQ delitem [%d]", thread.get_ident())
        self.__addremovesem.acquire()
        if self.__objects.has_key(key.id):
            if pys:
                key.removeFromSpace(self.__space)
            elif key in self.__toadd:
                self.__toadd.remove(key)
            else:
                self.__toremove.append(key)

            del self.__objects[key.id]
            if key in self.__influential:
                self.__influential.remove(key)
        self.__addremovesem.release()
        logging.debug("SEMAPHORE REL delitem [%d]", thread.get_ident())

        # Notify after removed, in case re-add same object
        for objListener in self.__objectListener:
            objListener(key, False)

    def __THREAD__gameloop(self):
        lasttime = MINIMUM_GAMESTEP_TIME
        try:
            while self.__active:
                if self.__pys:
                    self.__space.step(
                        MINIMUM_GAMESTEP_TIME)  # Advance Physics Engine

                tstamp = time.time()

                # update all game objects
                for obj in self:  # self is dictionary
                    # Wrap Object in World
                    if obj.body.position[0] < 0:
                        obj.body.position[0] += self.width
                    elif obj.body.position[0] > self.width:
                        obj.body.position[0] -= self.width
                    if obj.body.position[1] < 0:
                        obj.body.position[1] += self.height
                    elif obj.body.position[1] > self.height:
                        obj.body.position[1] -= self.height

                    # Apply Gravity for Planets
                    if obj.gravitable:
                        for influencer in self.__influential:
                            for point in wrappos(obj.body.position,
                                                 influencer.influence_range,
                                                 self.size):
                                if in_circle(influencer.body.position,
                                             influencer.influence_range,
                                             point):
                                    influencer.apply_influence(
                                        obj, point, lasttime)
                                    break

                    # Update and Run Commands
                    obj.update(lasttime)

                    if obj.has_expired() or obj.destroyed:
                        del self[obj]
                #next

                # game time notification
                self.__game.game_update(lasttime)

                # update time
                lasttime = time.time() - tstamp

                logging.debug("SEMAPHORE ACQ gameloop [%d]",
                              thread.get_ident())
                self.__addremovesem.acquire()
                # NOTE ISSUE #68 - If server is slow, can be adding and removing object in same step... add first, so we can immediately remove instead of crash
                # TODO: just look for common set between two lists and remove... or have each list check the other first before adding to the lists...
                #       probably best to refactor to PyMunk 4.0.0 and be able to have PyMunk handle adding/removing objects during physics step.
                # add objects
                for item in self.__toadd:
                    logging.info("World Adding #%d to Physics Engine %s",
                                 item.id, repr(item))
                    item.addToSpace(self.__space)

                self.__toadd = []

                #remove objects
                for item in self.__toremove:
                    logging.info("World Removing #%d from Physics Engine %s",
                                 item.id, repr(item))
                    item.removeFromSpace(self.__space)

                self.__toremove = []
                self.__addremovesem.release()
                logging.debug("SEMAPHORE REL gameloop [%d]",
                              thread.get_ident())

                # minimum to conserve resources
                if lasttime < MINIMUM_GAMESTEP_TIME:
                    time.sleep(MINIMUM_GAMESTEP_TIME - lasttime)
                    lasttime = MINIMUM_GAMESTEP_TIME
                #else:
                #logg#ging.debug("Can't keep at 50fps at %dfps", 1.0 / lasttime)
                #eif
            #wend
        except:
            print "EXCEPTION IN GAMELOOP"
            logging.exception("FATAL Error in game loop!!!")
            logging.info(traceback.format_exc())
            logging.error(traceback.format_exc())
            print traceback.format_exc()
            self.gameerror = True
        logging.debug("Gameloop Ended")

    def getObjectsInArea(self, pos, radius, radar=False):
        """
        Returns objects within the given radius from the position (even wrapping around edge of world), pass radar = True if for environment
        """
        logging.debug("Get Objects In Area %s %d [%d]", repr(pos), radius,
                      thread.get_ident())
        objList = []
        for obj in self.__objects.values():
            for point in wrappos(obj.body.position, radius, self.size):
                if in_circle(pos, radius, point):
                    objList.append(obj)
                    break
                #eif
            #next
        #next
        return objList

    def get_count_of_objects(self, type):
        tname = type.__name__
        count = 0
        for obj in self.__objects.values():
            if obj.__class__.__name__ == tname:
                count += 1
            #eif
        #next
        return count

    def getObjectData(self, obj, player):
        #TODO: Move these properties to the 'getExtraInfo' of the base Entity and have child classes call super...
        objData = {}
        # Convert Type of Object to String
        objData["TYPE"] = friendly_type(obj)
        objData["ID"] = obj.id
        objData["POSITION"] = intpos(obj.body.position)
        objData["SPEED"] = obj.body.velocity.length
        # TODO: deal with -0.0 case OR match physics library coordinates?
        objData[
            "DIRECTION"] = -obj.body.velocity.angle_degrees % 360  # 30 = -120?, -80 = -10
        #objData["VELOCITYDIRECTION"] = obj.velocity.direction
        objData["MAXSPEED"] = obj.body.velocity_limit
        objData["CURHEALTH"] = obj.health.value
        objData["MAXHEALTH"] = obj.health.maximum
        objData["CURENERGY"] = obj.energy.value
        objData["MAXENERGY"] = obj.energy.maximum
        objData["ENERGYRECHARGERATE"] = obj.energyRechargeRate
        objData["MASS"] = obj.mass
        objData[
            "HITRADIUS"] = obj.radius  #TODO: Move to entites that have this i.e. physicalround?
        objData["TIMEALIVE"] = obj.timealive
        objData["INBODY"] = (len(obj.in_celestialbody) > 0)

        obj.getExtraInfo(objData, player)

        self.__game.game_get_extra_radar_info(obj, objData, player)

        return objData

    def getEnvironment(self, ship, radarlevel=0, target=-1, getMessages=False):
        #TODO: abstract radar to game level?
        radardata = None
        if radarlevel > 0:
            objs = self.getObjectsInArea(ship.body.position, ship.radarRange,
                                         True)
            #TODO: Need to wait lock the removing of ships with accessing...???
            if ship in objs: objs.remove(ship)  # Get rid of self...

            # remove ships with cloak
            for x in xrange(len(objs) - 1, -1, -1):
                if isinstance(objs[x],
                              Ship) and objs[x].commandQueue.containstype(
                                  CloakCommand):
                    del objs[x]
                #eif
            #next

            if radarlevel == 1:
                # number of objects
                radardata = []
                for i in xrange(len(objs)):
                    radardata.append({})
            elif radarlevel == 2:
                # (pos, id) list
                radardata = []
                for e in objs:
                    radardata.append({
                        "POSITION": intpos(e.body.position),
                        "ID": e.id
                    })
                #next
            elif radarlevel == 3:
                for obj in objs:
                    if obj.id == target:
                        radardata = [self.getObjectData(obj, ship.player)]
                        break
            elif radarlevel == 4:
                # (pos, id, type)
                radardata = []
                for e in objs:
                    radardata.append({
                        "POSITION": intpos(e.body.position),
                        "ID": e.id,
                        "TYPE": friendly_type(e)
                    })
                #next
            elif radarlevel == 5:
                radardata = []
                for e in objs:
                    radardata.append(self.getObjectData(e, ship.player))
                #next
            #eif
        #eif

        msgQueue = []
        if getMessages:
            msqQueue = list(ship.messageQueue)

        return {
            "SHIPDATA": self.getObjectData(ship, ship.player),
            "RADARLEVEL": radarlevel,
            "RADARDATA": radardata,
            "GAMEDATA": self.__game.game_get_extra_environment(ship.player),
            "MESSAGES": msgQueue,
        }
Пример #7
0
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

        # 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")     
        
        # TODO: Create Tournament Class
        self._tournament = cfgobj.getboolean("Tournament", "tournament")
        self._tournamentinitialized = False
        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._tournamentnumgroups = cfgobj.getint("Tournament", "tournament_groups")        
        self._tournamentgroups = []
        self._tournamentcurrentgroup = 0
        self._tournamentfinalgroup = []
        self._tournamentfinalround = False
        self._tournamentfinalwinner = None
        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 = self.world_create()
        self.world.registerObjectListener(self.world_add_remove_object)   

        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 = RoundTimer(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.timeLeft

    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:
            logging.info("Starting Game")
            self.__started = True
            self.__autostart = self.__allowafterstart

            if self._tournament:
                logging.info("[Tournament] Round %d of %d", self._tournamentcurrentgroup+1, self._tournamentnumgroups)
                if not self._tournamentinitialized:
                    logging.info("[Tournament] Initialized with %d players", len(self._players))
                    self._tournamentgroups = []
                    for x in xrange(self._tournamentnumgroups):
                        self._tournamentgroups.append([])
                    #next
                    x = 0
                    for player in self._players.values():
                        self._tournamentgroups[x % self._tournamentnumgroups].append(player)
                        x += 1
                    #next               
                    self._tournamentinitialized = True                     
                elif self._tournamentcurrentgroup == self._tournamentnumgroups:
                    logging.info("[Tournament] Final Round")
                    self._tournamentfinalround = True
                #eif
            

                # we'll reset the world here so it's "fresh" and ready as soon as the game starts
                if self.__resetworldround:
                    self._world_reset()
            #eif

            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
        """
        if reason == 1:
            player.score = self._points_initial
            player.bestscore = self._points_initial
            player.deaths = 0

    
    def player_get_start_position(self, force=False):
        """
        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

        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:
            if not self._tournamentfinalround:
                # get winner(s)
                for x in xrange(self.cfg.getint("Tournament", "number_to_final_round")):
                    logging.info("Adding player to final round %s stats: %s", self.__leaderboard_cache[x].name, self.laststats[x])
                    self._player_add_to_final_round(self.__leaderboard_cache[x])
                #next
            else:
                logging.info("Final Round Winner %s stats: %s", self._leader.name, self.laststats[0])
                self._tournamentfinalwinner = self._leader
            #eif
        #eif
        
        for player in self._players:
            player.roundover = True
            if player.object != None:
                #player.object.destroyed = True
                self.world.remove(player.object)

        # 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
            self._tournamentcurrentgroup += 1
            logging.debug("[Tournament] Group Number Now %d", self._tournamentcurrentgroup)
        if not self.__autostart:
            # If not autostart, wait for next round to start
            self.__started = False
        else:
            self.round_start()

    def world_create(self, pys=True):
        """
        Called by constructor to create world and when world reset, defaults to the standard world configuration from the config file definitions.
        """
        return ConfiguredWorld(self, self.cfg, pys)
    
    def _world_reset(self):
        """
        Recreates a world, called when the reset_world_each_round property is set.
        """
        self.world.newWorld(self.world_create(False))

    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, None)
                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 _game_add_ship_for_player(self, netid, force=False, 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(True)

            self._players[netid].object.ship_added() # tell AI ship to start
        else:
            self._players[netid].object = Ship(self.player_get_start_position(True), self.world)
        logging.info("Adding Ship for Player %d (%s) id #%d with Name %s", netid, repr(force), 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}

    
    def game_get_extra_radar_info(self, obj, objdata):
        """
        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_update_score(self, player, amount):
        """
        Should be called to manipulate a player's score, will do extra bookkeeping and sanity for you.
        """
        player.score += amount

        # scores can't be negative
        if player.score < 0:
            player.score = 0

        # TODO: should probably check if primary highest flag to see if we want to keep track of lowest or highest score here
        # update if this is a new personal best
        if player.score > player.bestscore:
            player.bestscore = player.score
        
    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
        """
        player.deaths += 1
        if self._points_lost_on_death > 0:
            self.player_update_score(player, -self._points_lost_on_death)

        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 _player_add_to_final_round(self, player):
        """
        Add the player to the final round.  Will be automatically called by the default implementation for round_over.
        """
        self._tournamentfinalgroup.append(player)

    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:
            if self._tournamentfinalround:
                return self._tournamentfinalgroup
            else:
                if self._tournamentcurrentgroup < len(self._tournamentgroups):
                    return self._tournamentgroups[self._tournamentcurrentgroup]
                else:
                    return []
            #eif
        #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, 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

            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, True)
    
    def world_physics_pre_collision(self, shapes):
        """
        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

        The default game prevents anything from colliding with BlackHole or Nebula.

        shapes is a list of pymunk Shape classes, but you can look at it's id or world_object properties to get useful information.
        """
        for shape in shapes:
            if isinstance(shape.world_object, BlackHole) or isinstance(shape.world_object, Nebula):
                return [ False, [] ]

    def world_physics_collision(self, shapes, damage):
        """
        Called by the physics engine when two objects collide

        Return [[func, obj, para...]...]

        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.
        """
        r = []
        for shape in shapes:
            #print shape.id, ":",
            gobj = shape.world_object
            if isinstance(gobj, Ship) and gobj.commandQueue.containstype(RaiseShieldsCommand) and gobj.shield.value > 0:
                gobj.shield -= damage * gobj.shieldDamageReduction
                gobj.health -= damage * (1.0 - gobj.shieldDamageReduction)
            else:
                logging.debug("Object #%d took %d damage", gobj.id, damage)
                gobj.health -= damage
            #eif

            if gobj.health.maximum > 0 and gobj.health.value <= 0:
                gobj.killedby = None
                if len(shapes) == 2:
                    for s2 in shapes:
                        if s2 != shape:
                            gobj.killedby = self.world[s2.id]
                            break

                logging.info("Object #%d destroyed by %s", gobj.id, repr(gobj.killedby))
                r.append([self.world_physics_post_collision, gobj, damage])
                
                if isinstance(gobj, Ship):
                    gobj.player.sound = "EXPLODE"
            elif isinstance(gobj, Ship):
                gobj.player.sound = "HIT"
            #eif
        if r == []: return None
        return r

    def world_physics_post_collision(self, dobj, para):
        """
        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 = para[0] / 75.0
        logging.info("Destroying Object: #%d, Force: %d [%d]", dobj.id, strength, thread.get_ident())
        dobj.destroyed = True

        self.world.causeExplosion(dobj.body.position, dobj.radius * 5, strength / 75.0, True) #Force in physics step

        self.world.remove(dobj)

    #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._tfont = pygame.font.Font("freesansbold.ttf", 18)
        self._dfont = debugfont()

    def gui_draw_game_world_info(self, surface, flags):
        """
        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):
        """
        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):
        """
        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._tournamentinitialized:
            # draw first Bracket
            y = 100
            for x in xrange(self._tournamentnumgroups):
                c = (128, 128, 128)
                if x == self._tournamentcurrentgroup:
                    c = (255, 255, 255)
                py = y
                for player in self._tournamentgroups[x]:
                    screen.blit(self._tfont.render(player.name, False, c), (100, y))
                    y += 24
                                
                # draw bracket lines
                pygame.draw.line(screen, (192, 192, 192), (400, py), (410, py))
                pygame.draw.line(screen, (192, 192, 192), (410, py), (410, y))
                pygame.draw.line(screen, (192, 192, 192), (400, y), (410, y))
                pygame.draw.line(screen, (192, 192, 192), (410, py + (y - py) / 2), (410, py + (y - py) / 2))
                                
                y += 36

            # draw Final Bracket
            y = 96 + ((y - 96) / 2) - len(self._tournamentfinalgroup) * 16
            py = y
            for player in self._tournamentfinalgroup:
                screen.blit(self._tfont.render(player.name, False, (255, 255, 128)), (435, y))
                y += 24
            pygame.draw.line(screen, (192, 192, 192), (800, py), (810, py))
            pygame.draw.line(screen, (192, 192, 192), (810, py), (810, y))
            pygame.draw.line(screen, (192, 192, 192), (800, y), (810, y))

            if self._tournamentfinalwinner:
                screen.blit(self._tfont.render(self._tournamentfinalwinner.name, False, (128, 255, 255)), (835, py + (y - py) / 2))
Пример #8
0
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)
Пример #9
0
class GameWorld(object):
    """
    represents the virtual space world and the objects in it
    Provides hit detection and radar between objects in the world
    """
    def __init__(self, game, worldsize, objlistener=None):
        """
        Parameters:
            game: game object that manages rules for world
            worldsize: tuple of world width,height
            pys: flag to indicate whether this should be the main world running a physics/game loop or not
            objlistener: initial listner callback function
        """
        self.__game = game
        self.size = intpos(worldsize)
        self.width = worldsize[0]
        self.height = worldsize[1]
        self.__space = pymunk.Space()
        #self.__space.add_collision_handler(0, 0, post_solve=self.collideShipStellarObject)
        self.__space.set_default_collision_handler(begin=self.__beginCollideObject, post_solve=self.__collideObject, separate=self.__endCollideObject)
        self.__objects = ThreadSafeDict()
        self.__addremovesem = threading.Semaphore()
        self.__influential = []
        if objlistener == None:
            self.__objectListener = []
        else:
            self.__objectListener = [objlistener]
        self.__toremove = []
        self.__toadd = []        
        self.__active = False
        self.__started = False
        self.gameerror = False
        logging.info("Initialized World of size %s", repr(worldsize))

    def start(self):
        if not self.__started:
            self.__started = True
            self.__active = True
            self.__pys = True
            logging.debug("Started gameloop for World %s", repr(self))
            threading.Thread(None, self.__THREAD__gameloop, "WorldMap_gameloop_" + repr(self)).start()

    def mid_point(self, xoff = 0, yoff = 0):
        return intpos((self.width / 2 + xoff, self.height / 2 + yoff))

    def endGameLoop(self):
        self.__active = False
            
    def destroy_all(self):
        self.__pys = False # disable physics step while we add/remove all objects
        time.sleep(0.2)
        
        # Remove All Objects
        for obj in self:
            self.pysremove(obj)
        
        self.__pys = True
        
    def __beginCollideObject(self, space, arbiter):
        if arbiter.is_first_contact:
            r = self.__game.world_physics_pre_collision( arbiter.shapes[0].world_object, arbiter.shapes[1].world_object )
            if r != None:
                if r == False or r == True:
                    return r
                for i in r[1:]:
                    space.add_post_step_callback(i[0], i[1], i[2])
                return r[0]

        return True       
        
    def __collideObject(self, space, arbiter):        
        if arbiter.is_first_contact:
            r = self.__game.world_physics_collision( arbiter.shapes[0].world_object, arbiter.shapes[1].world_object, arbiter.total_impulse.length / 250.0 )
            if r != None:
                for i in r:
                    space.add_post_step_callback(i[0], i[1], i[2])
        #eif

    def __endCollideObject(self, space, arbiter):
        # when object is destroyed in callback, arbiter may be empty
        if hasattr(arbiter, "shapes"):            
            self.__game.world_physics_end_collision( arbiter.shapes[0].world_object, arbiter.shapes[1].world_object )
        
    def causeExplosion(self, origin, radius, strength, force=False):
        logging.debug("Start Explosion %s, %d, %d [%d]", origin, radius, strength, thread.get_ident())
        origin = pymunk.Vec2d(origin)
        for obj in self.__objects:
            if obj.explodable:
                for point in wrappos(obj.body.position, radius, self.size):
                    if in_circle(origin, radius, point):                        
                        logging.debug("Applying Explosion Force of strength %d impulse %s to #%d", strength, repr((point - origin) * strength), obj.id)
                        obj.body.apply_impulse((point - origin) * strength, (0,0))
                        break        
        logging.debug("Done Explosion") 
        
    def registerObjectListener(self, callback):
        if callback not in self.__objectListener:
            self.__objectListener.append(callback)

    def __len__(self):
        return len(self.__objects)

    def __iter__(self):
        return self.__objects.__iter__()

    def __getitem__(self, key):
        return self.__objects.__getitem__(key)

    def __setitem__(self, key, value):
        return self.__objects.__setitem__(key, value)

    def has_key(self, key):
        return self.__objects.has_key(key)

    def append(self, item, pys=False):
        """
        Call to add an object to the game world from threads outside the game loop
        """
        logging.debug("Append Object to World %s [%d]", repr(item), thread.get_ident())
        for objListener in self.__objectListener:
            objListener(item, True)

        logging.debug("SEMAPHORE ACQ append [%d]", thread.get_ident())
        self.__addremovesem.acquire()
        if not self.__objects.has_key(item.id):
            self.__objects[item.id] = item
            if pys:
                item.addToSpace(self.__space)
            elif item in self.__toremove:
                self.__toremove.remove(item)
            else:
                self.__toadd.append(item)

            if isinstance(item, Influential) and item.influence_range > 0:
                self.__influential.append(item)
        self.__addremovesem.release()
        logging.debug("SEMAPHORE REL append [%d]", thread.get_ident())                

    def remove(self, item):
        """
        Call to remove an object from the game world
        """
        del self[item]

    def pysremove(self, item):
        self.__delitem__(item, True)
        
    def __delitem__(self, key, pys=False):
        logging.debug("Removing Object from World %s [%d]", repr(key), thread.get_ident())
        # Notify each item this may be in that it's no longer colliding
        # HACK: Get around issue with PyMunk not telling us shapes when object removed already before separate callback
        if len(key.in_celestialbody) > 0:
            for item in key.in_celestialbody[:]:
                item.collide_end(key)

        logging.debug("SEMAPHORE ACQ delitem [%d]", thread.get_ident())
        self.__addremovesem.acquire()            
        if self.__objects.has_key(key.id):
            if pys:
                key.removeFromSpace(self.__space)
            elif key in self.__toadd:
                self.__toadd.remove(key)
            else:
                self.__toremove.append(key)
            
            del self.__objects[key.id]
            if key in self.__influential:
                self.__influential.remove(key)
        self.__addremovesem.release()           
        logging.debug("SEMAPHORE REL delitem [%d]", thread.get_ident())
        
        # Notify after removed, in case re-add same object
        for objListener in self.__objectListener:
            objListener(key, False)

    def __THREAD__gameloop(self):
        lasttime = MINIMUM_GAMESTEP_TIME
        try:
            while self.__active:
                if self.__pys:
                    self.__space.step(MINIMUM_GAMESTEP_TIME) # Advance Physics Engine
                
                tstamp = time.time()

                # update all game objects
                for obj in self: # self is dictionary
                    # Wrap Object in World
                    if obj.body.position[0] < 0:
                        obj.body.position[0] += self.width
                    elif obj.body.position[0] > self.width:
                        obj.body.position[0] -= self.width
                    if obj.body.position[1] < 0:
                        obj.body.position[1] += self.height
                    elif obj.body.position[1] > self.height:
                        obj.body.position[1] -= self.height

                    # Apply Gravity for Planets
                    if obj.gravitable:
                        for influencer in self.__influential:
                            for point in wrappos(obj.body.position, influencer.influence_range, self.size):
                                if in_circle(influencer.body.position, influencer.influence_range, point):
                                    influencer.apply_influence(obj, point, lasttime)
                                    break
                    
                    # Update and Run Commands
                    obj.update(lasttime)

                    if obj.has_expired() or obj.destroyed:
                        del self[obj]
                #next            

                # game time notification
                self.__game.game_update(lasttime)

                # update time
                lasttime = time.time() - tstamp
                           
                logging.debug("SEMAPHORE ACQ gameloop [%d]", thread.get_ident())
                self.__addremovesem.acquire()
                # NOTE ISSUE #68 - If server is slow, can be adding and removing object in same step... add first, so we can immediately remove instead of crash
                # TODO: just look for common set between two lists and remove... or have each list check the other first before adding to the lists...
                #       probably best to refactor to PyMunk 4.0.0 and be able to have PyMunk handle adding/removing objects during physics step.
                # add objects
                for item in self.__toadd:
                    logging.info("World Adding #%d to Physics Engine %s", item.id, repr(item))
                    item.addToSpace(self.__space)

                self.__toadd = []

                #remove objects
                for item in self.__toremove:
                    logging.info("World Removing #%d from Physics Engine %s", item.id, repr(item))
                    item.removeFromSpace(self.__space)
                    
                self.__toremove = []
                self.__addremovesem.release()
                logging.debug("SEMAPHORE REL gameloop [%d]", thread.get_ident())
                    
                # minimum to conserve resources
                if lasttime < MINIMUM_GAMESTEP_TIME:
                    time.sleep(MINIMUM_GAMESTEP_TIME - lasttime)
                    lasttime = MINIMUM_GAMESTEP_TIME
                #else:
                    #logg#ging.debug("Can't keep at 50fps at %dfps", 1.0 / lasttime)
                #eif
            #wend
        except:
            print "EXCEPTION IN GAMELOOP"
            logging.exception("FATAL Error in game loop!!!")
            logging.info(traceback.format_exc())
            logging.error(traceback.format_exc())
            print traceback.format_exc()
            self.gameerror = True
        logging.debug("Gameloop Ended")

    def getObjectsInArea(self, pos, radius, radar=False):
        """
        Returns objects within the given radius from the position (even wrapping around edge of world), pass radar = True if for environment
        """
        logging.debug("Get Objects In Area %s %d [%d]", repr(pos), radius, thread.get_ident())
        objList = []
        for obj in self.__objects.values():
            for point in wrappos(obj.body.position, radius, self.size):
                if in_circle(pos, radius, point):
                    objList.append(obj)
                    break
                #eif
            #next
        #next
        return objList

    def get_count_of_objects(self, type):
        tname = type.__name__
        count = 0
        for obj in self.__objects.values():
            if obj.__class__.__name__ == tname:
                count += 1
            #eif
        #next
        return count

    def getObjectData(self, obj, player):
        #TODO: Move these properties to the 'getExtraInfo' of the base Entity and have child classes call super...
        objData = {}
        # Convert Type of Object to String
        objData["TYPE"] = friendly_type(obj)
        objData["ID"] = obj.id
        objData["POSITION"] = intpos(obj.body.position)
        objData["SPEED"] = obj.body.velocity.length
        # TODO: deal with -0.0 case OR match physics library coordinates?
        objData["DIRECTION"] = -obj.body.velocity.angle_degrees % 360 # 30 = -120?, -80 = -10
        #objData["VELOCITYDIRECTION"] = obj.velocity.direction
        objData["MAXSPEED"] = obj.body.velocity_limit
        objData["CURHEALTH"] = obj.health.value
        objData["MAXHEALTH"] = obj.health.maximum
        objData["CURENERGY"] = obj.energy.value
        objData["MAXENERGY"] = obj.energy.maximum
        objData["ENERGYRECHARGERATE"] = obj.energyRechargeRate
        objData["MASS"] = obj.mass
        objData["HITRADIUS"] = obj.radius #TODO: Move to entites that have this i.e. physicalround?
        objData["TIMEALIVE"] = obj.timealive
        objData["INBODY"] = (len(obj.in_celestialbody) > 0)

        obj.getExtraInfo(objData, player)

        self.__game.game_get_extra_radar_info(obj, objData, player)

        return objData

    def getEnvironment(self, ship, radarlevel=0, target=-1, getMessages=False):
        #TODO: abstract radar to game level?
        radardata = None
        if radarlevel > 0:
            objs = self.getObjectsInArea(ship.body.position, ship.radarRange, True)
            #TODO: Need to wait lock the removing of ships with accessing...???
            if ship in objs: objs.remove(ship) # Get rid of self...

            # remove ships with cloak or in Nebula
            # TODO: Should there be an 'invisible' flag?  Would need more testing.  Would a degradation/range be useful?
            for x in xrange(len(objs) - 1, -1, -1):
                if isinstance(objs[x], Ship) and (objs[x].commandQueue.containstype(CloakCommand) \
                                             or any(isinstance(y, Nebula) for y in objs[x].in_celestialbody)):
                    del objs[x]
                #eif
            #next

            if radarlevel == 1:
                # number of objects
                radardata = []
                for i in xrange(len(objs)):
                    radardata.append({})
            elif radarlevel == 2:
                # (pos, id) list
                radardata = []
                for e in objs:
                    radardata.append({"POSITION":intpos(e.body.position), "ID":e.id})
                #next
            elif radarlevel == 3:
                for obj in objs:
                    if obj.id == target:
                        radardata = [self.getObjectData(obj, ship.player)]
                        break
            elif radarlevel == 4:
                # (pos, id, type)
                radardata = []
                for e in objs:
                    radardata.append({"POSITION":intpos(e.body.position), "ID":e.id, "TYPE":friendly_type(e)})
                #next
            elif radarlevel == 5:
                radardata = []
                for e in objs:
                    radardata.append(self.getObjectData(e, ship.player))
                #next
            #eif
        #eif

        msgQueue = []
        if getMessages:
            msqQueue = list(ship.messageQueue)

        return {"SHIPDATA": self.getObjectData(ship, ship.player),
                "RADARLEVEL": radarlevel,
                "RADARDATA": radardata,
                "GAMEDATA": self.__game.game_get_extra_environment(ship.player),
                "MESSAGES": msgQueue,
                }
Пример #10
0
def startGame(windowcaption, game, fullscreen=True, resolution=None, showstats=False, sound=False, testcase=None):    
    #region Initialization
    logging.info("Initiating PyGame...")

    world = game.world
    pygame.init()
    fpsClock = pygame.time.Clock()

    logging.debug("Initiating Screen...")

    if sound:
        logging.info("Starting Sound...")
        pygame.mixer.init()
        SCache(True)
    else:
        logging.info("Sound Disabled")
        SCache(False)
    
    ipaddress = getIPAddress()

    if resolution == None:
        resolution = pygame.display.list_modes()[0]

    windowSurface = pygame.display.set_mode(resolution, (pygame.FULLSCREEN * fullscreen) | pygame.HWSURFACE | pygame.DOUBLEBUF)
    pygame.display.set_caption(windowcaption)

    logging.debug("Game GUI CFG...")
    # Let game initialize any GUI objects independently
    game.gui_initialize()

    colorBlack = pygame.Color(0, 0, 0)

    #shipw = ShipGUI(Ship((100, 100)))
    #shipw.ship.velocity.magnitude = 5

    #region World Registration
    bgobjects = ThreadSafeDict() # items we want always in the background
    objects = ThreadSafeDict()
    shipids = []
    trackshipid = None
    def addorremove(obj, added):
        logging.debug("GUI: Add/Remove Obj %s (%s) [%d]", repr(obj), repr(added), thread.get_ident())
        try:
            if added:
                # TODO: #18 - Clean-up this by auto creating wrapper on name...
                if isinstance(obj, Ship):
                    logging.debug("GUI: Adding Ship #%d", obj.id)
                    objects[obj.id] = ShipGUI(obj, world)
                    shipids.append(obj.id)
                    logging.debug("GUI: Added Ship #%d", obj.id)
                elif isinstance(obj, Nebula):
                    logging.debug("GUI: Adding Nebula #%d", obj.id)
                    bgobjects[obj.id] = NebulaGUI(obj, world)
                    logging.debug("GUI: Added Nebula #%d", obj.id)
                elif isinstance(obj, Planet):
                    logging.debug("GUI: Adding Planet #%d", obj.id)
                    bgobjects[obj.id] = PlanetGUI(obj, world)
                    logging.debug("GUI: Added Planet #%d", obj.id)
                elif isinstance(obj, Asteroid):
                    logging.debug("GUI: Adding Asteroid #%d", obj.id)
                    objects[obj.id] = AsteroidGUI(obj, world)
                    logging.debug("GUI: Added Asteroid #%d", obj.id)
                elif isinstance(obj, Torpedo):
                    logging.debug("GUI: Adding Torpedo #%d", obj.id)
                    objects[obj.id] = TorpedoGUI(obj, world)
                    logging.debug("GUI: Added Torpedo #%d", obj.id)
                else:
                    logging.debug("GUI: Adding %s #%d", repr(obj), obj.id)
                    objects[obj.id] = obj.WRAPPERCLASS(obj, world)
                    logging.debug("GUI: Added %s %%d", repr(obj), obj.id)
                #eif
            else:
                if isinstance(obj, Ship):
                    logging.debug("GUI: Ship #%d Dying", obj.id)
                    ship = objects.get(obj.id, None)
                    if ship != None:
                        ship.dying = True
                        logging.debug("GUI: Ship #%d Set Dying", obj.id)
                elif obj.id in objects:
                    logging.debug("GUI: Removing Object #%d", obj.id)
                    del objects[obj.id]
                    logging.debug("GUI: Removed Object #%d", obj.id)
                elif obj.id in bgobjects:
                    logging.debug("GUI: Removing BG Object #%d", obj.id)
                    del bgobjects[obj.id]
                    logging.debug("GUI: Removed BG Object #%d", obj.id)
                #eif
            #eif
        except:
            logging.error("GUI ERROR")
            logging.error(traceback.format_exc())
            print traceback.format_exc()
        #logging.debug("GUI: Add/Remove Done [%d]", thread.get_ident())
    #endregion

    logging.debug("Register World Listener...")
    world.registerObjectListener(addorremove)

    logging.debug("Add Objects to GUI...")
    # Create objects for all items in world
    for obj in world:
        addorremove(obj, True)
    #efor

    # Create Stars
    #stars = []
    #for i in xrange((world.width / STAR_DENSITY * world.height / STAR_DENSITY)):
    #    stars.append(Star((random.randint(4, world.width - 4), random.randint(4, world.height - 4))))

    logging.debug("Create Fonts...")
    # Start Main Loop
    font = pygame.font.Font("freesansbold.ttf", 12)
    bigfont = pygame.font.Font("freesansbold.ttf", 44)

    logging.debug("Load Textures...")
    spacetexture = Cache().getImage("Backgrounds/space")
    scalefactorx = float(world.size[0]) / resolution[0]
    scalefactory = float(world.size[1]) / resolution[1]
    offsetx = 0
    maxoffsetx = -world.width + resolution[0]
    offsety = 0
    maxoffsety = -world.height + resolution[1]

    def centerViewOnWorld(pos):
        return -pos[0] + resolution[0]/2, -pos[1] + resolution[1]/2
    
    #keyboard repeat rate
    pygame.key.set_repeat(75, 25)

    zoomout = True
    mouselock = True
    MOUSELOCK_RANGE = resolution[0] / 8
    logintercept = False
    mlh = MessageLogHandler(50)
    
    dynamiccamera = False

    mousemode = None
    prevzoom = zoomout
    showip = showstats
    showplayerlist = showstats
    showroundtime = game.cfg.getboolean("Tournament", "tournament")
    tournamentinfo = game.cfg.getboolean("Tournament", "tournament")
    
    flags = {"DEBUG":False,
             "STATS":showstats,
             "NAMES":True,
             "GAME":showstats}
    
    logging.info("Create Main Surface...")
    #TODO: Optimize by only drawing objects in viewport...
    worldsurface = pygame.Surface((world.size[0], world.size[1]), flags=HWSURFACE)
    
    logging.debug("Game Start!")
    obj = None
    
    notexit = True

    # In test case we only care about test being done
    if testcase != None:
        notexit = False

    #endregion

    while notexit or (testcase != None and not testcase.donetest):
        t = pygame.time.get_ticks() / 1000.0

        #for star in stars:
        #    star.draw(windowSurface)

        # TODO: figure out abstraction for pygame or method in order to get objwrapper to figure out if it needs
        # to draw in the screen or be no-op, ideally objwrapper just does what it does now...

        worldsurface.fill(colorBlack)
                
        #for x in xrange(offsetx, resolution[0], 512):
        #    for y in xrange(offsety, resolution[1], 512):
        #        worldsurface.blit(spacetexture, (x, y))

        # draw background items first in specific z-order
        for obj in sorted(bgobjects.values(), key=attrgetter('zorder')):
            obj.draw(worldsurface, flags)
            if obj.dead:
                del bgobjects[obj._worldobj.id]
       
        # draw active objects
        for obj in objects:
            obj.draw(worldsurface, flags)
            if hasattr(obj._worldobj, "player") and obj._worldobj.player.sound != None:
                #print "Trying to play sound", obj._worldobj.player.sound
                SCache().play(obj._worldobj.player.sound)
                obj._worldobj.player.sound = None
            if obj.dead:
                del objects[obj._worldobj.id]
                if isinstance(obj, ShipGUI):
                    shipids.remove(obj._worldobj.id)
                            
        if flags["GAME"]:
            game.gui_draw_game_world_info(worldsurface, flags)

        #region Key/Mouse Event Handling
        for event in pygame.event.get():
            if event.type == QUIT:
                notexit = False
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    pygame.event.post(pygame.event.Event(QUIT))
                elif event.key == K_SPACE and not game.round_get_has_started():
                    game.round_start()
                elif event.key == K_d:
                    flags["DEBUG"] = not flags["DEBUG"]
                elif event.key == K_i:
                    showip = not showip
                elif event.key == K_p:
                    showplayerlist = (showplayerlist + 1) % (2 + game.tournament_is_running())
                elif event.key == K_s:
                    flags["STATS"] = not flags["STATS"]
                elif event.key == K_n:
                    flags["NAMES"] = not flags["NAMES"]
                elif event.key == K_UP:
                    offsety += 16
                elif event.key == K_DOWN:
                    offsety -= 16
                elif event.key == K_RIGHT:
                    offsetx -= 16
                elif event.key == K_LEFT:
                    offsetx += 16
                elif event.key == K_g:
                    flags["GAME"] = not flags["GAME"]
                elif event.key == K_t and game.tournament_is_running():
                    tournamentinfo = not tournamentinfo
                elif event.key == K_z:
                    zoomout = not zoomout
                elif event.key == K_m:
                    mouselock = not mouselock
                elif event.key == K_PAGEUP and len(shipids) > 0:
                    if trackshipid == None: 
                        trackshipid = shipids[0]
                    else:
                        trackshipid = shipids[shipids.index(trackshipid) - 1]
                elif event.key == K_PAGEDOWN and len(shipids) > 0:
                    if trackshipid == None: 
                        trackshipid = shipids[-1]
                    else:
                        trackshipid = shipids[(shipids.index(trackshipid) + 1) % len(shipids)]
                elif event.key == K_END:
                    trackshipid = None
                    mlh.filter = None
                elif event.key == K_y:
                    dynamiccamera = not dynamiccamera
                    if not dynamiccamera:
                        trackshipid = None
                elif event.key == K_l:
                    logintercept = not logintercept
                    logger = logging.getLogger()
                    if logintercept:                        
                        logger.addHandler(mlh)
                    else:
                        logger.removeHandler(mlh)
                        mlh.messages.clear()
                elif event.key == K_a:
                    if mousemode not in ("AddAsteroid", "AddPlanet", "AddBlackHole", "AddNebula", "AddBubble"):
                        mousemode = "AddAsteroid"
                    elif mousemode == "AddAsteroid":
                        mousemode = "AddPlanet"
                    elif mousemode == "AddPlanet":
                        mousemode = "AddBlackHole"
                    elif mousemode == "AddBlackHole":
                        mousemode = "AddNebula"
                    elif mousemode == "AddNebula" and isinstance(game, KingOfTheBubbleGame):
                        mousemode = "AddBubble"
                    else:
                        mousemode = None
                elif event.key == K_k:
                    if mousemode == "Destroy":
                        mousemode = None
                    else:
                        mousemode = "Destroy"
                elif event.key == K_e:
                    if mousemode == "Explode":
                        mousemode = None
                    else:
                        mousemode = "Explode"                    
                elif event.key == K_v:
                    if mousemode == "Move":
                        mousemode = None
                        zoomout = prevzoom
                    else:
                        mousemode = "Move"
                        prevzoom = zoomout
                        zoomout = True
                elif event.key == K_r:
                    showroundtime = not showroundtime
            elif event.type == MOUSEBUTTONDOWN:                
                if zoomout:
                    x = event.pos[0]*scalefactorx
                    y = event.pos[1]*scalefactory
                else:
                    x, y = event.pos[0]-offsetx, event.pos[1]-offsety
                #print zoomout, x, y

                if mousemode == "Move" and trackshipid != None:
                    mousemode = None
                    zoomout = prevzoom
                    objects[trackshipid]._worldobj.body.position = (x, y)
                elif mousemode == "Destroy":
                    mousemode = None                
                    v = Vec2d(x, y)
                    for lst in objects, bgobjects:
                        for obj in lst:
                            if math.sqrt(obj._worldobj.body.position.get_dist_sqrd(v)) <= 32:
                                logging.info("[GUI] Destroying Object #%d", obj._worldobj.id)
                                if isinstance(obj, ShipGUI):
                                    obj._worldobj.killed = True
                                world.remove(obj._worldobj)
                                break
                elif mousemode == "Explode":
                    mousemode = None                
                    world.causeExplosion((x, y), 256, 1000)
                elif mousemode == "AddAsteroid":
                    mousemode = None
                    world.append(Asteroid((x, y)))
                elif mousemode == "AddPlanet":
                    mousemode = None
                    world.append(Planet((x, y)))
                elif mousemode == "AddBlackHole":
                    mousemode = None
                    world.append(BlackHole((x, y)))
                elif mousemode == "AddNebula":
                    mousemode = None
                    world.append(Nebula((x, y), (512, 128)))
                elif mousemode == "AddBubble":
                    mousemode = None
                    game.addBubbles(world, 1, pos=(x, y))
                elif zoomout and event.button == 1:
                    # zoom in
                    zoomout = False
                    #xoffset = -(((event.pos[0] - resolution[0]/2) % 16)*16 * scalefactorx)
                    #yoffset = -(((event.pos[1] - resolution[1]/2) % 16)*16 * scalefactory)
                    offsetx, offsety = centerViewOnWorld((event.pos[0]*scalefactorx, event.pos[1]*scalefactory))
                else:
                    # recenter on new click
                    offsetx, offsety = centerViewOnWorld((event.pos[0]*scalefactorx, event.pos[1]*scalefactory))
                    
        #endregion 

        # Tracks a ship on screen
        if len(shipids) == 0 or trackshipid not in objects: 
            trackshipid = None
        
        if dynamiccamera:
            trackshipid = game.game_get_current_leader_list()[0].id
            
        if trackshipid != None:
            mlh.filter = "#" + repr(trackshipid)
            offsetx, offsety = centerViewOnWorld(objects[trackshipid]._worldobj.body.position)

        # Pans world with mouse
        if not mouselock:
            # TODO Edge Detect
            pos = pygame.mouse.get_pos()
            if pos[0] < MOUSELOCK_RANGE:
                offsetx += 48
            elif pos[0] > resolution[0] - MOUSELOCK_RANGE:
                offsetx -= 48

            if pos[1] < MOUSELOCK_RANGE:
                offsety += 48
            elif pos[1] > resolution[1] - MOUSELOCK_RANGE:
                offsety -= 48
        #eif        

        if offsetx > 0: offsetx = 0
        if offsety > 0: offsety = 0
        if offsetx < maxoffsetx: offsetx = maxoffsetx
        if offsety < maxoffsety: offsety = maxoffsety

        if zoomout:
            pygame.transform.scale(worldsurface, resolution, windowSurface)
        else:
            windowSurface.blit(worldsurface, (offsetx, offsety)) 

        if flags["DEBUG"] or showip:
            ip = bigfont.render(ipaddress, False, (255, 255, 255))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,0))

        if not game.round_get_has_started():
            ip = bigfont.render("Waiting For Start - Press Space", False, (255, 255, 255))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,resolution[1]/2-ip.get_height()/2))
            
            if game.laststats != None:
                x = 0
                for i in game.laststats:
                    windowSurface.blit(font.render(i, False, (192, 192, 255)), (resolution[0]/2-300, resolution[1]/2 + 64 + 12*x))
                    x += 1
            
        if showplayerlist > 0:
            x = 0
            for i in game.gui_get_player_stats(all=(showplayerlist == 1)):
                windowSurface.blit(font.render(i, False, (192, 192, 192)), (resolution[0]-300, 64 + 12*x))
                x += 1
            if showplayerlist == 1:
                windowSurface.blit(font.render(repr(x) + " Players Connected", False, (192, 192, 192)), (resolution[0]-300, 64 + 12 * x))
            else:
                windowSurface.blit(font.render(repr(x) + " Players In Round", False, (192, 192, 192)), (resolution[0]-300, 64 + 12 * x))

        if showroundtime:
            ip = bigfont.render(str(datetime.timedelta(seconds=game.round_get_time_remaining())), False, (255, 255, 255))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2,32))
            
        if flags["DEBUG"]:
            mpos = pygame.mouse.get_pos()
            if zoomout: # TODO: Need to refactor and centralize these calculations as part of 'Camera' system
                x = mpos[0]*scalefactorx
                y = mpos[1]*scalefactory
            else:
                x, y = mpos[0]-offsetx, mpos[1]-offsety

            windowSurface.blit(font.render("DEBUG MODE", False, (255, 255, 255)), (resolution[0]/2-38,32))
            windowSurface.blit(font.render("FPS: " + repr(fpsClock.get_fps()), False, (255, 255, 255)), (0,0))
            windowSurface.blit(font.render("T: " + repr(len(threading.enumerate())), False, (192, 192, 192)), (0, 12))
            windowSurface.blit(font.render("World Offset: " + repr((-offsetx, -offsety)), False, (255, 255, 255)), (0,24))
            windowSurface.blit(font.render("World Size: " + repr(world.size) + " - " + repr((int(x), int(y))), False, (255, 255, 255)), (0,36))
            windowSurface.blit(font.render("World Objects: " + repr(len(world)), False, (255, 255, 255)), (0,48))
            
            if mouselock:
                windowSurface.blit(font.render("MOUSELOCK: ON", False, (255, 255, 255)), (resolution[0]-104,0))
            else:
                windowSurface.blit(font.render("MOUSELOCK: OFF", False, (255, 255, 255)), (resolution[0]-104,0))
            windowSurface.blit(font.render(repr(flags), False, (255, 255, 255)), (0, resolution[1]-12))            

            if logintercept:
                for i in xrange(len(mlh.messages)):
                    if i >= len(mlh.messages): break
                    c = 145 + i*2
                    windowSurface.blit(font.render(mlh.messages[i], False, (c, c, c)), (10, 64 + 12*i))

            if trackshipid == None:
                windowSurface.blit(font.render("Tracking Off", False, (255, 255, 255)), (resolution[0]/2-36,resolution[1]-12))

        if flags["GAME"]:
            game.gui_draw_game_screen_info(windowSurface, flags)

        if tournamentinfo:
            game.gui_draw_tournament_bracket(windowSurface, flags)

        if trackshipid != None:
            n = font.render("Tracking: " + objects[trackshipid]._worldobj.player.name, False, objects[trackshipid]._worldobj.player.color)
            windowSurface.blit(n, (resolution[0]/2-n.get_width()/2,resolution[1]-12))                          

        if mousemode == "Destroy":
            ip = bigfont.render("DESTROY OBJECT BY CLICK", False, (255, 0, 0))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
        elif mousemode == "Explode":
            ip = bigfont.render("CLICK TO CAUSE EXPLOSION FORCE", False, (255, 0, 0))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
        elif mousemode != None and mousemode[:3] == "Add":
            ip = bigfont.render("Click to Add " + mousemode[3:], False, (255, 255, 0))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))        

        if mousemode == "Move" and trackshipid == None:
            ip = bigfont.render("TRACK OBJECT BEFORE MOVING", False, (255, 255, 255))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))
        elif mousemode == "Move":
            ip = bigfont.render("CLICK TO MOVE "+objects[trackshipid]._worldobj.player.name, False, (255, 255, 255))
            windowSurface.blit(ip, (resolution[0]/2-ip.get_width()/2, resolution[1]/2-ip.get_height()/2))        

        pygame.display.update()
        fpsClock.tick(30) # keep in sync with physics engine?

    #TODO: Add Logging???

    # close out a game 
    game.end()

    logging.info("Closing Pygame...")
    print "Closing Pygame"
    pygame.quit()