Example #1
0
    def __init__(self, serverip, serverport, team, nick, screenMgr):
        super(SpylightGame, self).__init__()
        self.screenMgr = screenMgr

        # Register to the server
        self._ni = NetworkInterface(serverip, serverport)
        init_response = self._ni.connect(MessageFactory.init(team, nick))

        # Parse server init message
        self.team = init_response['team']
        self.playerid = init_response['id']

        # Init environment
        loaded_map = SpyLightMap(init_response['map'])
        Logger.info("SL|SLGame: Map loaded: %s", loaded_map.title)
        Logger.info("SL|SLGame: Map size: %s", loaded_map.size)

        if init_response['map_hash'] != loaded_map.get_hash():
            Logger.error("SL|SLGame: Wrong map hash. Expected %s",
                         loaded_map.get_hash())
            sys.exit()

        self.init_game_view(loaded_map, init_response)

        self.hud = SpylightHUD(self, max_hp=init_response['max_hp'])
        self.add_widget(self.hud)

        # Register input listeners
        kbMgr = KeyboardManager()
        kbMgr.bind(quit=screenMgr.goToPauseScreen)
        self._am = ActionManager(self._ni, kbMgr, self)

        # Game client ready
        self._ni.on_message_recieved = self.update
        self._ni.ready()
Example #2
0
    def __init__(self, serverip, serverport, team, nick, screenMgr):
        super(SpylightGame, self).__init__()
        self.screenMgr = screenMgr

        # Register to the server
        self._ni = NetworkInterface(serverip, serverport)
        init_response = self._ni.connect(MessageFactory.init(team, nick))

        # Parse server init message
        self.team = init_response['team']
        self.playerid = init_response['id']

        # Init environment
        loaded_map = SpyLightMap(init_response['map'])
        Logger.info("SL|SLGame: Map loaded: %s", loaded_map.title)
        Logger.info("SL|SLGame: Map size: %s", loaded_map.size)

        if init_response['map_hash'] != loaded_map.get_hash():
            Logger.error("SL|SLGame: Wrong map hash. Expected %s",
                         loaded_map.get_hash())
            sys.exit()

        self.init_game_view(loaded_map, init_response)

        self.hud = SpylightHUD(self, max_hp=init_response['max_hp'])
        self.add_widget(self.hud)

        # Register input listeners
        kbMgr = KeyboardManager()
        kbMgr.bind(quit=screenMgr.goToPauseScreen)
        self._am = ActionManager(self._ni, kbMgr, self)

        # Game client ready
        self._ni.on_message_recieved = self.update
        self._ni.ready()
Example #3
0
class GameEngine(object):
    _instances = {}

    def __new__(cls, *args, **kargs):
        if GameEngine._instances.get(cls) is None:
            GameEngine._instances[cls] = object.__new__(cls, *args, **kargs)
        return GameEngine._instances[cls]

    def init(self, config_file, map_file=None):
        self.__actionable_items = {} # Will contain the ActionableItem objects on the map that can do something when a player does 'action' on them (action = press the action key)
        self.__proximity_objects = {} # Will contain the ProximityObject objects on the map that can do something when a player is in a given range of them
        self.__loop = Event()
        self.__curr_player_number = 0
        self.__lock = Lock()
        self.__start_time = None
        self.__stepper_busy = Event()
        self.__stepper_interval = -1
        self.__stepper = None
        self.__end_time = -1
        self.all_players_connected = Event()
        self.load_config(config_file)
        self.auto_mode = False
        if map_file is not None:
            self.load_map(map_file)
        else:
            self.load_map(self.config.map_file)
        # will look like this : {"x,y": [item1, item2, item3]} (yes, there could potentially be multiple objects at the exact same position...)
        return self # allow chaining

    def acquire(self, blocking=1):
        self.__lock.acquire(blocking)

    def release(self):
        self.__lock.release()

    def setup_stepper(self, interval):
        def _stepper_action():
            self.__stepper = Timer(self.__stepper_interval, _stepper_action)
            self.__stepper.start()

            if not self.__stepper_busy.is_set():
                self.__stepper_busy.set()
                self.step()
                self.__stepper_busy.clear()

        if self.__stepper is not None:
            self.__stepper.cancel()
            self.__stepper_interval = -1
            self.__stepper = None

        if interval > 0:
            self.__stepper_interval = interval
            self.__stepper = Timer(interval, _stepper_action)
            self.__stepper.start()

    # @function push_new_item will register a new ActionableItem on the current game's map
    # @param item
    def push_new_item(self, item):
        key = self.__map_item_key_from_row_col(item.pos_row, item.pos_col)
        if isinstance(item, ActionableItem):
            dict_ = self.__actionable_items
        elif isinstance(item, ProximityObject):
            dict_ = self.__proximity_objects
        else:
            return self # allow chaining
        try:
            dict_[key].append(item)
        except KeyError:
            dict_[key] = [item]
        return self # allow chaining

    def remove_new_actionable_item(self, item):# TODO implementation of that
        return self # allow chaining

    def end_of_game(self):
        self.__loop.set()

    @property
    def loop(self):
        return not self.__loop.is_set()

    def step(self):
        # TODO: Maybe re-write the following lines, for a better handling of
        #       game termination.
        if self.auto_mode and self.__game_finished():
            self.end_of_game()
            return

        # Update players' positions and visions
        for p in self.__players:
            normalized_array = self.__get_normalized_direction_vector_from_angle(p.move_angle)
            self.__move_player(p, normalized_array[0] * p.speedx, normalized_array[1] * p.speedy)
            p.obstacles_in_sight = []
            p.obstacles_in_sight_n = 0
            # ------- Update player's sight -------
            # Parametrize things for occlusion (get obstacles that need to be taken into account by occlusion)
            sight_direction = self.__get_normalized_direction_vector_from_angle(p.sight_angle) * p.sight_range
            # A bit bruteforce here, let's use a circle instead of the real shaped vision
            # Just because there won't be many items to go through anyway
            # and for simplicity's and implementation speed's sakes
            y_start = max(0, p.posy - p.sight_range)
            y_end = min(self.slmap.max_y, p.posy + p.sight_range)
            x_start = max(0, p.posx - p.sight_range)
            x_end = min(self.slmap.max_x, p.posx + p.sight_range)
            row_start   = utils.norm_to_cell(y_start)
            row_end     = utils.norm_to_cell(y_end)
            col_start   = utils.norm_to_cell(x_start)
            col_end     = utils.norm_to_cell(x_end)
            vect = ((x_start, y_start), (x_end, y_end))
            self.__for_obstacle_in_range(vect, self.__occlusion_get_obstacle_in_range_callback, player=p)
            p.compute_sight_polygon_coords()
            # Launch occlusion
            p.sight_vertices, p.occlusion_polygon = occlusion(p.posx, p.posy, p.sight_polygon_coords, p.obstacles_in_sight, p.obstacles_in_sight_n)

            # ---------- Update player's visible objects list ----------
            # Note: Here we only go through the visible objects that are in a given range, not through all of them
            # We will go through the complete list, in order to update them, later in this method
            del p.visible_objects[:] # Empty the list

            for row in xrange(row_start, row_end+1):
                for col in xrange(col_start, col_end+1):
                    try:
                        for item in self.__actionable_items[self.__map_item_key_from_row_col(row, col)]:
                            if p.occlusion_polygon.intersects(item.geometric_point):
                                p.add_new_visible_object(item)
                    except KeyError:
                        pass # There was nothing at this (row,col) position...
                    try:
                        for item in self.__proximity_objects[self.__map_item_key_from_row_col(row, col)]:
                            if p.occlusion_polygon.intersects(item.geometric_point):
                                p.add_new_visible_object(item)
                    except KeyError:
                        pass # There was nothing at this (row,col) position...

            # ---------- Update player's visible players list ----------

            del p.visible_players[:] # Empty the list
            # Re-populate it
            for p2 in self.__players:
                if p2 is p:
                    continue # Do not include ourself in visible objects
                if p.occlusion_polygon.intersects(p2.hitbox):
                    p.visible_players.append((p2.player_id, p2.posx, p2.posy, p2.move_angle))
        # end of by player loop

        # Now go through all of the visible items to update them
        for row in xrange(0, self.slmap.height):
            for col in xrange(0, self.slmap.width):
                try:
                    for item in self.__actionable_items[self.__map_item_key_from_row_col(row, col)]:
                        item.update()
                except KeyError:
                    pass # There was nothing at this (row,col) position...
                try:
                    for item in self.__proximity_objects[self.__map_item_key_from_row_col(row, col)]:
                        item.update()
                        # Try to activate the proximity object on this player
                        for p_ in self.__players:
                            item.activate(p_)
                except KeyError:
                    pass # There was nothing at this (row,col) position...


    def __game_finished(self):
        # Note: This function is the right place to set end time of the
        #       game, because it knows the cause of the termination. If time is
        #       over, we must adapt the end time calculation to ensure it is
        #       accurate (i.e. we cannot trust time()). In all the other cases,
        #       end time is current time (i.e. time() value).

        # TODO: The end of the time is obviously not the only cause of game
        #       termination. Other causes should be handled too.
        if self.get_remaining_time() <= 0:
            self.__end_time = self.__start_time + self.__total_time
            return True
        else:
            return False

    def __move_player(self, player, dx, dy):
        """

        Moves the given player using the given dx nd dy deltas for x and y axis
        taking into account collisions with obstacles

        :param player: Instance of Player class, the player we want to move
        :param dx: float, the x coordinate difference we want to apply to the current player
            (may or may not be pplied depending on wether there are collisions)
        :param dy: float, the y coordinate difference we want to apply to the current player
            (may or may not be pplied depending on wether there are collisions)
        :return None
        """

        x_to_be, y_to_be = player.posx + dx, player.posy + dy

        # Do not go out of the map please :

        if x_to_be > self.slmap.max_x:
            x_to_be = self.slmap.max_x

        if y_to_be > self.slmap.max_y:
            y_to_be = self.slmap.max_y

        if x_to_be < 0:
            x_to_be = 0

        if y_to_be < 0:
            y_to_be = 0

        row, col = utils.norm_to_cell(player.posy), utils.norm_to_cell(player.posx)
        row_to_be, col_to_be = utils.norm_to_cell(y_to_be), utils.norm_to_cell(x_to_be)

        is_obs_by_dx = self.slmap.is_obstacle_from_cell_coords(row, col_to_be)
        is_obs_by_dy = self.slmap.is_obstacle_from_cell_coords(row_to_be, col)
        if is_obs_by_dx is False and is_obs_by_dy is False: # no collision
            player.posx = x_to_be
            player.posy = y_to_be
        elif is_obs_by_dx is False: # no collision only for x displacement
            player.posx = x_to_be
            player.posy = row_to_be * const.CELL_SIZE - 1 # maximum possible posy before colliding
        elif is_obs_by_dy is False: # no collision only for y displacement
            player.posy = y_to_be
            player.posx = col_to_be * const.CELL_SIZE - 1 # maximum possible posx before colliding
        else: # collision along all axis
            player.posx = col_to_be * const.CELL_SIZE - 1 # maximum possible posx before colliding
            player.posy = row_to_be * const.CELL_SIZE - 1 # maximum possible posy before colliding

        player.compute_hitbox()

        return player # allow chaining

    def __occlusion_get_obstacle_in_range_callback(self, vector, row, col, **kwargs):
        p = kwargs['player']
        x, y = col * const.CELL_SIZE, row * const.CELL_SIZE
        p.obstacles_in_sight.extend(
            [x, y,
            x + const.CELL_SIZE, y,
            x + const.CELL_SIZE, y + const.CELL_SIZE,
            x, y + const.CELL_SIZE])
        p.obstacles_in_sight_n += 8
        return None # just to explicitely tell the calling function to continue (I hate implicit things)


    def __map_item_key_from_row_col(self, row, col):
        return str(row) + "," + str(col)

    def get_player_sight(self, pid):
        return self.__players[pid].sight_vertices

    def action(self, pid):
        """

        :param pid: id of the player that is "actioning" (doing "action" action)
        :return: True of there was something to do, False else
        """
        actioner = self.__players[pid]
        key = self.__map_item_key_from_row_col(actioner.posx // const.CELL_SIZE, actioner.posy // const.CELL_SIZE)
        try:
            objs = self.__actionable_items[key]
        except KeyError:
            return False
        # Arbitrary here: Take the first of the list to act on... (TODO: See if we want to make priorities)
        objs[0].act(actioner)
        return True

    def load_config(self, config_file):
        self.config = ConfigHandler(config_file, default_config, option_types)
        return self # allow chaining

    def load_map(self, map_file):
        self.__map_file = map_file
        print "load_map: Loading map_file" + str(map_file)
        self.slmap = SpyLightMap()
        self.slmap.load_map(map_file)
        # Go through the whole map to find for special things to register, like actionable items...
        for row in xrange(0, self.slmap.height):
            for col in xrange(0, self.slmap.width):
                if self.slmap.map_tiles[row][col] == self.slmap.TERMINAL_KEY:
                    terminal = TerminalAI(row * const.CELL_SIZE, col * const.CELL_SIZE)
                    self.push_new_item(terminal)

        self.__total_time = 120  # TODO: Update with the real time read from the map file.
        self.__max_player_number = self.slmap.nb_players[Player.SPY_TEAM] + self.slmap.nb_players[Player.MERC_TEAM]  # TODO: Update with the true player number
                                      #       read from the map file.
        # Loading players
        start_merc_pids = 0 # firt merc pid to be assigned
        end_merc_pids = max(0, self.slmap.nb_players[0]-1) # Last mercernary pid to be assigned
        start_spy_pids = end_merc_pids+1 # firt spy pid to be assigned
        end_spy_pids = max(start_merc_pids, start_spy_pids + self.slmap.nb_players[1]-1) # Last spy pid to be assigned
        self.__players = [MercenaryPlayer(i) for i in xrange(start_merc_pids, end_merc_pids+1)] # TODO: replace that by the actual player loading
        self.__players.extend([SpyPlayer(i) for i in xrange(start_spy_pids, end_spy_pids+1)]) # TODO: replace that by the actual player loading

        # Move players to their respective spawn location
        for p in self.__players:
            spawn = self.slmap.get_spawn_point(p.team, p.player_id)
            dx, dy = spawn[1] * const.CELL_SIZE, spawn[0] * const.CELL_SIZE
            self.__move_player(p, dx, dy)
        # Do some things like settings the weapon for each player...
        return self # allow chaining

    def connect_to_player(self, team, nickname):
        if self.all_players_connected.is_set():
            return None

        self.acquire()

        players = [p for p in self.__players if not p.connected and p.team ==
            team]

        if len(players) > 1:
            player = choice(players)
        elif len(players) == 1:
            player = players[0]
        else:
            self.release()
            return None

        player.connected = True
        player.nickname = nickname
        self.__curr_player_number += 1
        if self.__curr_player_number == self.__max_player_number:
            self.start_auto_mode()

        self.release()

        return player.player_id

    def get_map_name(self):
        return self.__map_file

    def get_map_title(self):
        return self.slmap.title

    def get_map_hash(self):
        return self.slmap.get_hash()

    def get_player_state(self, pid):
        return self.__players[pid].get_state()

    def get_nb_players(self):
        return self.__curr_player_number

    def get_players_info(self):
        return [(p.nickname, p.player_id, p.team) for p in self.__players]

    def get_remaining_time(self):
        if self.__end_time > 0:
            return 0
        else:
            return max(int(round(self.__total_time - time() + self.
                                 __start_time)), 0)

    def get_current_time(self):
        if self.__end_time > 0:
            return int(round(self.__end_time - self.__start_time))
        else:
            return min(int(round(time() - self.__start_time)), self.
                                 __total_time)

    def get_game_statistics(self):
        # TODO: Return the game statistics useful to build the `end` frame.
        return {'winners': Player.SPY_TEAM, 'ttime': int(round(time() -
                __start_time))}

    def start_auto_mode(self):
        """
            This method will enable the "auto_mode"
            When auto_mode is enabled, the GameEngine will execute a step() every once a while
            This interval is controlled by self.config.step_state_interval

        :return: Nothing
        """
        self.auto_mode = True
        self.__loop.clear()
        self.setup_stepper(self.config.step_state_interval)
        self.__start_time = time()
        self.all_players_connected.set()
        return self # allow chaining

    def set_sight_angle(self, pid, angle):
        self.__players[pid].sight_angle = radians(angle)
        return self # allow chaining

    def set_movement_angle(self, pid, angle):
        """
        Set the movement angle ("kivy convention") of the given player.
        This angle will define in which direction the player is heading when it will have a speed assigned

        :param pid: Player id (int)
        :param angle: heading direction angle IN DEGREES (real or integer)
        """
        self.__players[pid].move_angle = radians(angle)
        return self # allow chaining

    def set_movement_speedx(self, pid, percentage):
        """
        Set the speed of a given player, on the xy axis

        :param pid: Player id (int)
        :param percentage: (real) between 0 and 1, percentage of its maximum speed along this axis,
        after taking into account the angular direction (this is like a speed modifier)
        """
        p = self.__players[pid]
        p.speedx = percentage * p.max_speedx
        return self

    def set_movement_speedy(self, pid, percentage):
        """
        Set the speed of a given player, on the y axis

        :param pid: Player id (int)
        :param percentage: (real) between 0 and 1, percentage of its maximum speed along this axis,
        after taking into account the angular direction (this is like a speed modifier)
        """
        p = self.__players[pid]
        p.speedy = percentage * p.max_speedy
        return self

    def __get_normalized_direction_vector_from_angle(self, a):
        x, y = -sin(a), cos(a)
        return (array((x, y)) / sqrt(x**2 + y**2))

    # @param pid player id
    # @param angle shoot angle, "kivy convention", in degree
    # @return{Player} the victim that has been shot, if any, else None
    def shoot(self, pid, angle):
        _logger.info("Starting shoot method")
        shooter = self.__players[pid]
        # Shoot "angle"
        a = radians(angle)
        # Weapon error angle application:
        a += shooter.weapon.draw_random_error()

        # Direction of the bullet (normalized vector)
        normalized_direction_vector = self.__get_normalized_direction_vector_from_angle(a) # x, y, but in the "kivy convention"

        # This vector/line represents the trajectory of the bullet
        origin = array((shooter.posx, shooter.posy))
        vector = (tuple(origin), tuple(origin + (normalized_direction_vector * shooter.weapon.range)))
        line = LineString(vector)

        _logger.info("origin=" + str(origin))
        _logger.info("vector=" + str(vector))

        # First, check if we could even potentially shoot any player
        victims = []
        for p in self.__players:
            if p == shooter:
                continue # you cannot shoot yourself
            # Yes, we do compute the player's hitbox on shoot. It is in fact lighter that storing it in the player, because storing it in the player's object would mean
            # updating it on every player's move. Here we do computation only on shoots, we are going to be many times less frequent that movements!
            hitbox = p.hitbox
            if line.intersects(hitbox): # hit!
                victims.append(p)

        # Then, if yes, check that there is not any obstacle to that shoot
        # Only check on obstacles that are close to that shot's trajectory (that is to say, not < (x,y) (depending on the angle, could be not > (x,y) or event more complex cases, but that's the idea)))
        if 0 != len(victims):
            distance, first_victim = self.__find_closest_victim(victims, shooter)
            # We re-compute the vector, stopping it at the victim's position. Indeed, if we used the "vector" variable
            # to look for collisions, as it uses the maximum weapon's range, we would look for collision BEHIND the
            # victim as well !
            to_first_victim_vector = (tuple(origin), tuple(origin + (normalized_direction_vector * distance)))
            if not self.__shoot_collide_with_obstacle(to_first_victim_vector, line): # no collision with any obstacle, thus we can harm the victim
                return self.__harm_victim(first_victim, shooter)
        else: # Else, there's just nothing to do, you did not aim at anyone, n00b
            return None

    stepx = const.CELL_SIZE
    stepy = const.CELL_SIZE

    def __find_closest_victim(self, victims, shooter):
        return sorted([(sqrt((shooter.posx - v.posx)**2 + (shooter.posy - v.posy)**2), v) for v in victims])[0] # Ugly line, huh? We create a list of (distance, victim) tuples, sort it (thus, the shortest distance will bring the first victim at pos [0] of the list


    # @param{Player} shooter : Player object (will give us the weapon to harm the victim and the original position of the shoot, to find who to harm)
    # @return{Player} t        # First, check if we could even potentially shoot any player
    # @return{Player} the victim harmed
    def __harm_victim(self, victim, shooter):
        shooter.weapon.damage(victim)
        return victim

    def __for_obstacle_in_range(self, vector, callback, **callback_args):
        """
            Finds the obstacle in the given range (range = a distance range + an angle (factorized in the "vector" argument))
            and executes the callback for each found obstacle

            :param vector: range/direction vector, of the form ((x_orig, y_orig), (x_end, y_end)) in real map coordinates
            :param callback: callback function, signature must be func([self, ]vector, row, col, **kwargs)
            :param callback_args: Additional arguments that will be passed to the callback function when executed

            /!\ Important /!\ Returns:
                - None either if the callback was never called or if it never returned anything else than None
                - the callback value, if a callback call returns anything that is not None
        """
        col_orig = utils.norm_to_cell(vector[0][0]) # x origin, discretize to respect map's tiles (as, we will needs the true coordinates of the obstacle, when we'll find one)
        _logger.info("__shoot_collide_with_obstacle(): x=" + str(col_orig))
        row = utils.norm_to_cell(vector[0][1]) # y origin, same process as for x
        _logger.info("__shoot_collide_with_obstacle(): y=" + str(row))
        col_end = int(utils.norm_to_cell(vector[1][0]))
        row_end = int(utils.norm_to_cell(vector[1][1]))
        # The following variables will be used to increment in the "right direction" (negative if the end if lower
        # than the origin, etc....
        col_increment_sign = 1 if (col_end-col_orig) > 0 else -1
        row_increment_sign = 1 if (row_end-row) > 0 else -1
        # A bit of explanation of the conditions here :
        # row < self.slmap.height --> Self explanatory, do not go over the map (as the line is multiplied by a
        # coefficient, this check is necessary
        # (row-row_end) != row_increment_sign --> This means that we want to stop when the "row" variable has gone one
        # unit further than the row_end variable. row_increment_sign will always have the same sign as
        # (row-row_end) when row is one unit further than row_end (by further we mean : one iteration further, in the
        # "direction" in which we are iterating: that could be forward or backward). Stopping when we are "one further"
        # means that we iterate until we reach the row_end... INCLUDED! (else, would not work when perfectly aligned)
        # same thing for the condition with "row" replaced by "col"
        while row < self.slmap.height and (row-row_end) != row_increment_sign:
            col = col_orig
            while col < self.slmap.width and (col-col_end) != col_increment_sign:
                if self.slmap.is_obstacle_from_cell_coords(row, col):
                    callback_result = callback(vector, row, col, **callback_args)
                    if callback_result is not None:
                        return callback_result
                col += col_increment_sign * 1
            row += row_increment_sign * 1
        return None

    def __shoot_collide_with_obstacle(self, vector, geometric_line):
        if self.__for_obstacle_in_range(vector, self.__shoot_collide_with_obstacle_callback, geomatric_line=geometric_line) is not None:
            return True # Found some obstacle collision !
        return False # Did not found any obstacle collision

    def __shoot_collide_with_obstacle_callback(self, vector, row, col, **kwargs):
        geometric_line = kwargs['geomatric_line']
        obstacle = utils.create_square_from_top_left_coords(row*const.CELL_SIZE, col*const.CELL_SIZE, const.CELL_SIZE) # Construct the obstacle
        if geometric_line.intersects(obstacle): # Is the obstacle in the way of the bullet?
            return True # Yes!
        return None

    def stop_auto_mode(self, force=False):
        """
        Will disable auto_mode.
        :param force:
        :return:
        """
        if self.loop:
            self.end_of_game()
        self.setup_stepper(-1)  # Disable stepping
        return self # allow chaining