def disconnect(): # La sesión del usuario se limpia al reconectarse, aunque existen casos que # necesitan limpieza. logger.info(f"Ending session with user {session['user'].name}") # Puede estar buscando una partida pública if session["user"] in MM.users_waiting: MM.stop_waiting(session["user"]) # NOTE: si el usuario está en una partida privada se cuenta como una # desconexión temporal y se podrá volver a unir. remove_from_public()
def play_discard(card): """ Descarta la carta indicada de la mano del usuario. Esta acción se puede repetir varias veces hasta que se pase el turno de forma automática o con ``play_pass``. La carta a descartar se puede indicar con el índice de ésta en su mano. Requiere que el usuario esté en una partida y que esté empezada o se devolverá un :ref:`error <errores>`. :param card: La carta del usuario a descartar :type card: int :return: Un mensaje :ref:`msg_game_update` para cada jugador. Si el usuario no está en una partida se devolverá un :ref:`error <errores>`. También se devolverá uno en caso de que el jugador no tenga cartas restantes para descartar o si la carta indicada no existe en la mano del jugador. """ if type(card) is not int: return {"error": "Tipo incorrecto para la carta"} match = MM.get_match(session["game"]) name = session["user"].name try: match.run_action(name, Discard(card)) except GameLogicException as e: return {"error": str(e)}
def stop_searching(): """ Parar de buscar una partida pública organizada por el servidor. :return: Devuelve un mensaje de tipo :ref:`msg_stop_searching` si se ha podido cancelar la búsqueda. Si se produce cualquier error (por ejemplo, que el usuario no esté buscando partida) se devolverá un :ref:`error <errores>`. """ if session["user"] in MM.users_waiting: MM.stop_waiting(session["user"]) emit("stop_searching") else: return {"error": "No estás buscando partida"}
def start_game(): """ Puesta en marcha de una partida privada. :return: Un broadcast de :ref:`msg_start_game`. Requiere que el usuario esté en una partida y que sea el líder o se devolverá un :ref:`error <errores>`. También deben haber al menos 2 jugadores en total esperando la partida para empezarla. """ game = session["game"] match = MM.get_match(game) # Comprobamos si el que empieza la partida es el creador try: if match.owner != session["user"]: return {"error": "Debes ser el lider para empezar partida"} except (AttributeError, TypeError): # Si la partida devuelta por el MM es una pública, no tiene sentido # empezar la partida (ya se encarga el manager) return {"error": "La partida no es privada"} if len(match.users) < 2: return {"error": "Se necesitan al menos dos jugadores"} match.start()
def check_card_interactions(self, card_order, expected_pile_states): """ Se prueba la secuencia de cartas card_order en la pila 0 del jugador 0 y se comprueba que la interacción sea la deseada (expected_pile_states). """ clients, code = self.create_game() # Primero se tendrá el game_update inicial received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) game = MM.get_match(code)._game # Usaremos al cliente 0 como target target = game.players[0].name for (i, card) in enumerate(card_order): # Cambiamos el turno según la carta para cumplir las restricciones # de no colocar medicina en el cuerpo de otros, etc. if isinstance(card, Organ) or isinstance(card, Medicine): game._turn = 0 elif isinstance(card, Virus): game._turn = 1 # Obtenemos el cliente al que le toca turn_player = game.players[game._turn] turn_client = clients[self.player_names.index(turn_player.name)] turn_player.hand[0] = card # Ignoramos los mensajes anteriores en un cliente cualquiera _ = clients[5].get_received() # Colocamos la carta en el jugador target. Las cartas se colocarán # en el orden de testing_hand y se espera que resulten en la pila 0 # == expected_pile_states[i] callback_args = turn_client.emit( "play_card", { "slot": 0, "target": target, "organ_pile": 0, }, callback=True, ) self.assertNotIn("error", callback_args) # Recibimos en un cliente cualquiera received = clients[5].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("bodies", args) self.assertEqual( args["bodies"][target][0], # Miramos la pila 0 del target expected_pile_states[i], )
def pause_game(paused): """ Pausa o reanuda una partida privada. :param paused: Pausar la partida :type paused: ``bool`` Requiere que el usuario esté en una partida privada y que esté empezada o se devolverá un :ref:`error <errores>`. :return: Un mensaje :ref:`msg_game_update` para cada jugador. Si no se cumplen los requisitos comentados anteriormente, se devolverá un :ref:`error <errores>`. """ if paused is None or type(paused) != bool: return {"error": "Parámetro incorrecto"} # TODO: Si la pausa pasa del tiempo límite comentado anteriormente, la # partida se reanuda automáticamente, y el jugador que no esté se le # irán pasando los turnos. match = MM.get_match(session["game"]) name = session["user"].name if not isinstance(match, PrivateMatch): return {"error": "No estás en una partida privada"} try: match.set_paused(paused, paused_by=name) except GameLogicException as e: return {"error": str(e)}
def search_game(): """ Unión a una partida pública organizada por el servidor. :return: El cliente no recibirá respuesta hasta que el servidor haya encontrado oponentes contra los que jugar. Una vez encontrada una partida, hará un broadcast de :ref:`msg_found_game`. Si ya está buscando partida, se devolverá un :ref:`error <errores>`. """ if session.get("game") is not None: return {"error": "El usuario ya está en una partida privada"} try: MM.wait_for_game(session["user"]) except GameLogicException as e: return {"error": str(e)}
def remove_from_public(): code = session.get("game") if code is None: return match = MM.get_match(code) if match is None: return if not isinstance(match, PublicMatch): return leave()
def test_auto_pass_discard(self): """ Comprueba que si un usuario se olvida de pasar el turno se le descarta una carta y roba. Si el jugador está descartando esto no se debería hacer. Como no se puede asumir que las cartas robadas sean diferentes a las descartadas, no se puede saber si se ha descartado una adicional al final del turno, y por tanto es imposible hacer un test unitario de esto. """ self.set_turn_timeout(0.5) clients, code = self.create_game() # Se tiene que acceder a la partida directamente para tener la mano del # jugador actual. match = MM.get_match(code) game = match._game current_player = game.turn_player() start_hand = [id(card) for card in current_player.hand] # Caso base (cambia la mano al final): client = self.get_current_turn_client(clients) client.get_received() # Limpia recibidos # Espera el tiempo de partida y comprueba que la mano no sea la misma, # comparando las instancias y no los datos de las cartas. self.wait_turn_timeout() end_hand = [id(card) for card in current_player.hand] self.assertNotEqual(start_hand, end_hand) # Caso de descarte (no cambia la mano al final): client = self.get_current_turn_client(clients) client.get_received() # Limpia recibidos # Descarte de 2 cartas self.discard_ok(client) self.discard_ok(client) current_player = game.turn_player() start_hand = [id(card) for card in current_player.hand] # Espera a fin de turno y se asegura que la última carta que quedaba (en # la posición 0) no ha sido modificada, es decir, que únicamente se han # descartado las dos cartas indicadas en el proceso de descarte. # esperado. self.wait_turn_timeout() end_hand = [id(card) for card in current_player.hand] self.assertEqual(start_hand[0], end_hand[0])
def wrapper(*args, **kwargs): game = session.get("game") if not game: return {"error": "No estás en una partida"} match = MM.get_match(game) if not match: # FIXME: creo que esto no podría llegar a pasar, porque para que # la partida no exista, el usuario ha tenido que salir. Sino, se # retiene la partida. return {"error": "La partida no existe"} if started and not match.is_started(): return {"error": "La partida no ha comenzado"} return f(*args, **kwargs)
def place_card(self, target_body, card, place_in_self=False): """ Se prueba a colocar la carta `card` en el cuerpo de otro jugador distinto (si `place_in_self` es True, el otro jugador es el mismo que usa la carta). El cuerpo inicial del jugador donde se va a colocar la carta será `target_body`. """ clients, code = self.create_game() # Primero se tendrá el game_update inicial received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) from gatovid.api.game.match import MM game = MM.get_match(code)._game turn_name = args["current_turn"] turn_client = clients[self.player_names.index(args["current_turn"])] turn_player = next(filter(lambda p: p.name == turn_name, game.players)) if place_in_self: other_player = turn_player else: other_player = next( filter(lambda p: p.name != turn_name, game.players)) turn_player.hand[0] = card other_player.body = target_body # Ignoramos los eventos anteriores _ = turn_client.get_received() # Intentamos colocar la carta en el jugador callback_args = turn_client.emit( "play_card", { "slot": 0, "target": other_player.name, "organ_pile": 0, }, callback=True, ) received = turn_client.get_received() return callback_args, received, turn_player
def play_card(data): """ .. warning:: Este endpoint está en construcción aún. Juega una carta de su mano. Requiere que el usuario esté en una partida y que esté empezada o se devolverá un :ref:`error <errores>`. :param data: Diccionario con datos sobre la carta a jugar. Todas las cartas deberán tener un campo ``slot`` (``int``) con el slot de la carta jugada en la mano del jugador. Adicionalmente, algunas cartas en concreto tendrán parámetros específicos: * Órgano, Virus y Medicina: * ``target`` (``str``): nombre del jugador destino * ``organ_pile`` (``int``): número de pila del jugador destino * Tratamientos: * Transplante: * ``target1`` (``str``): nombre de uno de los dos jugadores entre los que se intercambiarán los órganos. * ``target2`` (``str``): nombre del otro jugador. * ``organ_pile1`` (``str``): pila del jugador 1. * ``organ_pile2`` (``str``): pila del jugador 2. * Ladrón de Órganos: * ``target`` (``str``): nombre del jugador destino. * ``organ_pile`` (``int``): pila del jugador destino. * Error médico: * ``target`` (``str``): nombre del jugador destino :type data: Dict :return: Un mensaje :ref:`msg_game_update` para cada jugador. Si el usuario no está en una partida se devolverá un :ref:`error <errores>`. """ match = MM.get_match(session["game"]) name = session["user"].name try: match.run_action(name, PlayCard(data)) except GameLogicException as e: return {"error": str(e)}
def create_game(): """ Creación y unión automática a una partida privada. :return: Un mensaje de tipo :ref:`msg_create_game`. Si está buscando una partida pública, se devolverá un :ref:`error <errores>`. """ if session.get("game") is not None: return {"error": "El usuario ya está en una partida privada"} try: game_code = MM.create_private_game(owner=session["user"]) except GameLogicException as e: return {"error": str(e)} join(game_code) emit("create_game", {"code": game_code})
def test_fuzzy_ai(self): """ Fuzzy Test que prueba varios segundos en los que se usa Inteligencia Artificial en la partida para asegurar su correcto funcionamiento por lo general. Esto se puede conseguir de forma más eficiente si se consigue modificar el juego de forma que se pueda componer únicamente de IA. """ self.set_turn_timeout(0.5) clients, code = self.create_game() match = MM.get_match(code) game = match._game for player in game.players: player.is_ai = True # Ejecución de varios turnos time.sleep(5)
def play_pass(): """ .. warning:: Este endpoint está en construcción aún. Pasa el turno del usuario. Requiere que el usuario esté en una partida y que esté empezada o se devolverá un :ref:`error <errores>`. :return: Un mensaje :ref:`msg_game_update` para cada jugador. Si el usuario no está en una partida se devolverá un :ref:`error <errores>`. """ match = MM.get_match(session["game"]) name = session["user"].name try: match.run_action(name, Pass()) except GameLogicException as e: return {"error": str(e)}
def test_return_to_deck(self): """ Se comprueba que las cartas se devuelven a la baraja. En este test también se comprueba que las cartas usadas no dan problemas por tener parámetros incorrectos. """ TOTAL_CARDS = 30 # Generamos una baraja custom antes de que empiece la partida y se # repartan las cartas. custom_deck = [ Virus(color=Color.Red), Virus(color=Color.Red), Medicine(color=Color.Red), LatexGlove(), ] # Rellenamos las restantes con órganos. NOTE: no hacen falta 68 cartas # solo para 2 jugadores. for i in range(TOTAL_CARDS - 4): custom_deck.append(Organ(color=Color.Red)) self.set_custom_deck(custom_deck) def try_invalid(client, slot, pile_slot, target) -> None: payload = { "slot": slot, "organ_pile": pile_slot, "target": target, } items = payload.items() # Faltan de 1 a 3 campos for length in range(3): for payload in permutations(items, length): callback_args = client.emit("play_card", dict(payload), callback=True) self.assertIn("error", callback_args) # Tipos inválidos callback_args = client.emit("play_card", {"target": True}, callback=True) self.assertIn("error", callback_args) def try_use(slot, pile_cond, search_in, target) -> bool: pile_slot = None for (p_slot, pile) in enumerate(player.body.piles): if pile_cond(pile): pile_slot = p_slot break if pile_slot is not None: # Primero intenta las opciones inválidas try_invalid(client, slot, pile_slot, target) callback_args = client.emit( "play_card", { "slot": slot, "organ_pile": pile_slot, "target": target, }, callback=True, ) self.assertNotIn("error", callback_args) return pile_slot is not None clients, code = self.create_game(players=2) game = MM.get_match(code)._game game._turn = 0 def total_cards() -> int: count = len(game.deck) for player in game.players: count += len(player.hand) for pile in player.body.piles: if pile.is_empty(): continue count += 1 + len(pile.modifiers) return count # Ignoramos todos los mensajes anteriores for client in clients: _ = client.get_received() clients_order = list( map(lambda p: self.player_names.index(p.name), game.players)) for i in range(100): # Evitamos problemas con los saltos de turno p = game._turn self.assertEqual(total_cards(), TOTAL_CARDS) which_client = clients_order[p] client = clients[which_client] player = game.players[p] other_player = game.players[(p + 1) % 2] card_types = list(map(lambda c: c.card_type, player.hand)) if "treatment" in card_types: slot = card_types.index("treatment") callback_args = client.emit("play_card", {"slot": slot}, callback=True) self.assertNotIn("error", callback_args) continue if "organ" in card_types: slot = card_types.index("organ") if player.body.organ_unique(player.hand[slot]): if try_use( slot=slot, search_in=player.body.piles, pile_cond=lambda p: p.is_empty(), target=player.name, ): continue if "virus" in card_types: slot = card_types.index("virus") if try_use( slot=slot, search_in=other_player.body.piles, pile_cond=lambda p: not p.is_empty(), target=other_player.name, ): continue if "medicine" in card_types: slot = card_types.index("medicine") if try_use( slot=slot, search_in=player.body.piles, pile_cond=lambda p: not p.is_empty(), target=player.name, ): continue # Descartamos todas las cartas for i in reversed(range(3)): callback_args = client.emit("play_discard", i, callback=True) self.assertNotIn("error", callback_args) callback_args = client.emit("play_pass", callback=True) self.assertNotIn("error", callback_args)
def test_treatment_infection(self): """ Se prueba a usar el tratamiento Contagio. """ clients, code = self.create_game(players=3) # Primero se tendrá el game_update inicial received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) game = MM.get_match(code)._game # Forzamos el turno al client 0 game._turn = 0 def organ(color: Color) -> OrganPile: return OrganPile.from_data(organ=Organ(color=color)) def infected_organ(color: Color, virus_color: Color = None) -> OrganPile: if virus_color is None: virus_color = color return OrganPile.from_data( organ=Organ(color=color), modifiers=[ Virus(color=virus_color), ], ) bodies = [ { "have": [ infected_organ(Color.Yellow), infected_organ(Color.Red, virus_color=Color.All), infected_organ(Color.Blue), # El virus de este no se debería colocar en ningún sitio infected_organ(Color.Green), ], "expected": [ organ(Color.Yellow), organ(Color.Red), organ(Color.Blue), infected_organ(Color.Green), ], }, { "have": [ # No se debería colocar en esta infected_organ(Color.Green, virus_color=Color.All), OrganPile(), organ(Color.Blue), # Se debería colocar el multicolor organ(Color.Red), ], "expected": [ infected_organ(Color.Green, virus_color=Color.All), OrganPile(), infected_organ(Color.Blue), infected_organ(Color.Red, virus_color=Color.All), ], }, { "have": [ OrganPile(), # No se debería colocar en este OrganPile.from_data( organ=Organ(color=Color.Green), modifiers=[Medicine(color=Color.Green)], ), OrganPile(), organ(Color.Yellow), ], "expected": [ OrganPile(), OrganPile.from_data( organ=Organ(color=Color.Green), modifiers=[Medicine(color=Color.Green)], ), OrganPile(), infected_organ(Color.Yellow), ], }, ] clients_order = list( map(lambda p: self.player_names.index(p.name), game.players)) # Para todos los clientes, inicializamos su cuerpo al cuerpo de pruebas # y le damos la carta de contagio al cliente 0. for (i, which_client) in enumerate(clients_order): client = clients[which_client] player = game.players[i] if which_client == 0: player.hand[0] = Infection() player.body = Body.from_data(piles=bodies[i]["have"]) # Ignoramos los eventos anteriores con los clientes for client in clients: _ = client.get_received() # Usamos la carta desde el cliente 0 callback_args = clients[0].emit("play_card", {"slot": 0}, callback=True) self.assertNotIn("error", callback_args) # Comprobamos que todos los clientes reciben los cuerpos esperados. for (i, which_client) in enumerate(clients_order): client = clients[which_client] player = game.players[i] received = client.get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("bodies", args) self.assertIn(player.name, args["bodies"]) expected = list( map( lambda b: asdict(b, dict_factory=asdict_factory_enums), bodies[i]["expected"], )) self.assertEqual(args["bodies"][player.name], expected)
def test_treatment_organ_thief(self): """ Se prueba a usar el tratamiento Ladrón de órganos. """ clients, code = self.create_game() caller_name = GENERIC_USERS_NAME.format(0) target_name = GENERIC_USERS_NAME.format(1) game = MM.get_match(code)._game # Forzamos el turno al client 0 game._turn = 0 caller_player = game.players[game._turn] target_player = game.players[(game._turn + 1) % 2] caller_player.hand[0] = OrganThief() caller_player.body = Body.from_data(piles=[ OrganPile(), OrganPile.from_data(organ=Organ(color=Color.Red)), OrganPile.from_data( organ=Organ(color=Color.Blue), modifiers=[ Virus(color=Color.Blue), ], ), OrganPile(), ]) target_player.body = Body.from_data(piles=[ OrganPile.from_data(organ=Organ(color=Color.Green)), OrganPile(), OrganPile.from_data( organ=Organ(color=Color.Yellow), modifiers=[ Virus(color=Color.Yellow), ], ), OrganPile(), ]) # Guardamos en el cliente el cuerpo anterior clients[1].last_pile = asdict(target_player.body)["piles"][2] # Ignoramos los eventos anteriores con todos los clientes for client in clients: _ = client.get_received() # Usamos la carta desde el cliente 0 callback_args = clients[0].emit( "play_card", { "slot": 0, "target": target_name, "organ_pile": 2, }, callback=True, ) self.assertNotIn("error", callback_args) # Comprobamos que todos los clientes reciben los cuerpos intercambiados. for client in clients: received = client.get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("bodies", args) self.assertIn(caller_name, args["bodies"]) self.assertIn(target_name, args["bodies"]) self.assertEqual(args["bodies"][caller_name][0], clients[1].last_pile) self.assertEqual(args["bodies"][target_name][2], asdict(OrganPile()))
def test_treatment_medical_error(self): """ Se prueba a usar el tratamiento Error Médico. """ clients, code = self.create_game() caller_name = GENERIC_USERS_NAME.format(0) target_name = GENERIC_USERS_NAME.format(1) # Primero se tendrá el game_update inicial received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) game = MM.get_match(code)._game # Forzamos el turno al client 0 game._turn = 0 caller_player = game.players[game._turn] caller_player.hand[0] = MedicalError() caller_player.body = Body.from_data(piles=[ OrganPile(), OrganPile.from_data(organ=Organ(color=Color.Red)), OrganPile.from_data( organ=Organ(color=Color.Blue), modifiers=[ Virus(color=Color.Blue), ], ), OrganPile(), ]) clients[0].last_body = asdict(caller_player.body)["piles"] # Ignoramos los eventos anteriores con el target received = clients[1].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) # Guardamos en el cliente el cuerpo anterior clients[1].last_body = asdict(Body())["piles"] # Ignoramos los eventos anteriores con el resto de los clientes for client in clients[2:]: _ = client.get_received() # Usamos la carta desde el cliente 0 callback_args = clients[0].emit( "play_card", { "slot": 0, "target": target_name, }, callback=True, ) self.assertNotIn("error", callback_args) # Comprobamos que todos los clientes reciben los cuerpos intercambiados. for client in clients: received = client.get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("bodies", args) self.assertIn(caller_name, args["bodies"]) self.assertIn(target_name, args["bodies"]) self.assertEqual(args["bodies"][caller_name], clients[1].last_body) self.assertEqual(args["bodies"][target_name], clients[0].last_body)
def test_treatment_latex_glove(self): """ Se prueba a usar el tratamiento Guante de látex y se comprueba que el resto de jugadores han descartado las cartas, mientras que el jugador que lanza el tratamiento conserva su mano (excepto la carta de tratamiento). """ # HACK: Establecemos siempre la misma semilla para evitar el caso en el # que el random genere una mano igual a la que se tenia anteriormente. random.seed(10) clients, code = self.create_game() # Primero se tendrá el game_update inicial received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) game = MM.get_match(code)._game # Forzamos el turno al client 0 game._turn = 0 turn_player = game.players[game._turn] turn_player.hand[0] = LatexGlove() last_hand = list(map(asdict, turn_player.hand.copy())) # Ignoramos los eventos anteriores en el resto de jugadores y guardamos # la mano anterior de estos. for client in clients[1:]: received = client.get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) # Guardamos en el cliente la mano anterior (para iterar facilmente) client.last_hand = args["hand"].copy() # Usamos la carta desde el cliente 0 callback_args = clients[0].emit( "play_card", { "slot": 0, }, callback=True, ) self.assertNotIn("error", callback_args) for client in clients[1:]: # Comprobamos que al resto de jugadores les ha borrado la mano (se # habrán robado nuevas cartas al saltarles el turno). received = client.get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("hand", args) self.assertNotEqual(args["hand"], client.last_hand) # Comprobamos que el cliente que la lanza conserva su mano received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) self.assertNotIn("error", args) self.assertIn("hand", args) self.assertEqual(args["hand"][0], last_hand[1]) self.assertEqual(args["hand"][1], last_hand[2])
def test_player_leaderboard(self): """ Se comprueba la recepción del leaderboard cuando todos los jugadores han acabado. """ TOTAL_CARDS = 68 # Generamos una baraja custom antes de que empiece la partida y se # repartan las cartas. Todas las cartas serán órganos para solo permitir # ganar la partida. custom_deck = [] for i in range(int(TOTAL_CARDS / 4)): custom_deck.append(Organ(color=Color.Red)) custom_deck.append(Organ(color=Color.Green)) custom_deck.append(Organ(color=Color.Blue)) custom_deck.append(Organ(color=Color.Yellow)) self.set_custom_deck(custom_deck) def try_use(slot, pile_cond, search_in, target) -> bool: pile_slot = None for (p_slot, pile) in enumerate(player.body.piles): if pile_cond(pile): pile_slot = p_slot break if pile_slot is not None: callback_args = client.emit( "play_card", { "slot": slot, "organ_pile": pile_slot, "target": target, }, callback=True, ) self.assertNotIn("error", callback_args) return pile_slot is not None clients, code = self.create_game() game = MM.get_match(code)._game game._turn = 0 # Ignoramos todos los mensajes anteriores for client in clients: _ = client.get_received() clients_order = list( map(lambda p: self.player_names.index(p.name), game.players) ) players_finished = [] leaderboards_received = 0 for i in range(100): # Evitamos problemas con los saltos de turno p = game._turn which_client = clients_order[p] client = clients[which_client] player = game.players[p] # Tratamos de colocar alguno de los órganos could_place = False for (slot, organ) in enumerate(player.hand): if player.body.organ_unique(player.hand[slot]): if try_use( slot=slot, search_in=player.body.piles, pile_cond=lambda p: p.is_empty(), target=player.name, ): could_place = True break # Si no hemos podido colocar un órgano, descartamos toda la mano if not could_place: # Descartamos todas las cartas for i in reversed(range(3)): callback_args = client.emit("play_discard", i, callback=True) self.assertNotIn("error", callback_args) callback_args = client.emit("play_pass", callback=True) self.assertNotIn("error", callback_args) # Obtenemos el game_update received = clients[0].get_received() _, args = self.get_msg_in_received(received, "game_update", json=True) if args.get("leaderboard") is not None: # Si hemos recibido el leaderboard, es porque el jugador actual # ha acabado. players_finished.append(player.name) leaderboards_received += 1 # Debería estar su nombre en la clasificación. self.assertIn(player.name, args["leaderboard"]) # Si hemos contado 5 jugadores finalizados, deberíamos haber # recibido también el finished y el playtime_mins. if len(players_finished) == 5: self.assertIn("finished", args) self.assertIn("playtime_mins", args) self.assertEqual(args["finished"], True) expected_leaderboard = dict() for player in game.players: if player.name not in players_finished: players_finished.append(player.name) for (pos, player) in enumerate(players_finished): pos = pos + 1 expected_leaderboard[player] = { "position": pos, "coins": 10 * (6 - pos), } self.assertEqual(args["leaderboard"], expected_leaderboard) break # Comprobamos que realmente ha acabado la partida self.assertEqual(len(players_finished), 6) # Y que se han recibido todos los leaderboards esperados self.assertEqual(leaderboards_received, 5)
def join(game_code): """ Unión a una partida proporcionando un código de partida. Si se proporciona un código con minúsculas se intentará con el equivalente en mayúsculas. :param game_code: Código de partida :type game_code: ``str`` :return: Si la partida es privada, un broadcast de :ref:`msg_users_waiting`. En cualquier caso, un broadcast de :ref:`msg_chat` indicando que el jugador se ha unido a la partida. Un jugador no se puede unir a una partida si ya está en otra o si ya está llena. En caso contrario se devolverá un :ref:`error <errores>`. """ if session.get("game"): return {"error": "Ya estás en una partida"} if type(game_code) is not str: return {"error": "Tipo incorrecto para el código de partida"} # No importa si es minúsculas game_code = game_code.upper() # Restricciones para unirse a la sala match = MM.get_match(game_code) if match is None or len(match.users) > MAX_MATCH_USERS: return {"error": "La partida no existe o está llena"} # Guardamos la partida actual en la sesión session["game"] = game_code # Actualizamos los datos del usuario. NOTE: estos ya serán los # definitivos, no los puede modificar a mitad de partida. session["user"] = db.session.query(User).get(session["user"].email) db.session.commit() session["user"].sid = request.sid # Unimos al usuario a la sesión de socketio join_room(game_code) # Comprobar si es una reconexión y en ese caso indicarle que empiece # directamente. can_rejoin, initial_update = match.check_rejoin(session["user"]) if can_rejoin: logger.info(f"User {session['user']} reconnecting to game") # Actualizamos el SID del usuario y el nombre si lo ha cambiado try: match.update_user(session["user"]) except GameLogicException as e: # NOTE: No debería darse este error por la condición de # can_rejoin, pero por curarnos en salud. return {"error": str(e)} emit("start_game", room=session["user"].sid) emit("game_update", initial_update, room=session["user"].sid) return # Guardamos al jugador en la partida try: match.add_user(session["user"]) except GameLogicException as e: return {"error": str(e)} if isinstance(match, PrivateMatch): # Si es una partida privada, informamos a todos los de la sala del nuevo # número de jugadores. El lider decidirá cuando iniciarla. emit("users_waiting", len(match.users), room=game_code) else: # Si es una partida pública, iniciamos la partida si ya están todos. # Si hay algún jugador que no se une a la partida, la partida acabará # empezando (si hay suficientes jugadores) debido al start_timer en # PublicMatch. if len(match.users) == match.num_users: match.start() emit( "chat", { "msg": session["user"].name + " se ha unido a la partida", "owner": "[GATOVID]", }, room=game_code, ) logger.info(f"User {session['user'].name} has joined the game {game_code}")
def leave(): """ Salir de la partida actual. :return: * Si la partida no se borra porque quedan jugadores: - Un mensaje de tipo :ref:`msg_users_waiting`. - Un broadcast de :ref:`msg_chat` indicando que el jugador ha abandonado la partida. - Si se ha delegado el cargo de líder, el nuevo líder recibirá un mensaje de tipo :ref:`msg_game_owner`. * Si la partida se borra porque no quedan jugadores: - Si ya había terminado, un :ref:`error <errores>`. - Si no había terminado y se ha cancelado, un mensaje de tipo :ref:`msg_game_cancelled`. Requiere que el usuario esté en una partida o se devolverá un :ref:`error <errores>`. """ # No hay partida de la que salir ni limpieza que hacer if session.get("game") is None: return {"error": "No hay ninguna partida de la que salir"} game_code = session["game"] leave_room(game_code) emit( "chat", { "msg": session["user"].name + " ha abandonado la partida", "owner": "[GATOVID]", }, room=game_code, ) del session["game"] match = MM.get_match(game_code) if match is None: return # Limpieza de partidas ya canceladas, no hace falta seguir match.remove_user(session["user"]) logger.info(f"User {session['user'].name} has left the game {game_code}") if len(match.users) == 0: match.end() MM.remove_match(game_code) return # La partida ha acabado, no seguir emit("users_waiting", len(match.users), room=game_code) # Comprobar si hay que delegar el cargo de lider if isinstance(match, PrivateMatch): if match.owner == session["user"]: # Si él es el lider, delegamos el cargo de lider a otro jugador match.owner = match.users[0] emit( "chat", { "msg": match.owner.name + " es el nuevo líder", "owner": "[GATOVID]", }, room=game_code, ) # Mensaje solo al nuevo dueño de la sala emit("game_owner", room=match.owner.sid)