Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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)
Esempio n. 4
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