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
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