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 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 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 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 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 _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 _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
Beispiel #8
0
    def apply(self, action: "PlayCard", game: "Game") -> GameUpdate:
        logger.info("infection played")

        # Diccionario: color -> lista de pilas con virus de ese color
        virus = dict()
        for color in Color:
            virus[color] = []

        # Listamos los virus que tiene en el cuerpo accediendo en orden
        # aleatorio a las pilas.
        for pile in random.sample(action.caller.body.piles, 4):
            if pile.is_infected():
                color = pile.get_top_color()
                virus[color].append(pile)

        if all(map(lambda x: len(x) == 0, virus.values())):
            raise GameLogicException("No tienes virus disponibles")

        # Lista de pilas libres de todos los jugadores
        candidates = []

        # Accederemos a los jugadores en orden aleatorio
        unfinished = game.get_unfinished_players()
        random.shuffle(unfinished)
        for player in unfinished:
            # Eliminamos al caller de la iteración
            if player == action.caller:
                continue

            # Añadimos las pilas libres a la lista de candidatas
            candidates.extend(
                list(filter(lambda p: p.is_free(), player.body.piles)))

        if len(candidates) == 0:
            raise GameLogicException(
                "No hay nadie que pueda recibir tus virus")

        # Aplicamos un orden aleatorio también a las pilas candidatas
        for candidate_pile in random.sample(candidates, len(candidates)):
            color = candidate_pile.get_top_color()

            # Asignamos el primer virus de ese color y lo quitamos de los
            # posibles.

            if len(virus[color]) == 0:
                # Si no hay virus de ese color -> comprobamos si hay virus
                # multicolor
                if len(virus[Color.All]) > 0:
                    color = Color.All
                else:  # No tenemos opción
                    continue

            pile = virus[color].pop()
            # Eliminamos el virus del cuerpo del caller
            pile.pop_modifiers()
            # Lo colocamos en la pila candidata
            candidate_pile.add_modifier(Virus(color=color))

        # Por simplificar, devolvemos el cuerpo de todos los jugadores
        update = GameUpdate(game)
        for player in game.players:
            body_update = GameUpdate(game)
            body_update.repeat({"bodies": {player.name: player.body.piles}})
            update.merge_with(body_update)

        update.msg = "un Contagio"
        return update