def set_paused(
        self, paused: bool, paused_by: str, resume_callback
    ) -> Optional[GameUpdate]:
        with self._paused_lock:
            if self.is_paused() == paused:
                return None

            # Solo el jugador que ha pausado la partida puede volver a reanudarla.
            if self._paused and self._paused_by != paused_by:
                raise GameLogicException(
                    "Solo el jugador que inicia la pausa puede reanudar"
                )

            # Si la pausa pasa del tiempo límite comentado anteriormente, la
            # partida se reanuda automáticamente
            if paused:
                # Se para mientras tanto el timer del turno
                self._turn_timer.pause()

                # Iniciamos un timer
                self._pause_timer = Timer(TIME_UNTIL_RESUME, resume_callback)
                self._pause_timer.start()

                logger.info(f"Game paused by {paused_by}")
            else:
                # Continúa el timer del turno
                self._turn_timer.resume()

                self._pause_timer.cancel()
                logger.info("Game resumed")

            self._paused = paused
            self._paused_by = paused_by
            return self.pause_update()
Beispiel #2
0
    def wait_for_game(self, user: User) -> None:
        """
        Añade al usuario a la cola de usuarios esperando partida.
        """

        with self._public_lock:
            # No añadimos al usuario si ya está esperando.
            if user in self.users_waiting:
                raise GameLogicException(
                    "El usuario ya está esperando a una partida pública")

            self.users_waiting.append(user)
            logger.info(f"User {user.name} is waiting for a game")

            # Si la cola tiene el máximo de jugadores para una partida, se crea
            # una partida para todos.
            if len(self.users_waiting) >= MAX_MATCH_USERS:
                self.create_public_game()
                return

            # En caso contrario, si se ha llegado al mínimo de usuarios se
            # inicia el timer.
            if len(self.users_waiting) == MIN_MATCH_USERS:
                self._public_timer = Timer(TIME_UNTIL_START,
                                           self.matchmaking_check)
                self._public_timer.start()
    def _start_turn_timer(self):
        """
        Reinicia el temporizador de pase de turno automático.
        """

        if self._turn_timer is not None:
            self._turn_timer.cancel()

        self._turn_timer = Timer(TIME_TURN_END, self._timer_end_turn)
        self._turn_timer.start()
Beispiel #4
0
    def __init__(self, num_users: int = 0) -> None:
        super().__init__()

        # Número de jugadores a la hora de hacer el matchmaking. Para comprobar
        # si están todos los jugadores que se habían organizado.
        self.num_users = num_users

        # Timer para empezar la partida si en TIME_UNTIL_START segundos no se
        # han conectado todos los jugadores.
        self.start_timer = Timer(TIME_UNTIL_START, self.start_check)
        self.start_lock = threading.Lock()
class Game:
    """
    Información global sobre la partida y su estado, de forma desacoplada a la
    base de datos.

    Los jugadores se guardan en una lista, y se sabe el turno actual con el
    índice en esta.
    """

    def __init__(self, users: List[User], turn_callback, enable_ai: bool) -> None:
        self.players = [Player(user.name) for user in users]
        self.deck: List[Card] = []
        self._start_time = datetime.now()
        self._enabled_ai = enable_ai
        self._bots_num = 0

        self._turn = 0
        self._turn_timer = None
        self._turn_lock = threading.Lock()
        self._turn_number = 0
        # El callback del timer se llamará cuando se cambie el turno de la
        # partida de forma automática con el timer. Incluirá el `game_update` y
        # si algún usuario ha sido expulsado de la partida.
        self._turn_callback = turn_callback

        self._paused = False
        self._paused_by = ""
        self._paused_lock = threading.Lock()
        self._paused_timer = None

        self._finished = False
        self._players_finished = 0
        # Indica la fase de descarte, en la que no se podrá hacer otra cosa
        # excepto seguir descartando o pasar el turno.
        self.discarding = False

    def __del__(self) -> None:
        """
        Destructor que termina la partida si no se ha hecho ya anteriormente.
        """

        if not self.is_finished():
            self.finish()

    def start(self) -> GameUpdate:
        """
        Inicializa la baraja, la reordena de forma aleatoria, y reparte 3 cartas
        a cada jugador, iterando de forma similar a cómo se haría en la vida
        real. También elige de forma aleatoria el turno inicial.

        Devuelve un game_update con el estado actual del juego.
        """

        logger.info("Setting up game")

        self.deck = DECK.copy()
        random.shuffle(self.deck)

        for i in range(3):
            for player in self.players:
                self.draw_card(player)

        self._turn = random.randint(0, len(self.players) - 1)
        logger.info(f"First turn is for {self.turn_player().name}")
        self._start_turn_timer()

        # Genera el estado inicial con las manos y turno
        update = GameUpdate(self)
        update.merge_with(self.current_turn_update())
        update.merge_with(self.hands_update())
        return update

    def is_finished(self) -> bool:
        return self._finished

    def get_player(self, user_name: str) -> Player:
        for player in self.players:
            if player.name == user_name:
                return player

        raise GameLogicException("El jugador no está en la partida")

    def get_unfinished_player(self, user_name: str) -> Player:
        """
        Devuelve un jugador que esté todavía jugando (que no haya ganado
        todavía).
        """
        player = self.get_player(user_name)
        if player.has_finished():
            raise GameLogicException("El jugador ya ha acabado")
        return player

    def get_unfinished_players(self) -> List[Player]:
        """
        Devuelve los jugadores que estén todavía jugando (que no hayan ganado
        todavía).
        """

        players = []
        for player in self.players:
            if not player.has_finished():
                players.append(player)

        return players

    def set_paused(
        self, paused: bool, paused_by: str, resume_callback
    ) -> Optional[GameUpdate]:
        with self._paused_lock:
            if self.is_paused() == paused:
                return None

            # Solo el jugador que ha pausado la partida puede volver a reanudarla.
            if self._paused and self._paused_by != paused_by:
                raise GameLogicException(
                    "Solo el jugador que inicia la pausa puede reanudar"
                )

            # Si la pausa pasa del tiempo límite comentado anteriormente, la
            # partida se reanuda automáticamente
            if paused:
                # Se para mientras tanto el timer del turno
                self._turn_timer.pause()

                # Iniciamos un timer
                self._pause_timer = Timer(TIME_UNTIL_RESUME, resume_callback)
                self._pause_timer.start()

                logger.info(f"Game paused by {paused_by}")
            else:
                # Continúa el timer del turno
                self._turn_timer.resume()

                self._pause_timer.cancel()
                logger.info("Game resumed")

            self._paused = paused
            self._paused_by = paused_by
            return self.pause_update()

    def is_paused(self) -> bool:
        return self._paused

    def run_action(self, caller: str, action: Action) -> List[GameUpdate]:
        """
        Llamado ante cualquier acción de un jugador en la partida. Devolverá el
        nuevo estado de la partida por cada jugador, o en caso de que ya hubiera
        terminado anteriormente o estuviera pausada, un error.

        Se terminará el turno automáticamente en caso de que no haya quedado el
        usuario en fase de descarte.
        """

        with self._turn_lock:
            if self.is_finished():
                raise GameLogicException("El juego ya ha terminado")

            if self.is_paused():
                raise GameLogicException("El juego está pausado")

            if self.players[self._turn].name != caller:
                raise GameLogicException("No es tu turno")

            player = self.get_unfinished_player(caller)
            try:
                # Importante el orden para que se inicialice
                update = action.apply(player, game=self)
            except GameLogicException as e:
                logger.info(f"Error running action: {e}")
                raise

            if not self.discarding and not self.is_finished():
                end_update = self._end_turn()
                update.merge_with(end_update)

            # Se reestablecen los turnos AFK del usuario que ha terminado
            # correctamente la partida.
            player.afk_turns = 0

            return update

    def fmt_action_msg(self, caller: str, action_msg: str) -> str:
        if action_msg is None:
            return None

        return f"{caller} ha jugado {action_msg}"

    def draw_card(self, player: Player) -> None:
        """
        Roba una carta del mazo para el jugador.
        """

        logger.info(f"{player.name} draws a card")

        drawn = self.deck.pop()
        player.hand.append(drawn)

    def draw_hand(self, player) -> None:
        """
        Roba cartas para un jugador hasta que tiene el mínimo de ellas.
        """

        update = GameUpdate(self)

        while len(self.turn_player().hand) < MIN_HAND_CARDS:
            self.draw_card(self.turn_player())

        update.add(
            player_name=self.turn_player().name,
            value={"hand": self.turn_player().hand},
        )

        return update

    def _end_turn(self) -> GameUpdate:
        """
        Tiene en cuenta que si el jugador al que le toca el turno no tiene
        cartas en la mano, deberá ser skipeado. Antes de pasar el turno el
        jugador automáticamente robará cartas hasta tener 3.

        Es posible, por tanto, que el fin de turno modifique varias partes de la
        partida, incluyendo las manos, por lo que se devuelve un game_update
        completo.
        """

        update = GameUpdate(self)

        # Termina la fase de descarte si estaba activada
        self.discarding = False

        while True:
            logger.info(f"{self.turn_player().name}'s turn has ended")
            self._turn_number += 1

            # Roba cartas hasta tener las necesarias, se actualiza el estado de
            # ese jugador en concreto.
            draw_update = self.draw_hand(self.turn_player())
            update.merge_with(draw_update)

            turn_update = self._advance_turn()
            update.merge_with(turn_update)

            # Continúa pasando el turno si el jugador siguiente no tiene cartas
            # disponibles.
            if len(self.turn_player().hand) == 0:
                logger.info(f"{self.turn_player().name} skipped (no cards)")
                continue

            # Se tratan también los casos en los que juega la Inteligencia
            # Artificial, que realmente no cuentan como un turno tampoco.
            if self.turn_player().is_ai:
                logger.info(f"AI playing in place of {self.turn_player().name}")
                ai_update = self._ai_turn()
                update.merge_with(ai_update)

            # Comprobamos si ha ganado algún jugador
            for unfinished_player in self.get_unfinished_players():
                if unfinished_player.body.is_healthy():
                    # Si tiene un cuerpo completo sano, se considera que ha ganado.
                    finished_update = self.player_finished(unfinished_player)
                    update.merge_with(finished_update)

            if self._players_finished == len(self.players) - 1:
                finish_update = self.finish()
                update.merge_with(finish_update)

            # Posiblemente acabe la partida después de que juegue la IA, en
            # cuyo caso ya no se sigue iterando.
            if self.is_finished():
                return update

            # Si era una IA, saltamos al siguiente turno
            if self.turn_player().is_ai:
                continue  # Se salta al siguiente turno

            # Sino, dejamos de buscar jugador
            break

        self._start_turn_timer()

        return update

    def _advance_turn(self) -> GameUpdate:
        """
        Siguiente turno, y actualización del estado a todos los jugadores

        No se le pasará el turno a un jugador que ya ha terminado la partida.
        """

        has_changed = False
        for i in range(len(self.players)):
            self._turn = (self._turn + 1) % len(self.players)

            if not self.turn_player().has_finished():
                has_changed = True
                break

        if not has_changed:
            raise Exception("Logic error: no users left to advance turn")

        logger.info(f"{self.turn_player().name}'s turn has started")

        return self.current_turn_update()

    def _ai_attempt(self, actions: List[Action]) -> (bool, Optional[GameUpdate]):
        """
        Ejecuta un intento de la inteligencia artificial.

        Devuelve verdadero si ha tenido éxito, y será acompañado por un
        game_update.
        """

        update = GameUpdate(self)

        # Se iteran las acciones de cada intento, y si alguna de
        # ellas falla se continúa con el siguiente intento.
        for action in actions:
            try:
                action_update = action.apply(self.turn_player(), game=self)
            except GameLogicException as e:
                logger.info(f"Skipping error in IA action: {e}")
                return False, None  # Intento fallido, no se continúa

            update.merge_with(action_update)

            # Se comprueba si se ha terminado la partida, en cuyo caso
            # no hace falta continuar.
            if self._players_finished == len(self.players) - 1:
                finish_update = self.finish()
                update.merge_with(finish_update)
                return True, update

        return True, update

    def _ai_turn(self) -> GameUpdate:
        """
        Ejecuta un turno de la inteligencia artificial.
        """

        logger.info("AI turn starts")
        attempts = AI.next_action(self.turn_player(), game=self)

        # Se iteran todos los intentos, cada uno con una lista de acciones a
        # probar.
        for actions in attempts:
            success, update = self._ai_attempt(actions)
            if success:
                return update

        # La IA garantiza que siempre realizará una acción.
        raise GameLogicException("Unreachable: no attempts remaining for the IA")

    def _auto_discard(self) -> Optional[GameUpdate]:
        if not self.discarding and len(self.turn_player().hand) > 0:
            logger.info(f"Player {self.turn_player().name} auto discarded")
            discarded = random.randint(0, len(self.turn_player().hand) - 1)
            action = Discard(discarded)
            return action.apply(self.turn_player(), game=self)

        return None

    def _timer_end_turn(self):
        """
        Termina el turno automáticamente por parte del timer.

        El turno es controlado tanto de forma manual con `run_action` como de
        forma automática en este método. Como la ejecución es secuencial por
        usar locks entre ambas fuentes, se pueden dar las siguientes
        situaciones, que podrían provocar comportamiento indeseado a tener en
        cuenta:

        1. run_action
        2. _timer_end_turn

        1. _timer_end_turn
        2. run_action

        El segundo caso no sería un problema, porque se pasaría el turno
        automáticamente con el timer, y posteriormente el usuario con el turno
        anterior intentaría hacer una acción que terminase el turno, como por
        ejemplo jugar una carta. Sin embargo, en `run_action` ya se comprueba
        que el jugador que lo invoca es el que tiene el turno, y se producirá un
        error.

        El primer caso, sin embargo, sí que puede producir una condición de
        carrera. Es posible que el timer salte justo cuando se esté terminando
        un turno de forma manual en `run_action`, en cuyo caso al terminar
        `run_action` se llamaría a `_timer_end_turn` y se volvería a pasar el
        turno (de forma incorrecta).

        Para mitigar la anterior condición de carrera, es necesario asegurarse
        en esta función que antes y después de tenerse el lock no haya cambiado
        el turno ya. Esto no se puede comprobar comparando el nombre del usuario
        que tiene el turno, ya que es posible que después de pasar el turno le
        toque al mismo usuario otra vez porque los demás no tengan cartas. Es
        por ello por lo que se mantiene un contador con el número de turno, que
        adicionalmente puede tener otros usos.
        """

        initial_turn = self._turn_number
        with self._turn_lock:
            # Para el caso en el que la partida ha sido terminada externamente y
            # el timer sigue llamando al callback.
            if self.is_finished():
                logger.info("Match finished externally, stopping timer")
                return

            # El turno ha cambiado externamente al obtener el lock.
            if self._turn_number != initial_turn:
                return

            update = GameUpdate(self)
            caller = self.turn_player().name

            self.turn_player().afk_turns += 1
            logger.info(
                f"Turn timeout for {self.turn_player().name}"
                f" ({self.turn_player().afk_turns} in a row)"
            )

            # Expulsión de jugadores AFK en caso de que esté activada la IA.
            kicked = None
            is_afk = self._enabled_ai and self.turn_player().afk_turns == MAX_AFK_TURNS
            if is_afk:
                kicked = self.turn_player().name
                logger.info(f"Player {kicked} is AFK")
                kick_update = self.remove_player(self.turn_player().name)
                update.merge_with(kick_update)

                # Si no quedan suficientes jugadores se acaba la partida.
                if self.is_finished():
                    self._turn_callback(None, None, True, caller)
                    return
            else:
                # Al terminar un turno de forma automática se le tendrá que
                # descartar al jugador una carta de forma aleatoria, excepto
                # cuando esté en la fase de descarte.
                #
                # La carta ya se le robará de forma automática al terminar el
                # turno.
                discard_update = self._auto_discard()
                if discard_update is not None:
                    update.merge_with(discard_update)

                # Terminación automática del turno
                logger.info(f"Player turn {kicked} automatically ended")
                end_update = self._end_turn()
                update.merge_with(end_update)

            # Notificación de que ha terminado el turno automáticamente,
            # posiblemente con un usuario nuevo expulsado.
            self._turn_callback(update, kicked, self.is_finished(), caller)

    def _start_turn_timer(self):
        """
        Reinicia el temporizador de pase de turno automático.
        """

        if self._turn_timer is not None:
            self._turn_timer.cancel()

        self._turn_timer = Timer(TIME_TURN_END, self._timer_end_turn)
        self._turn_timer.start()

    def turn_player(self) -> Player:
        """
        Devuelve el nombre del usuario con el turno actual.
        """

        return self.players[self._turn]

    def _playtime_mins(self) -> int:
        """
        Devuelve el tiempo de juego de la partida.
        """

        elapsed = datetime.now() - self._start_time
        return int(elapsed.total_seconds() / 60)

    def _leaderboard(self) -> Dict:
        """
        Calcula los resultados de la partida hasta el momento, incluyendo las
        monedas obtenidas para cada jugador según la posición final, siguiendo
        la fórmula establecida:

          Sea N el número de jugadores de la partida, el jugador en puesto i
          ganará 10 * (N - i) monedas en la partida. El primero será por ejemplo
          N * 10, y el último 0.
        """

        leaderboard = {}
        N = len(self.players)

        for player in self.players:
            # Si la partida ha terminado todos los jugadores tendrán que tener
            # asignados una posición.
            position = player.position
            if position is None and self.is_finished():
                position = self._players_finished + 1

            coins = None
            if position is not None:
                coins = 10 * (N - position)

            leaderboard[player.name] = {
                "position": position,
                "coins": coins,
            }

        return leaderboard

    def remove_player(self, player_name: str) -> GameUpdate:
        """
        Elimina un jugador de la partida.

        Si está activada la IA el jugador es reemplazado por un bot, y en caso
        contrario se mueven sus cartas al inicio de la baraja y se elimina.

        El GameUpdate devuelto tendrá datos vacíos para el usuario que se ha
        eliminado para simplificar el problema.
        """

        update = GameUpdate(self)

        if self.is_finished():
            return update

        try:
            player = self.get_player(player_name)
        except GameLogicException:
            return update

        if self.is_paused() and self._paused_by == player_name:
            pause_update = self.set_paused(False, player_name, None)
            update.merge_with(pause_update)

        if self._enabled_ai:
            logger.info(f"Player {player_name} is being replaced by the AI")
            player.is_ai = True
            self._bots_num += 1

            if self.turn_player() == player:
                # Descartamos automáticamente si no lo ha hecho ya
                discard_update = self._auto_discard()
                if discard_update is not None:
                    update.merge_with(discard_update)

                # Terminación automática del turno
                end_update = self._end_turn()
                update.merge_with(end_update)
        else:
            logger.info(f"Player {player_name} is being removed")
            # Si es su turno se pasa al siguiente
            if self.turn_player() == player:
                self._advance_turn()

            # Índices antes de eliminar jugadores
            turn_index = self._turn
            removed_index = self.players.index(player)

            # Se añaden sus cartas al mazo y se elimina de la partida
            player.empty_hand(return_to=self.deck)
            player.empty_body(return_to=self.deck)
            self.players.remove(player)

            # Si por ejemplo se elimina el primer usuario y tiene el turno el
            # cuarto, el índice apuntará ahora al quinto en la partida.
            if removed_index < turn_index:
                self._turn -= 1

            update.merge_with(self.current_turn_update())

        # Comprobando si quedan suficientes usuarios
        remaining = len(self.players)
        if self._enabled_ai:
            remaining -= self._bots_num
        if remaining < MIN_MATCH_USERS:
            finish_update = self.finish()
            update.merge_with(finish_update)

        update.merge_with(self.players_update())
        return update

    def player_finished(self, player: Player) -> GameUpdate:
        """
        Finaliza la partida para un jugador en concreto.
        """

        if player.has_finished():
            raise GameLogicException("El jugador ya ha terminado")

        self._players_finished += 1
        player.position = self._players_finished

        logger.info(f"{player.name} has finished at position {player.position}")

        # Vaciamos la mano del jugador y devolvemos las cartas a la baraja
        player.empty_hand(return_to=self.deck)
        # Vaciamos el cuerpo del jugador
        player.empty_body(return_to=self.deck)

        # Generamos un GameUpdate donde:
        # 1. Avisamos a todos los jugadores de que el jugador ha acabado.
        update = GameUpdate(self)
        update.repeat({"leaderboard": self._leaderboard()})
        # 2. Mostramos las pilas vacías
        empty_piles = GameUpdate(self)
        empty_piles.repeat({"bodies": {player.name: player.body.piles}})
        update.merge_with(empty_piles)
        # 3. Le enviamos al jugador la mano vacía
        empty_hand = GameUpdate(self)
        empty_hand.add(player.name, {"hand": player.hand})
        update.merge_with(empty_hand)

        return update

    def players_update(self) -> GameUpdate:
        update = GameUpdate(self)

        players = []
        for player in self.players:
            data = {"name": player.name}

            if player.is_ai:
                data["picture"] = BOT_PICTURE_ID
                data["is_ai"] = True

            players.append(data)

        update.repeat({"players": players})
        return update

    def hands_update(self) -> GameUpdate:
        update = GameUpdate(self)
        update.add_for_each(lambda player: {"hand": player.hand})
        return update

    def current_turn_update(self) -> GameUpdate:
        update = GameUpdate(self)
        update.repeat({"current_turn": self.turn_player().name})
        return update

    def finish_update(self) -> GameUpdate:
        update = GameUpdate(self)

        data = {"finished": self.is_finished()}
        if self.is_finished():
            data["leaderboard"] = self._leaderboard()
            data["playtime_mins"] = self._playtime_mins()

        update.repeat(data)
        return update

    def pause_update(self) -> GameUpdate:
        update = GameUpdate(self)

        data = {
            "paused": self.is_paused(),
            "paused_by": self._paused_by,
        }

        update.repeat(data)
        return update

    def bodies_update(self) -> GameUpdate:
        update = GameUpdate(self)

        data = {"bodies": {}}
        for player in self.players:
            data["bodies"][player.name] = player.body.piles

        update.repeat(data)
        return update

    def time_update(self) -> GameUpdate:
        update = GameUpdate(self)

        # No se envía si no hay timer o si no ha empezado
        if self._turn_timer is None:
            return update
        remaining = self._turn_timer.remaining_secs()
        if remaining is None:
            return update

        update.repeat({"remaining_turn_secs": remaining})
        return update

    def full_update(self) -> GameUpdate:
        update = GameUpdate(self)

        update.merge_with(self.bodies_update())
        update.merge_with(self.current_turn_update())
        update.merge_with(self.finish_update())
        update.merge_with(self.hands_update())
        update.merge_with(self.time_update())
        if self.is_paused():  # Solo se envía si la partida está pausada
            update.merge_with(self.pause_update())
        update.merge_with(self.players_update())

        return update

    def finish(self) -> GameUpdate:
        """
        Finaliza el juego y devuelve un game_update.
        """

        logger.info("Game has finished")

        self._finished = True
        if self._turn_timer is not None:
            self._turn_timer.cancel()
        if self._paused_timer is not None:
            self._paused_timer.cancel()

        return self.finish_update()
Beispiel #6
0
class MatchManager:
    def __init__(self) -> None:
        # Cola de usuarios buscando una partida pública
        self.users_waiting = deque()
        # Temporizador para el tiempo de pánico para generar una partida. Se
        # generará una partida con un número de jugadores menor a 6. Será
        # activado únicamente cuando se tengan suficientes usuarios para
        # comenzar una partida.
        self._public_timer = None
        # El servidor es secuencial, excepto en el caso de las partidas
        # públicas, que tienen timers que pueden modificar el estado desde un
        # thread distinto.
        self._public_lock = threading.Lock()

    def wait_for_game(self, user: User) -> None:
        """
        Añade al usuario a la cola de usuarios esperando partida.
        """

        with self._public_lock:
            # No añadimos al usuario si ya está esperando.
            if user in self.users_waiting:
                raise GameLogicException(
                    "El usuario ya está esperando a una partida pública")

            self.users_waiting.append(user)
            logger.info(f"User {user.name} is waiting for a game")

            # Si la cola tiene el máximo de jugadores para una partida, se crea
            # una partida para todos.
            if len(self.users_waiting) >= MAX_MATCH_USERS:
                self.create_public_game()
                return

            # En caso contrario, si se ha llegado al mínimo de usuarios se
            # inicia el timer.
            if len(self.users_waiting) == MIN_MATCH_USERS:
                self._public_timer = Timer(TIME_UNTIL_START,
                                           self.matchmaking_check)
                self._public_timer.start()

    def matchmaking_check(self):
        """
        Comprobación de si se puede crear una partida pública "de emergencia"
        (con menos jugadores que el máximo). La partida se crea si es posible.
        """

        with self._public_lock:
            if len(self.users_waiting) >= MIN_MATCH_USERS:
                self.create_public_game()

    def stop_waiting(self, user: User) -> None:
        """
        Elimina al usuario de la cola de usuarios esperando partida.
        """

        with self._public_lock:
            self.users_waiting.remove(user)
            logger.info(f"User {user.name} has stopped searching")

            not_enough_users = len(self.users_waiting) < MIN_MATCH_USERS
            timer_running = self._public_timer is not None
            if not_enough_users and timer_running:
                self._public_timer.cancel()
                self._public_timer = None

    def create_public_game(self) -> None:
        # Se cancela el timer si es necesario.
        if self._public_timer is not None:
            self._public_timer.cancel()
            self._public_timer = None

        # Obtener los jugadores esperando
        users = self.get_waiting()

        # Creamos la partida
        new_match = PublicMatch(num_users=len(users))
        code = new_match.code
        # Añadimos la partida a la lista de partidas
        matches[code] = new_match

        # Avisar a todos los jugadores de la partida
        for user in users:
            socket.emit("found_game", {"code": code}, room=user.sid)

        # Ponemos un timer para empezar la partida, por si no se unen todos
        logger.info(f"Public match {code} has been created")
        new_match.start_timer.start()

    def create_private_game(self, owner: User) -> None:
        if owner in self.users_waiting:
            raise GameLogicException(
                "El usuario ya está esperando a una partida pública")

        new_match = PrivateMatch(owner=owner)
        matches[new_match.code] = new_match
        logger.info(
            f"Private match {new_match.code} has been created by {owner.name}")
        return new_match.code

    def remove_game(self, code: str) -> None:
        del matches[code]

    def get_match(self, code: str) -> Optional[Match]:
        return matches.get(code)

    def remove_match(self, code: str) -> None:
        logger.info(f"Removing {code} from matches")
        # Eliminar con seguridad (para evitar crashes)
        matches.pop(code, None)

    def get_waiting(self) -> List[User]:
        """
        Devuelve el máximo de jugadores (y los elimina de la cola de espera)
        intentando completar una partida. Si no hay suficientes jugadores para
        una partida, devuelve una lista vacía y no los elimina de la cola.
        """

        waiting = []
        max_to_play = min(len(self.users_waiting), MAX_MATCH_USERS)

        for i in range(max_to_play):
            waiting.append(self.users_waiting.popleft())

        return waiting
Beispiel #7
0
class PublicMatch(Match):
    """
    Información de una partida pública, gestionada completamente por el sistema
    gestor de partidas.
    """
    def __init__(self, num_users: int = 0) -> None:
        super().__init__()

        # Número de jugadores a la hora de hacer el matchmaking. Para comprobar
        # si están todos los jugadores que se habían organizado.
        self.num_users = num_users

        # Timer para empezar la partida si en TIME_UNTIL_START segundos no se
        # han conectado todos los jugadores.
        self.start_timer = Timer(TIME_UNTIL_START, self.start_check)
        self.start_lock = threading.Lock()

    # NOTE: se declaran de forma separada y privada los métodos `_start` y
    # `_end`. Esto es porque para los métodos públicos `start` y `end` es
    # necesario hacer lock para evitar problemas de concurrencia con el timer,
    # pero el mismo timer también necesita acceso a esas funciones. Por tanto,
    # para evitar un deadlock el timer accederá a las versiones sin lock, y las
    # interfaces públicas sí que usarán el lock.

    def _start(self):
        logger.info(f"Starting public game {self.code}"
                    f" with {len(self.users)} users")

        # Cancelamos el timer si sigue
        self.start_timer.cancel()

        super().start()

    def _end(self, cancel: bool = False):
        # Cancelamos el timer si sigue
        self.start_timer.cancel()

        super().end(cancel)

    def start(self):
        with self.start_lock:
            self._start()

    def end(self, cancel: bool = False):
        with self.start_lock:
            self._end(cancel)

    def start_check(self):
        """
        Comprobación de si la partida puede comenzar tras haber dado un tiempo a
        los jugadores para que se conecten. Si es posible, la partida empezará,
        y sino se cancela la partida por esperar demasiado.

        Como esta parte se realiza de forma concurrente, es necesario usar el
        lock de inicio de turno y asegurarse que después de obtenerlo no se ha
        iniciado la partida ya.
        """

        logger.info("Public match timer triggered")

        with self.start_lock:
            if self.is_started():
                logger.info("Timer skipping check; game already started")
                return

            # Empezamos la partida únicamente si hay suficientes usuarios
            if len(self.users) >= MIN_MATCH_USERS:
                self._start()
            else:
                self._end(cancel=True)