def parse_card(data: Dict) -> (object, Dict): """ Devuelve los datos necesarios para la inicialización de una carta de los datos JSON. """ # En el caso de cartas simples solo se necesita el color simple_cards = { "organ": Organ, "medicine": Medicine, "virus": Virus, } cls = simple_cards.get(data["type"]) try: col = Color(data["color"]) except ValueError: col = None # Fallará con los tratamientos if cls is not None: return cls, {"color": col} # Si no es una carta simple es un tratamiento, que no tiene color treatment_cards = { "latex_glove": LatexGlove, "organ_thief": OrganThief, "infection": Infection, "medical_error": MedicalError, "transplant": Transplant, } cls = treatment_cards.get(data["treatment_type"]) if cls is not None: return cls, {} raise GameLogicException(f"Couldn't parse card with data {data}")
def apply(self, caller: "Player", game: "Game") -> GameUpdate: logger.info(f"{caller.name} plays a card") # No podrá jugar una carta si el mismo jugador está en proceso de # descarte. if game.discarding: raise GameLogicException("El jugador está en proceso de descarte") self.caller = caller # Obtiene la carta y la elimina de su mano. No hace falta actualizar el # estado al eliminar la carta porque ya se hará cuando robe al final del # turno. card = caller.get_card(self.slot) # NOTE: no hay ninguna carta que intercambie manos de jugadores, en ese # caso habría que guardar el estado completo de la mano anterior y # borrar la carta (para que cuando se intercambiase no hubiera # problemas) y, en caso de fallo, restaurarla. # Usa la carta update = card.apply(self, game) # Solo si hemos podido "aplicar" el comportamiento de la carta, la # quitaremos de la mano. if card.is_placeable(): # No devolvemos la carta a la baraja (está puesta en un cuerpo). caller.remove_card(self.slot) else: caller.remove_card(self.slot, return_to=game.deck) return update
def get_action_data(self, action: "PlayCard", game: "Game") -> None: # Jugador con el que queremos intercambiar el cuerpo self.target_name = action.data.get("target") if self.target_name in (None, ""): raise GameLogicException("Parámetro target vacío") self.target = game.get_unfinished_player(self.target_name)
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 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 get_action_data(self, action: "PlayCard", game: "Game") -> None: """ """ # Jugador objetivo target = action.data.get("target") # Pilas del jugador objetivo self.organ_pile_slot = action.data.get("organ_pile") if None in (target, self.organ_pile_slot): raise GameLogicException("Parámetro vacío") if type(target) is not str or type(self.organ_pile_slot) is not int: raise GameLogicException("Tipo de parámetro incorrecto") self.target = game.get_unfinished_player(target) self.organ_pile = self.target.body.get_pile(self.organ_pile_slot)
def apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) if self.target.name == action.caller.name: raise GameLogicException("No puedes colocar un virus en tu cuerpo") logger.info( f"{self.color}-colored virus played over {self.target.name}") # Comprobamos si hay que extirpar o destruir vacuna if self.organ_pile.is_infected(): # Lo añadimos para que vuelva a la baraja self.organ_pile.add_modifier(self) # Si está infectado -> se extirpa el órgano self.organ_pile.remove_organ(return_to=game.deck) elif self.organ_pile.is_protected(): # Lo añadimos para que vuelva a la baraja self.organ_pile.add_modifier(self) # Si está protegido -> se destruye la vacuna self.organ_pile.pop_modifiers(return_to=game.deck) else: # Se infecta el órgano (se añade el virus a los modificadores) self.organ_pile.add_modifier(self) update = self.piles_update(game) update.msg = ( f"un virus {self.color.translate()['male']} sobre {self.target.name}" ) return update
def get_pile(self, pile: int) -> OrganPile: if pile < 0 or pile > 3: raise GameLogicException("Slot de pila inválido") if self.piles[pile] is None: self.piles[pile] = OrganPile() return self.piles[pile]
def apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) # Comprobamos que la pila tiene órgano if self.organ_pile.is_empty(): raise GameLogicException("No puedes robar órganos inexistentes") # Comprobamos que ninguno de los dos órganos está inmunizado if self.organ_pile.is_immune(): raise GameLogicException("No puedes robar órganos inmunes") # Comprobamos que el caller no tiene ya un órgano de ese color if not action.caller.body.organ_unique(self.organ_pile.organ): raise GameLogicException("Ya tienes un órgano de ese color") # Comprobamos que no se va a robar un órgano a sí mismo if action.caller == self.target: raise GameLogicException("No puedes robarte un órgano a ti mismo") # Obtenemos un espacio libre del caller self.empty_slot = None for (slot, pile) in enumerate(action.caller.body.piles): if pile.is_empty(): self.empty_slot = slot break if self.empty_slot is None: raise GameLogicException("No tienes espacio libre") logger.info("organ-thief played") # Robamos la pila del target y la guardamos en el caller empty_pile = action.caller.body.piles[self.empty_slot] action.caller.body.piles[self.empty_slot] = self.organ_pile self.target.body.piles[self.organ_pile_slot] = empty_pile update = GameUpdate(game) # Añadimos el cuerpo del caller al GameUpdate update.repeat({ "bodies": { self.target.name: self.target.body.piles, action.caller.name: action.caller.body.piles, }, }) update.msg = f"un Ladrón de Órganos sobre {self.target.name}" return update
def remove_card(self, slot: int, return_to: Optional[List[Card]] = None) -> None: try: card = self.hand[slot] if return_to is not None: return_to.insert(0, card) del self.hand[slot] except IndexError: raise GameLogicException("Slot no existente en la mano del jugador")
def apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) # Comprobamos que las dos pilas tienen órgano if self.organ_pile1.is_empty() or self.organ_pile2.is_empty(): raise GameLogicException( "No puedes intercambiar órganos inexistentes") # Comprobamos que ninguno de los dos órganos está inmunizado if self.organ_pile1.is_immune() or self.organ_pile2.is_immune(): raise GameLogicException("No puedes intercambiar órganos inmunes") # Comprobamos que no se haga un transplante a sí mismo. if self.player1 == self.player2: raise GameLogicException( "No puedes intercambiar óganos entre el mismo jugador") # Comprobamos que ninguno de los dos jugadores tienen ya un órgano del # mismo color del órgano a añadir. NOTE: Ignoramos las pilas sobre las # que se va a reemplazar, porque no crean conflicto. if not (self.player1.body.organ_unique(self.organ_pile2.organ, ignored_piles=[self.pile_slot1]) and self.player2.body.organ_unique( self.organ_pile1.organ, ignored_piles=[self.pile_slot2])): raise GameLogicException("Ya tiene un órgano de ese color") logger.info("transplant played") update = GameUpdate(game) # Intercambiamos las pilas de ambos jugadores tmp = self.player1.body.piles[self.pile_slot1] self.player1.body.piles[self.pile_slot1] = self.player2.body.piles[ self.pile_slot2] self.player2.body.piles[self.pile_slot2] = tmp # Añadimos los dos cuerpos al GameUpdate update.repeat({ "bodies": { self.player1.name: self.player1.body.piles, self.player2.name: self.player2.body.piles, }, }) update.msg = f"un Transplante entre {self.player1.name} y {self.player2.name}" return update
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 apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) if self.target.name != action.caller.name: raise GameLogicException( "No puedes colocar un órgano en otro cuerpo") if not self.target.body.organ_unique(self): raise GameLogicException("No puedes colocar un órgano repetido") logger.info( f"{self.color}-colored organ played over {self.target.name}") self.organ_pile.set_organ(self) update = self.piles_update(game) update.msg = f"un órgano {self.color.translate()['male']}" return update
def apply(self, caller: "Player", game: "Game") -> GameUpdate: if not game.discarding: raise GameLogicException( "El jugador no está en la fase de descarte") logger.info(f"{caller.name} stops discarding cards") game.discarding = False return GameUpdate(game, msg="un descarte")
def __init__(self, data) -> None: # Slot de la mano con la carta que queremos jugar. self.slot = data.get("slot") # Todos los datos pasados por el usuario self.data = data # El jugador que usa la carta self.caller: "Player" = None if self.slot is None: raise GameLogicException("Slot vacío")
def get_action_data(self, action: "PlayCard", game: "Game") -> None: """ Extraer la información común para las cartas simples y realizar las comprobaciones correspondientes. """ # Jugador donde queremos colocar la carta (en su cuerpo). target_name = action.data.get("target") # Pila de órgano donde se va a colocar la carta (dentro de dicho cuerpo). organ_pile_slot = action.data.get("organ_pile") if None in (target_name, organ_pile_slot): raise GameLogicException("Parámetro vacío") self.target = game.get_unfinished_player(target_name) self.organ_pile = self.target.body.get_pile(organ_pile_slot) # Comprobamos si podemos colocar if not self.organ_pile.can_place(self): raise GameLogicException("No se puede colocar la carta ahí")
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 _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 get_action_data(self, action: "PlayCard", game: "Game") -> None: """ """ # Jugadores entre los que queremos player1 = action.data.get("target1") player2 = action.data.get("target2") # Pilas de los jugadores a intercambiar self.pile_slot1 = action.data.get("organ_pile1") self.pile_slot2 = action.data.get("organ_pile2") if None in (player1, player2, self.pile_slot1, self.pile_slot2): raise GameLogicException("Parámetro vacío") self.player1 = game.get_unfinished_player(player1) self.player2 = game.get_unfinished_player(player2) self.organ_pile1 = self.player1.body.get_pile(self.pile_slot1) self.organ_pile2 = self.player2.body.get_pile(self.pile_slot2)
def apply(self, caller: "Player", game: "Game") -> GameUpdate: logger.info( f"{caller.name} discards their card at position {self.position}") # Activa la fase de descarte game.discarding = True if len(caller.hand) == 0: raise GameLogicException("El jugador no tiene cartas") # Elimina la carta de la mano del jugador y la añade al principio del # mazo, como indican las reglas del juego. caller.remove_card(self.position, return_to=game.deck) # No hay mensaje: ya se mostrará al pasar de turno de forma condensada. update = GameUpdate(game) update.add(caller.name, {"hand": caller.hand}) return update
def next_action(player: "Player", game: "Game") -> ActionAttempts: """ Punto principal de entrada que devuelve intentos a realizar por la IA. """ # Prioridad de las acciones, como se indica en el comentario del módulo: actions_priority = [ _action_special, _action_survive, _action_attack, _action_pass, ] for func in actions_priority: # Itera todos los intentos de esa acción attempts = func(player, game) for actions in attempts: yield actions # Nunca deberia llegarse aquí, puesto que la acción de pasar siempre # funcionará. raise GameLogicException("Unreachable: no possible action found for the IA")
def apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) if action.caller == self.target: raise GameLogicException( "No puedes intercambiar tu cuerpo contigo mismo") logger.info("medical-error played") update = GameUpdate(game) # Intercambiamos los cuerpos de ambos jugadores action.caller.body, self.target.body = self.target.body, action.caller.body # Añadimos los dos cuerpos al GameUpdate update.repeat({ "bodies": { self.target.name: self.target.body.piles, action.caller.name: action.caller.body.piles, }, }) update.msg = f"un Error Médico sobre {self.target.name}" return update
def apply(self, action: "PlayCard", game: "Game") -> GameUpdate: self.get_action_data(action, game) if self.target.name != action.caller.name: raise GameLogicException( "No puedes colocar una medicina en otro cuerpo") logger.info( f"{self.color}-colored medicine played over {self.target.name}") # Comprobamos si hay que destruir un virus if self.organ_pile.is_infected(): # Lo añadimos para que vuelva a la baraja self.organ_pile.add_modifier(self) # Destruimos el virus self.organ_pile.pop_modifiers(return_to=game.deck) else: # Se proteje o se inmuniza el órgano (se añade la vacuna a los # modificadores) self.organ_pile.add_modifier(self) update = self.piles_update(game) update.msg = f"una medicina {self.color.translate()['female']}" return update
def get_card(self, slot: int) -> Card: try: return self.hand[slot] except IndexError: raise GameLogicException("Slot no existente en la mano del jugador")
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
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")