From 5b98907c72843b15ffd5f46e587b4ad51082b261 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 3 Oct 2023 17:52:54 -0300 Subject: [PATCH 001/224] Se agrego archivo de pyenv al .gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b528d7d..6fa06f6 100644 --- a/.gitignore +++ b/.gitignore @@ -83,9 +83,7 @@ profile_default/ ipython_config.py # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. From f48215937f68ab941bdad999f1c27578b4df8139 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 4 Oct 2023 10:41:03 -0300 Subject: [PATCH 002/224] Cambio menor en Makefile y formateo de los archivos para que cumplan estandar pep8 --- Makefile | 3 +- app/tests/card_tests/test_create_cards.py | 154 +++++++++--------- .../game_tests/test_creation_of_games.py | 3 +- app/tests/player_tests/test_create_player.py | 2 + 4 files changed, 83 insertions(+), 79 deletions(-) diff --git a/Makefile b/Makefile index 294f3ba..6fcde47 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,8 @@ coverage-clean: # Define the 'autopep8' target for running autopep8 autopep8: - poetry run autopep8 --in-place --recursive . + @echo "Standarizing all files to pep8..." + @poetry run autopep8 --in-place --recursive . # Define the 'install' target to install dependencies and create the virtual environment install: pyproject.toml diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index 73df223..de93da1 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -16,12 +16,12 @@ def cleanup_database(): def test_create_cards_succesfully(): cleanup_database() - for i in range(4,13): + for i in range(4, 13): card_data = { "number": i, - "type":'THE_THING', - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" + "type": 'THE_THING', + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" } response = client.post("/cards", json=card_data) @@ -34,9 +34,9 @@ def test_create_cards_succesfully(): assert response.json() == { "number": i, - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" } cleanup_database() @@ -57,26 +57,25 @@ def test_create_card_missing_number(): response = client.post( "/cards", json={ - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() - def test_create_card_bad_number(): cleanup_database() response = client.post( "/cards", json={ - "number": "bad", - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "number": "bad", + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -87,25 +86,26 @@ def test_create_card_low_number(): response = client.post( "/cards", json={ - "number": 3, - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "number": 3, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() + def test_create_card_high_number(): cleanup_database() response = client.post( "/cards", json={ - "number": 13, - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "number": 13, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -117,11 +117,11 @@ def test_create_card_missing_type(): response = client.post( "/cards", json={ - "number": 4, - "name":"The Thing", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "name": "The Thing", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -132,25 +132,27 @@ def test_create_card_bad_type(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THIN", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "type": "THE_THIN", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() # Test card names + + def test_create_card_missing_name(): cleanup_database() response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "type": "THE_THING", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -161,11 +163,11 @@ def test_create_card_bad_name(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":3, - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "type": "THE_THING", + "name": 3, + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -176,11 +178,11 @@ def test_create_card_short_name(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"a", - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "type": "THE_THING", + "name": "a", + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -191,11 +193,11 @@ def test_create_card_large_name(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"a"*51, - "description":"You are the thing, infect or kill everyone" - }, + "number": 4, + "type": "THE_THING", + "name": "a"*51, + "description": "You are the thing, infect or kill everyone" + }, ) assert response.status_code == 422 cleanup_database() @@ -207,10 +209,10 @@ def test_create_card_missing_description(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"The Thing" - }, + "number": 4, + "type": "THE_THING", + "name": "The Thing" + }, ) assert response.status_code == 422 cleanup_database() @@ -221,11 +223,11 @@ def test_create_card_bad_description(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"The Thing", - "description":22 - }, + "number": 4, + "type": "THE_THING", + "name": "The Thing", + "description": 22 + }, ) assert response.status_code == 422 cleanup_database() @@ -236,11 +238,11 @@ def test_create_card_short_description(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"The Thing", - "description":"ee" - }, + "number": 4, + "type": "THE_THING", + "name": "The Thing", + "description": "ee" + }, ) assert response.status_code == 422 cleanup_database() @@ -251,11 +253,11 @@ def test_create_card_large_description(): response = client.post( "/cards", json={ - "number": 4, - "type":"THE_THING", - "name":"The Thing", - "description":"You are the thing, infect or kill everyone"*1000 - }, + "number": 4, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone"*1000 + }, ) assert response.status_code == 422 - cleanup_database() \ No newline at end of file + cleanup_database() diff --git a/app/tests/game_tests/test_creation_of_games.py b/app/tests/game_tests/test_creation_of_games.py index d04430d..7c4c1a4 100644 --- a/app/tests/game_tests/test_creation_of_games.py +++ b/app/tests/game_tests/test_creation_of_games.py @@ -3,6 +3,7 @@ from app.database.models import Game, Player from app.routers.games.schemas import GameCreationOut from pony.orm import db_session +import pytest client = TestClient(app) @@ -45,7 +46,6 @@ def test_create_successfull_game(): assert game_creation_response.max_players == game_data["max_players"] assert game_creation_response.is_private == True assert game_creation_response.host_player_id == test_player.id - cleanup_database() @@ -59,7 +59,6 @@ def test_create_without_player_in_ddbb(): "host_player_id": 1 } response = client.post("/games", json=game_data) - assert response.status_code == 404 cleanup_database() diff --git a/app/tests/player_tests/test_create_player.py b/app/tests/player_tests/test_create_player.py index 9219571..9ff3878 100644 --- a/app/tests/player_tests/test_create_player.py +++ b/app/tests/player_tests/test_create_player.py @@ -16,6 +16,7 @@ def test_create_player(): 'name': 'pepito', } + def test_create_player_with_same_name(): response = client.post( '/players', @@ -27,6 +28,7 @@ def test_create_player_with_same_name(): 'name': 'pepito', } + def test_create_player_bad_body(): response = client.post( "/players", From 1434f5ac65420690bb8bda2e47e32dd6d1771407 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 7 Oct 2023 12:33:59 -0300 Subject: [PATCH 003/224] Se reduce el largo de los mensajes cuando un test falla con un mensaje mas chico del problema, y se agrega a Makefile un nuevo objetivo con la idea de que muestre los comandos disponibles de la aplicacion. --- Makefile | 19 +++++++++-- app/tests/card_tests/test_create_cards.py | 34 +++++++++---------- .../game_tests/test_creation_of_games.py | 23 +++++++------ app/tests/game_tests/test_delete_of_games.py | 9 ++--- .../game_tests/test_find_game_by_name.py | 8 ++--- app/tests/game_tests/test_game_information.py | 18 +++++----- app/tests/game_tests/test_join_players.py | 30 ++++++++-------- app/tests/game_tests/test_list_of_games.py | 8 ++--- app/tests/game_tests/test_update_game.py | 8 ++--- app/tests/player_tests/test_create_player.py | 16 ++++----- app/tests/player_tests/test_delete_player.py | 8 ++--- app/tests/player_tests/test_read_player.py | 12 +++---- app/tests/player_tests/test_update_player.py | 8 ++--- 13 files changed, 110 insertions(+), 91 deletions(-) diff --git a/Makefile b/Makefile index 6fcde47..96f85f9 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,22 @@ COV_CARDS_FILE = .coverage.cards # Define the UVicorn command UVICORN_CMD = uvicorn $(MAIN_FILE):$(APP_NAME) --port $(PORT) --reload -.PHONY: run delete-db coverage-report coverage-clean test-all test-players test-games +.PHONY: run delete-db coverage-report coverage-clean test-all test-players test-games help + +# Define the 'help' target to display Makefile usage information +help: + @echo "Usage: make [target]" + @echo "Targets:" + @echo " run - Run the UVicorn server" + @echo " delete-db - Delete the database file" + @echo " coverage-report - Generate coverage reports" + @echo " coverage-clean - Remove coverage reports" + @echo " test-all - Run all tests sequentially" + @echo " test-players - Run player tests" + @echo " test-games - Run game tests" + @echo " test-cards - Run card tests" + @echo " autopep8 - Run autopep8 to format code" + @echo " install - Install dependencies and create virtual environment" # Define the 'run' target to run the UVicorn server within the virtual environment run: install @@ -50,7 +65,7 @@ TEST_DIRECTORY = ./app/tests # Define the 'test-games' target to run game tests in the test environment test-games: install - ENVIRONMENT=test poetry run coverage run --data-file=$(COV_GAMES_FILE) -m pytest -vv $(TEST_DIRECTORY)/game_tests; true + ENVIRONMENT=test poetry run coverage run --data-file=$(COV_GAMES_FILE) -m pytest --tb=no -vv $(TEST_DIRECTORY)/game_tests; true rm -f $(TEST_DB_FILE) unset ENVIRONMENT diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index de93da1..d75f68c 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -26,7 +26,7 @@ def test_create_cards_succesfully(): response = client.post("/cards", json=card_data) - assert response.status_code == 201 + assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." with db_session: created_card = Card.get(name=card_data["name"]) @@ -37,7 +37,7 @@ def test_create_cards_succesfully(): "type": "THE_THING", "name": "The Thing", "description": "You are the thing, infect or kill everyone" - } + }, "El contenido de la respuesta no coincide con los datos de la carta creada." cleanup_database() @@ -47,7 +47,7 @@ def test_create_card_bad_body(): "/cards", json={"bad": "body"}, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -62,7 +62,7 @@ def test_create_card_missing_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -77,7 +77,7 @@ def test_create_card_bad_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -92,7 +92,7 @@ def test_create_card_low_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -107,7 +107,7 @@ def test_create_card_high_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -123,7 +123,7 @@ def test_create_card_missing_type(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -138,7 +138,7 @@ def test_create_card_bad_type(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() # Test card names @@ -154,7 +154,7 @@ def test_create_card_missing_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -169,7 +169,7 @@ def test_create_card_bad_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -184,7 +184,7 @@ def test_create_card_short_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -199,7 +199,7 @@ def test_create_card_large_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -214,7 +214,7 @@ def test_create_card_missing_description(): "name": "The Thing" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -229,7 +229,7 @@ def test_create_card_bad_description(): "description": 22 }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -244,7 +244,7 @@ def test_create_card_short_description(): "description": "ee" }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() @@ -259,5 +259,5 @@ def test_create_card_large_description(): "description": "You are the thing, infect or kill everyone"*1000 }, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." cleanup_database() diff --git a/app/tests/game_tests/test_creation_of_games.py b/app/tests/game_tests/test_creation_of_games.py index 7c4c1a4..2b51432 100644 --- a/app/tests/game_tests/test_creation_of_games.py +++ b/app/tests/game_tests/test_creation_of_games.py @@ -33,19 +33,22 @@ def test_create_successfull_game(): response = client.post("/games", json=game_data) - assert response.status_code == 201 + assert response.status_code == 201, "El codigo de estado de la respuesta no es 201(Created)" with db_session: created_game = Game.get(name=game_data["name"]) assert created_game is not None game_creation_response = GameCreationOut(**response.json()) - assert game_creation_response.name == game_data["name"] - assert game_creation_response.status == "UNSTARTED" - assert game_creation_response.min_players == game_data["min_players"] - assert game_creation_response.max_players == game_data["max_players"] - assert game_creation_response.is_private == True - assert game_creation_response.host_player_id == test_player.id + assert game_creation_response.name == game_data[ + "name"], "El nombre del juego en la respuesta no coincide con el nombre proporcionado." + assert game_creation_response.status == "UNSTARTED", "El estado del juego en la respuesta no es 'UNSTARTED'." + assert game_creation_response.min_players == game_data[ + "min_players"], "El número mínimo de jugadores en la respuesta no coincide con el valor proporcionado." + assert game_creation_response.max_players == game_data[ + "max_players"], "El número máximo de jugadores en la respuesta no coincide con el valor proporcionado." + assert game_creation_response.is_private == True, "El juego no se creó como privado en la respuesta." + assert game_creation_response.host_player_id == test_player.id, "El ID del jugador anfitrión en la respuesta no coincide con el ID del jugador de prueba." cleanup_database() @@ -59,7 +62,7 @@ def test_create_without_player_in_ddbb(): "host_player_id": 1 } response = client.post("/games", json=game_data) - assert response.status_code == 404 + assert response.status_code == 404, "Se esperaba un código de estado 404 (Not Found) ya que el jugador no existe en la base de datos." cleanup_database() @@ -75,7 +78,7 @@ def test_create_game_with_a_host_already_hosting(): } for i in range(2): response = client.post("/games", json=game_data) - assert response.status_code == 400 + assert response.status_code == 400, "Se esperaba un código de estado 400 (Bad Request) ya que el jugador ya está hospedando un juego." cleanup_database() @@ -100,5 +103,5 @@ def test_create_game_with_same_name(): ] for i in game_data: response = client.post("/games", json=i) - assert response.status_code == 400 + assert response.status_code == 400, "Se esperaba un código de estado 400 (Bad Request) ya que un juego con el mismo nombre ya existe." cleanup_database() diff --git a/app/tests/game_tests/test_delete_of_games.py b/app/tests/game_tests/test_delete_of_games.py index 620143d..dbd3099 100644 --- a/app/tests/game_tests/test_delete_of_games.py +++ b/app/tests/game_tests/test_delete_of_games.py @@ -62,17 +62,18 @@ def test_delete_game_success(mocker): mocker.patch.object(FakeGame, "delete", return_value=None) response = client.delete("/games/game1") - assert response.status_code == 200 - assert response.json() == {"message": "Game deleted"} + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." + assert response.json() == { + "message": "Game deleted"}, "El mensaje de la respuesta no es 'Game deleted' (Juego eliminado)." def test_delete_game_with_invalid_name(mocker): mocker.patch.object(Game, "get", return_value=games[0]) response = client.delete("/games/game2") - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." def test_delete_non_existing_game(mocker): mocker.patch.object(Game, "get", return_value=None) response = client.delete("/games/non_existing_game") - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." diff --git a/app/tests/game_tests/test_find_game_by_name.py b/app/tests/game_tests/test_find_game_by_name.py index c0d1690..9962ecb 100644 --- a/app/tests/game_tests/test_find_game_by_name.py +++ b/app/tests/game_tests/test_find_game_by_name.py @@ -36,8 +36,8 @@ def test_find_game_successfully(): create_test_game(name="TestGame", min_players=4, max_players=6, password="secret", host_player_id=test_player.id) response = find_game_by_name("TestGame") - assert response.name == "TestGame" - assert response.min_players == 4 - assert response.max_players == 6 - assert response.password == "secret" + assert response.name == "TestGame", "El nombre del juego no coincide con el esperado." + assert response.min_players == 4, "El número mínimo de jugadores no coincide con el esperado." + assert response.max_players == 6, "El número máximo de jugadores no coincide con el esperado." + assert response.password == "secret", "La contraseña del juego no coincide con la esperada." cleanup_database() diff --git a/app/tests/game_tests/test_game_information.py b/app/tests/game_tests/test_game_information.py index ccff884..1574e3f 100644 --- a/app/tests/game_tests/test_game_information.py +++ b/app/tests/game_tests/test_game_information.py @@ -38,19 +38,19 @@ def test_information_of_created_game(): response = client.get("/games/TestGame") assert response.status_code == 200 game_information_response = GameInformationOut(**response.json()) - assert game_information_response.name == "TestGame" - assert game_information_response.status == "UNSTARTED" - assert game_information_response.min_players == 4 - assert game_information_response.max_players == 6 - assert game_information_response.is_private == True - assert game_information_response.host_player_id == test_player.id - assert game_information_response.host_player_name == test_player.name - assert game_information_response.num_of_players == 1 + assert game_information_response.name == "TestGame", "El nombre del juego no coincide con el esperado." + assert game_information_response.status == "UNSTARTED", "El estado del juego no coincide con el esperado." + assert game_information_response.min_players == 4, "El número mínimo de jugadores no coincide con el esperado." + assert game_information_response.max_players == 6, "El número máximo de jugadores no coincide con el esperado." + assert game_information_response.is_private == True, "El juego no se marcó como privado en la respuesta." + assert game_information_response.host_player_id == test_player.id, "El ID del jugador anfitrión no coincide con el esperado." + assert game_information_response.host_player_name == test_player.name, "El nombre del jugador anfitrión no coincide con el esperado." + assert game_information_response.num_of_players == 1, "El número de jugadores no coincide con el esperado." cleanup_database() def test_information_of_game_not_found(): cleanup_database() response = client.get("/games/InexistentGame") - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." cleanup_database() diff --git a/app/tests/game_tests/test_join_players.py b/app/tests/game_tests/test_join_players.py index 082433e..ba69e24 100644 --- a/app/tests/game_tests/test_join_players.py +++ b/app/tests/game_tests/test_join_players.py @@ -43,17 +43,17 @@ def test_join_player_succesfully(): response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." game_join_response = GameInformationOut(**response.json()) - assert game_join_response.name == "TestGame" - assert game_join_response.status == "UNSTARTED" - assert game_join_response.min_players == 4 - assert game_join_response.max_players == 6 - assert game_join_response.is_private == True - assert game_join_response.host_player_id == test_player1.id - assert game_join_response.host_player_name == "Ignacio" - assert game_join_response.num_of_players == 2 + assert game_join_response.name == "TestGame", "El nombre del juego no coincide con el esperado." + assert game_join_response.status == "UNSTARTED", "El estado del juego no coincide con el esperado." + assert game_join_response.min_players == 4, "El número mínimo de jugadores no coincide con el esperado." + assert game_join_response.max_players == 6, "El número máximo de jugadores no coincide con el esperado." + assert game_join_response.is_private == True, "El juego no se marcó como privado en la respuesta." + assert game_join_response.host_player_id == test_player1.id, "El ID del jugador anfitrión no coincide con el esperado." + assert game_join_response.host_player_name == "Ignacio", "El nombre del jugador anfitrión no coincide con el esperado." + assert game_join_response.num_of_players == 2, "El número de jugadores no coincide con el esperado." cleanup_database() @@ -66,7 +66,7 @@ def test_join_game_not_found(): "password": "secret" } response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." cleanup_database() @@ -81,7 +81,7 @@ def test_join_game_player_not_found(): "password": "secret" } response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." cleanup_database() @@ -97,7 +97,7 @@ def test_join_game_player_already_in_game(): } for i in range(2): response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." cleanup_database() @@ -117,7 +117,7 @@ def test_join_game_full_of_players(): "password": "secret" } response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." cleanup_database() @@ -132,7 +132,7 @@ def test_join_game_with_invalid_password(): "password": "wrong_password" } response = client.patch("/games/join/TestGame", json=game_data) - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." cleanup_database() @@ -151,5 +151,5 @@ def test_join_game_with_player_in_another_game(): } client.patch("/games/join/TestGame1", json=game_data) response = client.patch("/games/join/TestGame2", json=game_data) - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." cleanup_database() diff --git a/app/tests/game_tests/test_list_of_games.py b/app/tests/game_tests/test_list_of_games.py index 003127b..2ff7fc7 100644 --- a/app/tests/game_tests/test_list_of_games.py +++ b/app/tests/game_tests/test_list_of_games.py @@ -57,14 +57,14 @@ def __init__( def test_empty_get_games(mocker): mocker.patch.object(Game, "select", return_value=[]) response = client.get("/games") - assert response.status_code == 200 - assert response.json() == [] + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." + assert response.json() == [], "El contenido de la respuesta no está vacío como se esperaba." def test_get_one_game(mocker): mocker.patch.object(Game, "select", return_value=[games[0]]) response = client.get("/games") - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." assert response.json() == [ { "name": "game1", @@ -75,4 +75,4 @@ def test_get_one_game(mocker): "is_private": False, "num_of_players": 3 } - ] + ], "El contenido de la respuesta no coincide con el juego esperado." diff --git a/app/tests/game_tests/test_update_game.py b/app/tests/game_tests/test_update_game.py index a2890c9..037dc9c 100644 --- a/app/tests/game_tests/test_update_game.py +++ b/app/tests/game_tests/test_update_game.py @@ -63,14 +63,14 @@ def test_update_game_successful(mocker): } response = client.patch("games/game1", json=request_data) - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." assert response.json() == { "name": "game1", "min_players": 5, "max_players": 7, "is_private": True, "status": "UNSTARTED" - } + }, "El contenido de la respuesta no coincide con los datos actualizados." def test_update_non_existing_game(mocker): @@ -81,7 +81,7 @@ def test_update_non_existing_game(mocker): "password": "new_password" } response = client.patch("/games/non_existing_game", json=request_data) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." def test_update_game_with_wrong_name(mocker): @@ -92,4 +92,4 @@ def test_update_game_with_wrong_name(mocker): "password": "new_password" } response = client.patch("/games/wrong_game_name", json=request_data) - assert response.status_code == 400 + assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." diff --git a/app/tests/player_tests/test_create_player.py b/app/tests/player_tests/test_create_player.py index 9ff3878..9ceb357 100644 --- a/app/tests/player_tests/test_create_player.py +++ b/app/tests/player_tests/test_create_player.py @@ -10,11 +10,11 @@ def test_create_player(): '/players', json={'name': 'pepito'} ) - assert response.status_code == 201 + assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." assert response.json() == { 'id': 1, 'name': 'pepito', - } + }, "El contenido de la respuesta no coincide con los datos del jugador creado." def test_create_player_with_same_name(): @@ -22,11 +22,11 @@ def test_create_player_with_same_name(): '/players', json={'name': 'pepito'} ) - assert response.status_code == 201 + assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." assert response.json() == { 'id': 2, 'name': 'pepito', - } + }, "El contenido de la respuesta no coincide con los datos del jugador creado." def test_create_player_bad_body(): @@ -34,7 +34,7 @@ def test_create_player_bad_body(): "/players", json={"bad": "body"}, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." def test_create_player_bad_name(): @@ -42,7 +42,7 @@ def test_create_player_bad_name(): "/players", json={"name": 3}, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." def test_create_player_short_name(): @@ -50,7 +50,7 @@ def test_create_player_short_name(): "/players", json={"name": "a"}, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." def test_create_player_large_name(): @@ -58,4 +58,4 @@ def test_create_player_large_name(): "/players", json={"name": "a"*31}, ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." diff --git a/app/tests/player_tests/test_delete_player.py b/app/tests/player_tests/test_delete_player.py index c9426c0..2f84f86 100644 --- a/app/tests/player_tests/test_delete_player.py +++ b/app/tests/player_tests/test_delete_player.py @@ -9,25 +9,25 @@ def test_delete_player(): response = client.delete( '/players/2', ) - assert response.status_code == 204 + assert response.status_code == 204, "El código de estado de la respuesta no es 204 (No Content)." def test_delete_player_idempotent(): response = client.delete( '/players/2', ) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." def test_delete_inexistent_player(): response = client.delete( '/players/999999', ) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." def test_delete_player_bad_id(): response = client.delete( '/players/badid', ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." diff --git a/app/tests/player_tests/test_read_player.py b/app/tests/player_tests/test_read_player.py index 79fa098..0af9907 100644 --- a/app/tests/player_tests/test_read_player.py +++ b/app/tests/player_tests/test_read_player.py @@ -9,35 +9,35 @@ def test_read_player(): response = client.get( '/players/1', ) - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." assert response.json() == { 'name': 'pepito', 'id': 1, 'position': -1 - } + }, "El contenido de la respuesta no coincide con los datos del jugador." def test_read_players(): response = client.get( '/players', ) - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." assert response.json() == [{ 'name': 'pepito', 'id': 1, 'position': -1 - }] + }], "El contenido de la respuesta no coincide con los datos de los jugadores." def test_read_inexistent_player(): response = client.get( '/players/9999999', ) - assert response.status_code == 404 + assert response.status_code == 404, "El contenido de la respuesta no coincide con los datos de los jugadores." def test_read_player_bad_id(): response = client.get( '/players/a', ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." diff --git a/app/tests/player_tests/test_update_player.py b/app/tests/player_tests/test_update_player.py index fd6f9a3..4c2c0f3 100644 --- a/app/tests/player_tests/test_update_player.py +++ b/app/tests/player_tests/test_update_player.py @@ -10,12 +10,12 @@ def test_update_player(): '/players/1', json={'name': 'pepe'} ) - assert response.status_code == 200 + assert response.status_code == 200, "El código de estado de la respuesta no es 200 (OK)." assert response.json() == { 'name': 'pepe', 'id': 1, 'position': -1 - } + }, "El contenido de la respuesta no coincide con los datos del jugador actualizado." def test_update_inexistent_player(): @@ -23,7 +23,7 @@ def test_update_inexistent_player(): '/players/99999999', json={'name': 'pepe'} ) - assert response.status_code == 404 + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." def test_update_player_bad_body(): @@ -31,4 +31,4 @@ def test_update_player_bad_body(): '/players/1', json={'name': 7} ) - assert response.status_code == 422 + assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." From 35407ded39940c2b994923a5f9adecef20cafbf2 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 7 Oct 2023 16:39:15 -0300 Subject: [PATCH 004/224] Makefile modificado para que sea menos verboso a la hora de hacer los tests, se corrigio typo en archivo csv de cartas, ahora el listado de cartas pasa el ID, se elimino import innecesario en test_creation_of_games.py --- Makefile | 4 ++-- app/resources/cartas.csv | 4 ++-- app/routers/cards/schemas.py | 5 +++-- app/routers/cards/services.py | 1 + app/tests/game_tests/test_creation_of_games.py | 1 - 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 96f85f9..6d7c7d6 100644 --- a/Makefile +++ b/Makefile @@ -71,13 +71,13 @@ test-games: install # Define the 'test-players' target to run player tests in the test environment test-players: install - ENVIRONMENT=test poetry run coverage run --data-file=$(COV_PLAYERS_FILE) -m pytest -vv $(TEST_DIRECTORY)/player_tests; true + ENVIRONMENT=test poetry run coverage run --data-file=$(COV_PLAYERS_FILE) -m pytest --tb=no -vv $(TEST_DIRECTORY)/player_tests; true rm -f $(TEST_DB_FILE) unset ENVIRONMENT # Define the 'test-cards' target to run cards tests in the test environment test-cards: install - ENVIRONMENT=test poetry run coverage run --data-file=$(COV_CARDS_FILE) -m pytest -vv $(TEST_DIRECTORY)/card_tests; true + ENVIRONMENT=test poetry run coverage run --data-file=$(COV_CARDS_FILE) -m pytest --tb=no -vv $(TEST_DIRECTORY)/card_tests; true rm -f $(TEST_DB_FILE) unset ENVIRONMENT diff --git a/app/resources/cartas.csv b/app/resources/cartas.csv index cba07b8..c8760b0 100644 --- a/app/resources/cartas.csv +++ b/app/resources/cartas.csv @@ -28,8 +28,8 @@ 6;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 9;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 11;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -4;GET_AWAY;Hacha;retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. -9;GET_AWAY;Hacha;retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. +4;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. +9;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. 4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. 4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. 4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index aa6c1d6..05fcf39 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -30,7 +30,6 @@ class CardCreationOut(BaseCard): class CardUpdateIn(BaseModel): - model_config = ConfigDict(from_attributes=True) number: Optional[int] = Field( None, ge=4, le=12, description="Optional number of the card." ) @@ -50,4 +49,6 @@ class CardUpdateOut(BaseCard): class CardResponse(BaseCard): - pass + id: int = Field( + ge=1, le=110 + ) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 2c79636..0542b20 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -10,6 +10,7 @@ def get_cards() -> list[CardResponse]: cards = Card.select() cards_list = [CardResponse( + id=card.id, number=card.number, type=card.type, name=card.name, diff --git a/app/tests/game_tests/test_creation_of_games.py b/app/tests/game_tests/test_creation_of_games.py index 2b51432..ffc70f1 100644 --- a/app/tests/game_tests/test_creation_of_games.py +++ b/app/tests/game_tests/test_creation_of_games.py @@ -3,7 +3,6 @@ from app.database.models import Game, Player from app.routers.games.schemas import GameCreationOut from pony.orm import db_session -import pytest client = TestClient(app) From a6240504b98ad773f64982e229026784e73cfb04 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 7 Oct 2023 17:30:03 -0300 Subject: [PATCH 005/224] Se vuelve a poner linea de model_config en una clase de los schemas de cards --- app/routers/cards/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 05fcf39..90e3018 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -30,6 +30,7 @@ class CardCreationOut(BaseCard): class CardUpdateIn(BaseModel): + model_config = ConfigDict(from_attributes=True) number: Optional[int] = Field( None, ge=4, le=12, description="Optional number of the card." ) From 7f0faff25a5df2cfdb85aaae10fb68ea7d86fed6 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sun, 8 Oct 2023 18:14:14 -0300 Subject: [PATCH 006/224] se agrega logica para iniciar la partida --- app/config/config.py | 1 + app/database/initialize_data.py | 6 +++--- app/main.py | 1 - app/routers/cards/services.py | 35 +++++++++++++++++++++++++++--- app/routers/games/games.py | 7 ++++++ app/routers/games/schemas.py | 7 ++++++ app/routers/games/services.py | 38 ++++++++++++++++++++++----------- app/routers/games/utils.py | 28 ++++++++++++++++++++++++ 8 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 app/routers/games/utils.py diff --git a/app/config/config.py b/app/config/config.py index d3efc51..07097db 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -5,6 +5,7 @@ class Settings(BaseSettings): environment: Literal["test", "production", "development"] = "development" CORS_origins: List[str] = ["*"] + CARDS_CSV_FILE_PTAH: str = "app/resources/cartas.csv" model_config = SettingsConfigDict(env_file=".env", extra='allow') diff --git a/app/database/initialize_data.py b/app/database/initialize_data.py index b4eb666..7fc6c17 100644 --- a/app/database/initialize_data.py +++ b/app/database/initialize_data.py @@ -1,15 +1,15 @@ from pony.orm import db_session from .models import Card import csv +from app.config.config import settings @db_session def populate_card_table(): if Card.select().count() > 0: - print("Los datos de cartas ya estan en la base de datos. No se realizará la inicialización") return - cartas = 'app/resources/cartas.csv' - with open(cartas, newline='') as csvfile: + + with open(settings.CARDS_CSV_FILE_PTAH, newline='') as csvfile: csvreader = csv.reader(csvfile, delimiter=';') for row in csvreader: number, card_type, name, description = row diff --git a/app/main.py b/app/main.py index 78b14cb..a4ff6b5 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,6 @@ from app.routers.players import players from app.routers.games import games from app.routers.cards import cards -from pony.orm import * app = FastAPI() diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 2c79636..c3e5d02 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -1,7 +1,6 @@ from pony.orm import * -from app.database.models import Game, Player, Card -from app.routers.games.services import find_game_by_name -from app.routers.players.services import find_player_by_id +import random +from app.database.models import Card, Game from .schemas import * from fastapi import HTTPException, status @@ -68,3 +67,33 @@ def delete_card(card_id: int): card = find_card_by_id(card_id) card.delete() return {"message": "Card deleted"} + + +@db_session +def build_deck(players: int) -> List[Card]: + deck = list(Card.select(lambda c: c.number <= players and + c.name != 'La Cosa')) + the_thing = Card.get(name='La Cosa') + + if the_thing is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='The card "The Thing" not found.' + ) + + random.shuffle(deck) + + # Insert the The Thing card making sure that + # it will go to a player's hand on the first deal. + random_index = random.randint(0, players - 1) + deck.insert(random_index, the_thing) + + return deck + + +@db_session +def deal_cards_to_players(game: Game, deck: List[Card]): + for _ in range(4): + for player in game.players: + card = deck.pop(0) + player.hand.add(card) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4bc7290..f8235b4 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, status, HTTPException from typing import List from . import services +from . import utils from .schemas import * from ..players.schemas import PlayerResponse @@ -26,6 +27,12 @@ def create_game(game_data: GameCreationIn): return services.create_game(game_data) +@router.patch("/{name}/init") +def start(name: str, host_player_id: int) -> GameStartOut: + utils.verify_game_can_start(name, host_player_id) + return services.start_game(name) + + @router.patch("/{game_name}", response_model=GameUpdateOut, status_code=status.HTTP_200_OK) def update_game(game_name: str, game_data: GameUpdateIn): return services.update_game(game_name, game_data) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 7b2ced2..001c823 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -2,6 +2,7 @@ from typing import List, Optional from enum import Enum from ..players.schemas import PlayerResponse +from ..cards.schemas import CardType class GameStatus(str, Enum): @@ -86,3 +87,9 @@ class GameInformationOut(GameUpdateOut): host_player_id: int num_of_players: int list_of_players: List[PlayerResponse] + + +class GameStartOut(BaseModel): + list_of_players: List[PlayerResponse] + status: GameStatus + top_card_face: CardType diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8f2cee5..d993645 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,6 +1,7 @@ from pony.orm import * from app.database.models import Game, Player, Card from .schemas import * +from ..cards import services as cards_services from fastapi import HTTPException, status @@ -22,8 +23,6 @@ def get_games() -> list[GameResponse]: @db_session def create_game(game_data: GameCreationIn) -> GameCreationOut: host = Player.get(id=game_data.host_player_id) - deck_cards = [card for card in Card.select( - lambda c: c.number == game_data.min_players)] if not host: raise HTTPException( @@ -43,9 +42,6 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: host=host ) - for card in deck_cards: - new_game.draw_deck.add(card) - host.game = new_game.name return GameCreationOut( name=new_game.name, @@ -122,13 +118,6 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation players_joined = game.players.select()[:] num_players_joined = len(players_joined) - # Here we add the cards for the player that joins the game - if (num_players_joined > game.min_players): - cards_to_add = [card for card in Card.select( - lambda c: c.number == num_players_joined)] - for card in cards_to_add: - game.draw_deck.add(card) - return GameInformationOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -164,7 +153,7 @@ def get_game_information(game_name: str) -> GameInformationOut: @db_session -def find_game_by_name(game_name: str): +def find_game_by_name(game_name: str) -> Game: game = Game.get(name=game_name) if not game: @@ -173,3 +162,26 @@ def find_game_by_name(game_name: str): ) return game + + +@db_session +def start_game(name: str) -> Game: + game: Game = find_game_by_name(name) + players_joined = count(game.players) + + draw_deck = cards_services.build_deck(players_joined) + cards_services.deal_cards_to_players(game, draw_deck) + game.draw_deck.add(draw_deck) + + # setting the position of the players + for idx, player in enumerate(game.players): + player.position = idx + + game.status = GameStatus.STARTED + game.turn = 0 + + return GameStartOut( + list_of_players=game.players, + status=game.status, + top_card_face=list(game.draw_deck)[0].type + ) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py new file mode 100644 index 0000000..a414bc0 --- /dev/null +++ b/app/routers/games/utils.py @@ -0,0 +1,28 @@ +from pony.orm import * +from .schemas import * +from fastapi import HTTPException, status +from .services import find_game_by_name + + +@db_session +def verify_game_can_start(name: str, host_player_id: int): + game = find_game_by_name(name) + players_joined = count(game.players) + + if game.status == GameStatus.STARTED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The game has already started." + ) + + if not 4 <= players_joined <= 12: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The number of players joined ({players_joined}) is not allowed." + ) + + if game.host.id != host_player_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Only the host player can start the game." + ) From 473389172fcce624d2d55baf7a018a067c95b253 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 9 Oct 2023 11:57:41 -0300 Subject: [PATCH 007/224] se agrega model_config en clase GameStartOut --- app/routers/games/schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 001c823..68826ef 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -90,6 +90,8 @@ class GameInformationOut(GameUpdateOut): class GameStartOut(BaseModel): + model_config = ConfigDict(from_attributes=True) + list_of_players: List[PlayerResponse] status: GameStatus top_card_face: CardType From 0a6f36bcef817f87fc88ab5fef55c986a6fa6d08 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 9 Oct 2023 12:11:04 -0300 Subject: [PATCH 008/224] Se agrega intento de implementacion de websocket para actualizar el listado de partidas. --- app/routers/cards/services.py | 2 +- app/routers/games/games.py | 6 ++- app/routers/games/services.py | 87 ++++++++++++++++++++--------------- app/routers/games/utils.py | 49 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 app/routers/games/utils.py diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 0542b20..e5031bb 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -1,6 +1,6 @@ from pony.orm import * from app.database.models import Game, Player, Card -from app.routers.games.services import find_game_by_name +from app.routers.games.utils import find_game_by_name from app.routers.players.services import find_player_by_id from .schemas import * from fastapi import HTTPException, status diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4bc7290..c29d6a7 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, status, HTTPException +from fastapi import APIRouter, status, HTTPException, WebSocket from typing import List from . import services from .schemas import * @@ -39,3 +39,7 @@ def delete_game(game_name: str): @router.patch("/join/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) def join_player(game_name: str, game_data: GameInformationIn): return services.join_player(game_name, game_data) + +@router.websocket("/{game_name}/ws") +def websocket_endpoint(websocket:WebSocket, game_name: str): + return services.list_games(websocket, game_name) \ No newline at end of file diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8f2cee5..ac298dd 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,7 +1,8 @@ from pony.orm import * from app.database.models import Game, Player, Card from .schemas import * -from fastapi import HTTPException, status +from fastapi import HTTPException, status, WebSocket +from .utils import gameManager @db_session @@ -19,6 +20,27 @@ def get_games() -> list[GameResponse]: return games_list +@db_session +def get_game_information(game_name: str) -> GameInformationOut: + game = Game.get(name=game_name) + + if game is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail='Game not found') + players_joined = game.players.select()[:] + return GameInformationOut(name=game.name, + min_players=game.min_players, + max_players=game.max_players, + is_private=game.password is not None, + status=game.status, + host_player_name=game.host.name, + host_player_id=game.host.id, + num_of_players=len(game.players), + list_of_players=[PlayerResponse.model_validate( + p) for p in players_joined] + ) + + @db_session def create_game(game_data: GameCreationIn) -> GameCreationOut: host = Player.get(id=game_data.host_player_id) @@ -47,6 +69,7 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: new_game.draw_deck.add(card) host.game = new_game.name + gameManager.new_game(new_game.name) return GameCreationOut( name=new_game.name, status=new_game.status, @@ -94,10 +117,12 @@ def delete_game(game_name: str): return {"message": "Game deleted"} -@db_session -def join_player(game_name: str, game_data: GameInformationIn) -> GameInformationOut: - game = Game.get(name=game_name) - player = Player.get(id=game_data.player_id) + +async def join_player(game_name: str, game_data: GameInformationIn) -> GameInformationOut: + with db_session: + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) + commit() if not game: raise HTTPException( @@ -128,7 +153,15 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation lambda c: c.number == num_players_joined)] for card in cards_to_add: game.draw_deck.add(card) - + + gameInformation = { + "type": "join", + "min_players": "4", + "max_players": "4" + } + connectionGame = gameManager.return_game(game.name) + await connectionGame.broadcast(gameInformation) + return GameInformationOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -142,34 +175,14 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation ) -@db_session -def get_game_information(game_name: str) -> GameInformationOut: - game = Game.get(name=game_name) - - if game is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail='Game not found') - players_joined = game.players.select()[:] - return GameInformationOut(name=game.name, - min_players=game.min_players, - max_players=game.max_players, - is_private=game.password is not None, - status=game.status, - host_player_name=game.host.name, - host_player_id=game.host.id, - num_of_players=len(game.players), - list_of_players=[PlayerResponse.model_validate( - p) for p in players_joined] - ) - - -@db_session -def find_game_by_name(game_name: str): - game = Game.get(name=game_name) - - if not game: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Game not found" - ) - - return game +async def websocket_endpoint(websocket: WebSocket, game_name: str): + try: + manager = gameManager.return_match(game_name) + await manager.connect(websocket) + except RuntimeError: + raise "Error estableshing connection" + while True: + try: + await websocket.receive() + except RuntimeError: + break diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py new file mode 100644 index 0000000..0ccca5e --- /dev/null +++ b/app/routers/games/utils.py @@ -0,0 +1,49 @@ +from typing import List +from fastapi import WebSocket, HTTPException, status +from pony.orm import db_session +from app.database.models import Game + + +@db_session +def find_game_by_name(game_name: str): + game = Game.get(name=game_name) + + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Game not found" + ) + + return game + + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def diconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, message): + for connection in self.active_connections: + await connection.send_json(message) + + +class GameManager: + def __init__(self): + self.games = {} + + def new_game(self, game_name: str): + self.games[game_name] = ConnectionManager() + + def end_game(self, game_name: str): + self.games.pop(game_name) + + def return_game(self, game_name: str): + return self.games[game_name] + + +gameManager = GameManager() From ba1285104194953e8f54f83d8dbf9ec4043fbc5d Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 10 Oct 2023 09:15:04 -0300 Subject: [PATCH 009/224] Se elimina todo lo probado con websockets para empezar de vuelta --- app/routers/games/games.py | 6 +----- app/routers/games/services.py | 37 +++++++---------------------------- app/routers/games/utils.py | 36 +--------------------------------- 3 files changed, 9 insertions(+), 70 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index ec7ac67..180e962 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -45,8 +45,4 @@ def delete_game(game_name: str): @router.patch("/join/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) def join_player(game_name: str, game_data: GameInformationIn): - return services.join_player(game_name, game_data) - -@router.websocket("/{game_name}/ws") -def websocket_endpoint(websocket:WebSocket, game_name: str): - return services.list_games(websocket, game_name) \ No newline at end of file + return services.join_player(game_name, game_data) \ No newline at end of file diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 53e166d..bbdf3d0 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,8 +1,8 @@ from pony.orm import * -from app.database.models import Game, Player, Card +from app.database.models import Game, Player from .schemas import * -from fastapi import HTTPException, status, WebSocket -from .utils import gameManager, find_game_by_name +from fastapi import HTTPException, status +from .utils import find_game_by_name from ..cards import services as cards_services @@ -65,7 +65,6 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: ) host.game = new_game.name - gameManager.new_game(new_game.name) return GameCreationOut( name=new_game.name, status=new_game.status, @@ -113,11 +112,10 @@ def delete_game(game_name: str): return {"message": "Game deleted"} -async def join_player(game_name: str, game_data: GameInformationIn) -> GameInformationOut: - with db_session: - game = Game.get(name=game_name) - player = Player.get(id=game_data.player_id) - commit() +@db_session +def join_player(game_name: str, game_data: GameInformationIn) -> GameInformationOut: + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) if not game: raise HTTPException( @@ -142,14 +140,6 @@ async def join_player(game_name: str, game_data: GameInformationIn) -> GameInfor players_joined = game.players.select()[:] num_players_joined = len(players_joined) - gameInformation = { - "type": "join", - "min_players": "4", - "max_players": "4" - } - connectionGame = gameManager.return_game(game.name) - await connectionGame.broadcast(gameInformation) - return GameInformationOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -163,19 +153,6 @@ async def join_player(game_name: str, game_data: GameInformationIn) -> GameInfor ) -async def websocket_endpoint(websocket: WebSocket, game_name: str): - try: - manager = gameManager.return_match(game_name) - await manager.connect(websocket) - except RuntimeError: - raise "Error estableshing connection" - while True: - try: - await websocket.receive() - except RuntimeError: - break - - @db_session def start_game(name: str) -> Game: game: Game = find_game_by_name(name) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 69700a9..6d3229d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -3,7 +3,6 @@ from app.database.models import Game from pony.orm import * from .schemas import * -from .services import find_game_by_name @db_session @@ -38,37 +37,4 @@ def verify_game_can_start(name: str, host_player_id: int): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Only the host player can start the game." - ) - - -class ConnectionManager: - def __init__(self): - self.active_connections: List[WebSocket] = [] - - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - - def diconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) - - async def broadcast(self, message): - for connection in self.active_connections: - await connection.send_json(message) - - -class GameManager: - def __init__(self): - self.games = {} - - def new_game(self, game_name: str): - self.games[game_name] = ConnectionManager() - - def end_game(self, game_name: str): - self.games.pop(game_name) - - def return_game(self, game_name: str): - return self.games[game_name] - - -gameManager = GameManager() \ No newline at end of file + ) \ No newline at end of file From e66463bb531b8b4dfc942515ea7f61f106aac958 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 10 Oct 2023 12:39:27 -0300 Subject: [PATCH 010/224] Se manda el listado de las partidas por websockets siempre que haya alguna modificacion (se cree una partida, se elimine una partida, se inicie una partida, se unan jugadores, etc) --- Makefile | 2 +- app/main.py | 2 ++ app/routers/games/services.py | 33 +++++++++++++++---------- app/routers/games/utils.py | 16 ++++++++++++- app/routers/websockets/services.py | 12 ++++++++++ app/routers/websockets/utils.py | 36 ++++++++++++++++++++++++++++ app/routers/websockets/websockets.py | 11 +++++++++ 7 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 app/routers/websockets/services.py create mode 100644 app/routers/websockets/utils.py create mode 100644 app/routers/websockets/websockets.py diff --git a/Makefile b/Makefile index 6d7c7d6..32f8304 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ COV_GAMES_FILE = .coverage.games COV_CARDS_FILE = .coverage.cards # Define the UVicorn command -UVICORN_CMD = uvicorn $(MAIN_FILE):$(APP_NAME) --port $(PORT) --reload +UVICORN_CMD = uvicorn $(MAIN_FILE):$(APP_NAME) --port $(PORT) --reload --ws websockets .PHONY: run delete-db coverage-report coverage-clean test-all test-players test-games help diff --git a/app/main.py b/app/main.py index a4ff6b5..c95fd9e 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ from app.routers.players import players from app.routers.games import games from app.routers.cards import cards +from app.routers.websockets import websockets app = FastAPI() @@ -20,6 +21,7 @@ app.include_router(players.router) app.include_router(games.router) app.include_router(cards.router) +app.include_router(websockets.router) @app.get("/") diff --git a/app/routers/games/services.py b/app/routers/games/services.py index bbdf3d0..91a1bad 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -2,22 +2,15 @@ from app.database.models import Game, Player from .schemas import * from fastapi import HTTPException, status -from .utils import find_game_by_name +from .utils import find_game_by_name, list_of_games from ..cards import services as cards_services +from ..websockets.utils import player_connections +import asyncio +import json -@db_session def get_games() -> list[GameResponse]: - games = Game.select() - games_list = [GameResponse( - name=game.name, - min_players=game.min_players, - max_players=game.max_players, - host_player_id=game.host.id, - status=game.status, - is_private=game.password is not None, - num_of_players=len(game.players) - ) for game in games] + games_list = list_of_games() return games_list @@ -65,6 +58,10 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: ) host.game = new_game.name + + games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) + asyncio.run(player_connections.broadcast(games_json)) + return GameCreationOut( name=new_game.name, status=new_game.status, @@ -90,6 +87,9 @@ def update_game(game_name: str, request_data: GameUpdateIn) -> GameUpdateOut: game.max_players = request_data.max_players game.password = request_data.password + games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) + asyncio.run(player_connections.broadcast(games_json)) + return GameUpdateOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -107,6 +107,9 @@ def delete_game(game_name: str): if game_name != game.name: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid game name") + + games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) + asyncio.run(player_connections.broadcast(games_json)) game.delete() return {"message": "Game deleted"} @@ -140,6 +143,9 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation players_joined = game.players.select()[:] num_players_joined = len(players_joined) + games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) + asyncio.run(player_connections.broadcast(games_json)) + return GameInformationOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -169,6 +175,9 @@ def start_game(name: str) -> Game: game.status = GameStatus.STARTED game.turn = 0 + games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) + asyncio.run(player_connections.broadcast(games_json)) + return GameStartOut( list_of_players=game.players, status=game.status, diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 6d3229d..b91c71d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -37,4 +37,18 @@ def verify_game_can_start(name: str, host_player_id: int): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Only the host player can start the game." - ) \ No newline at end of file + ) + +@db_session +def list_of_games(): + games = Game.select() + games_list = [GameResponse( + name=game.name, + min_players=game.min_players, + max_players=game.max_players, + host_player_id=game.host.id, + status=game.status, + is_private=game.password is not None, + num_of_players=len(game.players) + ) for game in games] + return games_list \ No newline at end of file diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py new file mode 100644 index 0000000..d9c7c7d --- /dev/null +++ b/app/routers/websockets/services.py @@ -0,0 +1,12 @@ +from fastapi import WebSocket, WebSocketDisconnect +from .utils import player_connections + +async def websocket_games(websocket: WebSocket): + await player_connections.connect(websocket) + + try: + while True: + data = await websocket.receive() + + except WebSocketDisconnect: + player_connections.disconnect(websocket) \ No newline at end of file diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py new file mode 100644 index 0000000..889a22a --- /dev/null +++ b/app/routers/websockets/utils.py @@ -0,0 +1,36 @@ +from fastapi import WebSocket +from typing import List + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, message): + for connection in self.active_connections: + try: + await connection.send_json(message) + except: + self.active_connections.remove(connection) + +class GameManager: + def __init__(self): + self.games_connections = {} + + def new_game(self, game_name: str): + self.games_connections[game_name] = ConnectionManager() + + def end_game(self, game_name: str): + del self.games_connections[game_name] + + def return_game_connection(self, game_name: str): + return self.games_connections[game_name] + +player_connections = ConnectionManager() +# gamesManager = GameManager() \ No newline at end of file diff --git a/app/routers/websockets/websockets.py b/app/routers/websockets/websockets.py new file mode 100644 index 0000000..7bbd7b4 --- /dev/null +++ b/app/routers/websockets/websockets.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter, WebSocket +from . import services + +router = APIRouter( + prefix="/ws", + tags=["ws"], +) + +@router.websocket("/games") +def websockets_games(websocket: WebSocket): + return services.websocket_games(websocket) \ No newline at end of file From 955d16c0e78523b51aa21c1af6c7fcfefad4e841 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 10 Oct 2023 14:49:27 -0300 Subject: [PATCH 011/224] Modificacion para que no se envie el listado completo de partidas sino un mensaje con el evento que ocurrio para que el front pueda pegar a endpoint de listar las partidas --- app/routers/games/games.py | 2 +- app/routers/games/services.py | 38 ++++++++++++++++++++-------- app/routers/games/utils.py | 6 +++-- app/routers/websockets/services.py | 5 ++-- app/routers/websockets/utils.py | 15 ++++++----- app/routers/websockets/websockets.py | 3 ++- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 180e962..d4c1810 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -45,4 +45,4 @@ def delete_game(game_name: str): @router.patch("/join/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) def join_player(game_name: str, game_data: GameInformationIn): - return services.join_player(game_name, game_data) \ No newline at end of file + return services.join_player(game_name, game_data) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 91a1bad..12bae55 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -59,8 +59,11 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: host.game = new_game.name - games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) - asyncio.run(player_connections.broadcast(games_json)) + json_msg = { + "event": "game_created", + "game_name": new_game.name + } + asyncio.run(player_connections.broadcast(json_msg)) return GameCreationOut( name=new_game.name, @@ -87,8 +90,11 @@ def update_game(game_name: str, request_data: GameUpdateIn) -> GameUpdateOut: game.max_players = request_data.max_players game.password = request_data.password - games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) - asyncio.run(player_connections.broadcast(games_json)) + json_msg = { + "event": "game_updated", + "game_name": game.name + } + asyncio.run(player_connections.broadcast(json_msg)) return GameUpdateOut(name=game.name, min_players=game.min_players, @@ -107,9 +113,12 @@ def delete_game(game_name: str): if game_name != game.name: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid game name") - - games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) - asyncio.run(player_connections.broadcast(games_json)) + + json_msg = { + "event": "game_deleted", + "game_name": game.name + } + asyncio.run(player_connections.broadcast(json_msg)) game.delete() return {"message": "Game deleted"} @@ -143,8 +152,12 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation players_joined = game.players.select()[:] num_players_joined = len(players_joined) - games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) - asyncio.run(player_connections.broadcast(games_json)) + json_msg = { + "event": "player_join", + "player_id": player.id, + "game_name": game.name + } + asyncio.run(player_connections.broadcast(json_msg)) return GameInformationOut(name=game.name, min_players=game.min_players, @@ -175,8 +188,11 @@ def start_game(name: str) -> Game: game.status = GameStatus.STARTED game.turn = 0 - games_json = json.dumps(list_of_games(), default=lambda x: x.__dict__) - asyncio.run(player_connections.broadcast(games_json)) + json_msg = { + "event": "game_started", + "game_name": game.name + } + asyncio.run(player_connections.broadcast(json_msg)) return GameStartOut( list_of_players=game.players, diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b91c71d..563d5d2 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -16,6 +16,7 @@ def find_game_by_name(game_name: str): return game + @db_session def verify_game_can_start(name: str, host_player_id: int): game = find_game_by_name(name) @@ -38,7 +39,8 @@ def verify_game_can_start(name: str, host_player_id: int): status_code=status.HTTP_400_BAD_REQUEST, detail=f"Only the host player can start the game." ) - + + @db_session def list_of_games(): games = Game.select() @@ -51,4 +53,4 @@ def list_of_games(): is_private=game.password is not None, num_of_players=len(game.players) ) for game in games] - return games_list \ No newline at end of file + return games_list diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index d9c7c7d..7c41a16 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,12 +1,13 @@ from fastapi import WebSocket, WebSocketDisconnect from .utils import player_connections + async def websocket_games(websocket: WebSocket): await player_connections.connect(websocket) try: while True: data = await websocket.receive() - + except WebSocketDisconnect: - player_connections.disconnect(websocket) \ No newline at end of file + player_connections.disconnect(websocket) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 889a22a..56a24cd 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -1,6 +1,7 @@ from fastapi import WebSocket from typing import List + class ConnectionManager: def __init__(self): self.active_connections: List[WebSocket] = [] @@ -8,10 +9,10 @@ def __init__(self): async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) - + def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) - + async def broadcast(self, message): for connection in self.active_connections: try: @@ -19,18 +20,20 @@ async def broadcast(self, message): except: self.active_connections.remove(connection) + class GameManager: def __init__(self): self.games_connections = {} - + def new_game(self, game_name: str): self.games_connections[game_name] = ConnectionManager() - + def end_game(self, game_name: str): del self.games_connections[game_name] - + def return_game_connection(self, game_name: str): return self.games_connections[game_name] + player_connections = ConnectionManager() -# gamesManager = GameManager() \ No newline at end of file +# gamesManager = GameManager() diff --git a/app/routers/websockets/websockets.py b/app/routers/websockets/websockets.py index 7bbd7b4..5fca866 100644 --- a/app/routers/websockets/websockets.py +++ b/app/routers/websockets/websockets.py @@ -6,6 +6,7 @@ tags=["ws"], ) + @router.websocket("/games") def websockets_games(websocket: WebSocket): - return services.websocket_games(websocket) \ No newline at end of file + return services.websocket_games(websocket) From f03f304f73f65afc9f4d706a8d4e1092900ca2cb Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 10 Oct 2023 15:44:18 -0300 Subject: [PATCH 012/224] Se cambia forma de almacenar las conexiones de websocket para que queden asociadas con el id del jugador --- app/routers/websockets/services.py | 13 +++++++--- app/routers/websockets/utils.py | 37 +++++++++------------------- app/routers/websockets/websockets.py | 6 ++--- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 7c41a16..6605de3 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -2,12 +2,17 @@ from .utils import player_connections -async def websocket_games(websocket: WebSocket): - await player_connections.connect(websocket) +async def websocket_games(player_id: int, websocket: WebSocket): + await player_connections.connect(player_id, websocket) try: while True: data = await websocket.receive() - + ''' + Aca si recibimo data.event == message entonces mandamos el mensaje + al resto de los jugadores por ejemplo. + O si es un data.event == disconnect entonces debemos sacar el websocket + de la lista de player_connections. + ''' except WebSocketDisconnect: - player_connections.disconnect(websocket) + player_connections.disconnect(player_id) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 56a24cd..c7b24b3 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -1,39 +1,24 @@ from fastapi import WebSocket -from typing import List +from typing import Dict class ConnectionManager: def __init__(self): - self.active_connections: List[WebSocket] = [] + self.active_connections: Dict[int, WebSocket] = {} - async def connect(self, websocket: WebSocket): + async def connect(self, player_id: int, websocket: WebSocket): await websocket.accept() - self.active_connections.append(websocket) + self.active_connections[player_id] = websocket - def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) + def disconnect(self, player_id: int): + del self.active_connections[player_id] - async def broadcast(self, message): - for connection in self.active_connections: - try: - await connection.send_json(message) - except: - self.active_connections.remove(connection) - - -class GameManager: - def __init__(self): - self.games_connections = {} + async def send_message(self, player_id: int, message): + await self.active_connections[player_id].send_json(message) - def new_game(self, game_name: str): - self.games_connections[game_name] = ConnectionManager() - - def end_game(self, game_name: str): - del self.games_connections[game_name] - - def return_game_connection(self, game_name: str): - return self.games_connections[game_name] + async def broadcast(self, message): + for player_id, websocket in self.active_connections.items(): + await websocket.send_json(message) player_connections = ConnectionManager() -# gamesManager = GameManager() diff --git a/app/routers/websockets/websockets.py b/app/routers/websockets/websockets.py index 5fca866..75dad8a 100644 --- a/app/routers/websockets/websockets.py +++ b/app/routers/websockets/websockets.py @@ -7,6 +7,6 @@ ) -@router.websocket("/games") -def websockets_games(websocket: WebSocket): - return services.websocket_games(websocket) +@router.websocket("/{player_id}") +def websockets_games(player_id: int, websocket: WebSocket): + return services.websocket_games(player_id, websocket) From 34bae3bdb48e85afa8c3006b466e466ff7df70ba Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 10 Oct 2023 19:55:52 -0300 Subject: [PATCH 013/224] Si recibe un mensaje se manda por websockets a todos los unidos en esa partida --- app/routers/games/services.py | 1 - app/routers/websockets/services.py | 19 +++++++++++-------- app/routers/websockets/utils.py | 27 ++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 12bae55..6774e1b 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -6,7 +6,6 @@ from ..cards import services as cards_services from ..websockets.utils import player_connections import asyncio -import json def get_games() -> list[GameResponse]: diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 6605de3..ed0215b 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,5 +1,7 @@ +from pony.orm import db_session +from app.database.models import Game, Player from fastapi import WebSocket, WebSocketDisconnect -from .utils import player_connections +from .utils import player_connections, get_players_id async def websocket_games(player_id: int, websocket: WebSocket): @@ -7,12 +9,13 @@ async def websocket_games(player_id: int, websocket: WebSocket): try: while True: - data = await websocket.receive() - ''' - Aca si recibimo data.event == message entonces mandamos el mensaje - al resto de los jugadores por ejemplo. - O si es un data.event == disconnect entonces debemos sacar el websocket - de la lista de player_connections. - ''' + data = await websocket.receive_json() + if (data["event"] == "message"): + message_from = data["from"] + message = data["message"] + players = get_players_id(data["game_name"]) + for i in players: + if i != player_id: + await player_connections.send_message(player_id=i, message_from=message_from, message=message) except WebSocketDisconnect: player_connections.disconnect(player_id) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index c7b24b3..679a4b6 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -1,5 +1,19 @@ from fastapi import WebSocket -from typing import Dict +from pony.orm import db_session +from app.database.models import Player +from ..games import services as games_services +from typing import Dict, List +import json + + +@db_session +def get_players_id(game_name: str) -> List[Player]: + gameInformation = games_services.get_game_information(game_name) + result = [] + if gameInformation: + for p in gameInformation.list_of_players: + result.append(p.id) + return result class ConnectionManager: @@ -13,8 +27,15 @@ async def connect(self, player_id: int, websocket: WebSocket): def disconnect(self, player_id: int): del self.active_connections[player_id] - async def send_message(self, player_id: int, message): - await self.active_connections[player_id].send_json(message) + async def send_message(self, player_id: int, message_from: str, message: str): + try: + json_msg = { + "from": message_from, + "message": message + } + await self.active_connections[player_id].send_json(json_msg) + except KeyError: + pass async def broadcast(self, message): for player_id, websocket in self.active_connections.items(): From 1dc5d2367c5f55e5d9762582064c78fe78d1884f Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 09:45:41 -0300 Subject: [PATCH 014/224] Cambio menor en el formato del mensaje de broadcast del mensaje del chat al jugador --- app/routers/websockets/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 679a4b6..785a515 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -30,6 +30,7 @@ def disconnect(self, player_id: int): async def send_message(self, player_id: int, message_from: str, message: str): try: json_msg = { + "event": "message", "from": message_from, "message": message } From 1f5120153b6d1386e9b4669893f22926e5942e06 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 15:16:10 -0300 Subject: [PATCH 015/224] Se agrega dependencia WebSocket --- poetry.lock | 315 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 315 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 702e15f..1108d06 100644 --- a/poetry.lock +++ b/poetry.lock @@ -75,6 +75,70 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.1.7" @@ -201,6 +265,143 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "gevent" +version = "23.9.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, + {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, + {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, + {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, + {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, + {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, + {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, + {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, + {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, + {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, + {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, + {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, + {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, + {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, + {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, +] + +[package.dependencies] +cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0)"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] + +[[package]] +name = "greenlet" +version = "3.0.0" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a"}, + {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779"}, + {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9"}, + {file = "greenlet-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7"}, + {file = "greenlet-3.0.0-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f"}, + {file = "greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b"}, + {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"}, + {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"}, + {file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"}, + {file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"}, + {file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb"}, + {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"}, + {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"}, + {file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423"}, + {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed"}, + {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625"}, + {file = "greenlet-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947"}, + {file = "greenlet-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705"}, + {file = "greenlet-3.0.0-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353"}, + {file = "greenlet-3.0.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f"}, + {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d"}, + {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f"}, + {file = "greenlet-3.0.0-cp38-cp38-win32.whl", hash = "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a"}, + {file = "greenlet-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b"}, + {file = "greenlet-3.0.0-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464"}, + {file = "greenlet-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a"}, + {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692"}, + {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4"}, + {file = "greenlet-3.0.0-cp39-cp39-win32.whl", hash = "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9"}, + {file = "greenlet-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce"}, + {file = "greenlet-3.0.0-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355"}, + {file = "greenlet-3.0.0.tar.gz", hash = "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.14.0" @@ -326,6 +527,17 @@ files = [ {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, ] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.3.0" @@ -549,6 +761,22 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "sniffio" version = "1.3.0" @@ -618,7 +846,92 @@ h11 = ">=0.8" [package.extras] standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (==0.2.*)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] +[[package]] +name = "websocket" +version = "0.2.1" +description = "Websocket implementation for gevent" +optional = false +python-versions = "*" +files = [ + {file = "websocket-0.2.1.tar.gz", hash = "sha256:42b506fae914ac5ed654e23ba9742e6a342b1a1c3eb92632b6166c65256469a4"}, +] + +[package.dependencies] +gevent = "*" +greenlet = "*" + +[[package]] +name = "zope-event" +version = "5.0" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, + {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner"] + +[[package]] +name = "zope-interface" +version = "6.1" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, + {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, + {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, + {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, + {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, + {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, + {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, + {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, + {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, + {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] +test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] + [metadata] lock-version = "2.0" python-versions = "^3.10,<3.11" -content-hash = "59c328ec417d1d9f7885b735561edd0b3a278e03e5ffa3aa7eb243d76840fb50" +content-hash = "0241e1a35fa1f528fed9f03c10319c8744cc8875628d4a28c3a7665f94bd9a55" diff --git a/pyproject.toml b/pyproject.toml index af4bb67..3480243 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ pytest-mock = "^3.11.1" autopep8 = "^2.0.4" httpx = "^0.25.0" pytest-cov = "^4.1.0" +websocket = "^0.2.1" [build-system] requires = ["poetry-core"] From cca19c23c8d4e07b9c6cef8f15ac762845d08910 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 15:20:19 -0300 Subject: [PATCH 016/224] Agregada dependencia de websockets --- poetry.lock | 81 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 1108d06..edb3c90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -860,6 +860,85 @@ files = [ gevent = "*" greenlet = "*" +[[package]] +name = "websockets" +version = "11.0.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, + {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, + {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, + {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, + {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, + {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, + {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, + {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, + {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, + {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, + {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, + {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, + {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, + {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, +] + [[package]] name = "zope-event" version = "5.0" @@ -934,4 +1013,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.11" -content-hash = "0241e1a35fa1f528fed9f03c10319c8744cc8875628d4a28c3a7665f94bd9a55" +content-hash = "beda21a9488ddf7007cf1e56511356ac508da4475780a695e5ba2a816448f63b" diff --git a/pyproject.toml b/pyproject.toml index 3480243..0b7d839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ autopep8 = "^2.0.4" httpx = "^0.25.0" pytest-cov = "^4.1.0" websocket = "^0.2.1" +websockets = "^11.0.3" [build-system] requires = ["poetry-core"] From 9419c2dad4a860d747c6810ce034aa8c6f6a6ee4 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 16:53:51 -0300 Subject: [PATCH 017/224] Hotfix: Se quitan dependencias innecesarias de poetry. --- poetry.lock | 315 +------------------------------------------------ pyproject.toml | 1 - 2 files changed, 1 insertion(+), 315 deletions(-) diff --git a/poetry.lock b/poetry.lock index edb3c90..f6145e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -75,70 +75,6 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "click" version = "8.1.7" @@ -265,143 +201,6 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -[[package]] -name = "gevent" -version = "23.9.1" -description = "Coroutine-based network library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, - {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, - {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, - {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, - {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, - {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, - {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, - {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, - {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, - {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, - {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, - {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, - {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, - {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, - {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, -] - -[package.dependencies] -cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] -docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0)"] -recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] -test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] - -[[package]] -name = "greenlet" -version = "3.0.0" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6"}, - {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a"}, - {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c"}, - {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695"}, - {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f"}, - {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a"}, - {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779"}, - {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9"}, - {file = "greenlet-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7"}, - {file = "greenlet-3.0.0-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f"}, - {file = "greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7"}, - {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e"}, - {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c"}, - {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd"}, - {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14"}, - {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b"}, - {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"}, - {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"}, - {file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"}, - {file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"}, - {file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"}, - {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"}, - {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"}, - {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99"}, - {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66"}, - {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb"}, - {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"}, - {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"}, - {file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"}, - {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"}, - {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"}, - {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"}, - {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1"}, - {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423"}, - {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed"}, - {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625"}, - {file = "greenlet-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947"}, - {file = "greenlet-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705"}, - {file = "greenlet-3.0.0-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353"}, - {file = "greenlet-3.0.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9"}, - {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c"}, - {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0"}, - {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b"}, - {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365"}, - {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f"}, - {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d"}, - {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f"}, - {file = "greenlet-3.0.0-cp38-cp38-win32.whl", hash = "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a"}, - {file = "greenlet-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b"}, - {file = "greenlet-3.0.0-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464"}, - {file = "greenlet-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4"}, - {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06"}, - {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314"}, - {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870"}, - {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99"}, - {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a"}, - {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692"}, - {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4"}, - {file = "greenlet-3.0.0-cp39-cp39-win32.whl", hash = "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9"}, - {file = "greenlet-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce"}, - {file = "greenlet-3.0.0-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355"}, - {file = "greenlet-3.0.0.tar.gz", hash = "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b"}, -] - -[package.extras] -docs = ["Sphinx"] -test = ["objgraph", "psutil"] - [[package]] name = "h11" version = "0.14.0" @@ -527,17 +326,6 @@ files = [ {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, ] -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - [[package]] name = "pydantic" version = "2.3.0" @@ -761,22 +549,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "setuptools" -version = "68.2.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "sniffio" version = "1.3.0" @@ -846,20 +618,6 @@ h11 = ">=0.8" [package.extras] standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (==0.2.*)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] -[[package]] -name = "websocket" -version = "0.2.1" -description = "Websocket implementation for gevent" -optional = false -python-versions = "*" -files = [ - {file = "websocket-0.2.1.tar.gz", hash = "sha256:42b506fae914ac5ed654e23ba9742e6a342b1a1c3eb92632b6166c65256469a4"}, -] - -[package.dependencies] -gevent = "*" -greenlet = "*" - [[package]] name = "websockets" version = "11.0.3" @@ -939,78 +697,7 @@ files = [ {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, ] -[[package]] -name = "zope-event" -version = "5.0" -description = "Very basic event publishing system" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, - {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx"] -test = ["zope.testrunner"] - -[[package]] -name = "zope-interface" -version = "6.1" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, - {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, - {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, - {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, - {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, - {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, - {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, - {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, - {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, - {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] - [metadata] lock-version = "2.0" python-versions = "^3.10,<3.11" -content-hash = "beda21a9488ddf7007cf1e56511356ac508da4475780a695e5ba2a816448f63b" +content-hash = "d88f5df5874122ce388d3da8cfd9993025124b291d6b02d4d1f0cd3deebf66f9" diff --git a/pyproject.toml b/pyproject.toml index 0b7d839..945f237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ pytest-mock = "^3.11.1" autopep8 = "^2.0.4" httpx = "^0.25.0" pytest-cov = "^4.1.0" -websocket = "^0.2.1" websockets = "^11.0.3" [build-system] From ae8c5f44c2795bee7f2e7f298a9848bba03c340d Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 19:06:15 -0300 Subject: [PATCH 018/224] Se implementa que cuando se pega al endpoint GET /games se pasa solo el listado de partidas que no empezaron. --- app/routers/games/games.py | 4 ++-- app/routers/games/services.py | 6 +++--- app/routers/games/utils.py | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index d4c1810..5f0e2a6 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -13,8 +13,8 @@ @router.get("/", response_model=list[GameResponse], status_code=status.HTTP_200_OK) -def get_games(): - return services.get_games() +def get_unstarted_games(): + return services.get_unstarted_games() @router.get("/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 6774e1b..a967999 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -2,14 +2,14 @@ from app.database.models import Game, Player from .schemas import * from fastapi import HTTPException, status -from .utils import find_game_by_name, list_of_games +from .utils import find_game_by_name, list_of_unstarted_games from ..cards import services as cards_services from ..websockets.utils import player_connections import asyncio -def get_games() -> list[GameResponse]: - games_list = list_of_games() +def get_unstarted_games() -> List[GameResponse]: + games_list = list_of_unstarted_games() return games_list diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 563d5d2..dd034c1 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -42,7 +42,23 @@ def verify_game_can_start(name: str, host_player_id: int): @db_session -def list_of_games(): +def list_of_unstarted_games() -> List[GameResponse]: + games = Game.select(lambda game: game.status != + GameStatus.STARTED and game.status != GameStatus.ENDED) + games_list = [GameResponse( + name=game.name, + min_players=game.min_players, + max_players=game.max_players, + host_player_id=game.host.id, + status=game.status, + is_private=game.password is not None, + num_of_players=len(game.players) + ) for game in games] + return games_list + + +@db_session +def list_of_games() -> List[GameResponse]: games = Game.select() games_list = [GameResponse( name=game.name, From 9e262204934b261f325e02d592ed4bd5f128e775 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 11 Oct 2023 20:01:30 -0300 Subject: [PATCH 019/224] Se hicieron tests para el endpoint de iniciar partida --- app/tests/game_tests/test_init_of_games.py | 114 +++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 app/tests/game_tests/test_init_of_games.py diff --git a/app/tests/game_tests/test_init_of_games.py b/app/tests/game_tests/test_init_of_games.py new file mode 100644 index 0000000..bac3470 --- /dev/null +++ b/app/tests/game_tests/test_init_of_games.py @@ -0,0 +1,114 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player +from pony.orm import db_session + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Game.select().delete() + Player.select().delete() + + +def test_init_game_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}") + assert response.status_code == 200, "El codigo de estado de la respuesta no coincide con el esperado" + cleanup_database() + + +def test_init_game_less_min_players(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:2]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() + + +def test_init_game_twice(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() + + +def test_init_game_bad_host_id(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id + 1}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() From 3de76642f188110d80b35073bfcb02166742fd78 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 11 Oct 2023 20:46:20 -0300 Subject: [PATCH 020/224] se remueve la logica de websockets de los servicios y se la agrega al controlador --- app/routers/games/games.py | 62 +++++++++++++++++++++++++++++------ app/routers/games/services.py | 34 ------------------- app/routers/games/utils.py | 8 +++++ 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index d4c1810..ed6f30c 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -4,6 +4,7 @@ from . import utils from .schemas import * from ..players.schemas import PlayerResponse +from ..websockets.utils import player_connections router = APIRouter( @@ -23,26 +24,67 @@ def get_game_information(game_name: str): @router.post("/", response_model=GameCreationOut, status_code=status.HTTP_201_CREATED) -def create_game(game_data: GameCreationIn): - return services.create_game(game_data) +async def create_game(game_data: GameCreationIn): + new_game = services.create_game(game_data) + + json_msg = { + "event": utils.Events.GAME_CREATED, + "game_name": new_game.name + } + await player_connections.broadcast(json_msg) + + return new_game @router.patch("/{name}/init") -def start(name: str, host_player_id: int) -> GameStartOut: +async def start(name: str, host_player_id: int) -> GameStartOut: utils.verify_game_can_start(name, host_player_id) - return services.start_game(name) + game = services.start_game(name) + + json_msg = { + "event": utils.Events.GAME_STARTED, + "game_name": game.name + } + await player_connections.broadcast(json_msg) + + return game @router.patch("/{game_name}", response_model=GameUpdateOut, status_code=status.HTTP_200_OK) -def update_game(game_name: str, game_data: GameUpdateIn): - return services.update_game(game_name, game_data) +async def update_game(game_name: str, game_data: GameUpdateIn): + game_updated = services.update_game(game_name, game_data) + + json_msg = { + "event": utils.Events.GAME_UPDATED, + "game_name": game_updated.name + } + await player_connections.broadcast(json_msg) + + return game_updated @router.delete("/{game_name}", status_code=status.HTTP_200_OK) -def delete_game(game_name: str): - return services.delete_game(game_name) +async def delete_game(game_name: str): + services.delete_game(game_name) + + json_msg = { + "event": utils.Events.GAME_DELETED, + "game_name": game_name + } + await player_connections.broadcast(json_msg) + + return {"message": "Game deleted"} @router.patch("/join/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) -def join_player(game_name: str, game_data: GameInformationIn): - return services.join_player(game_name, game_data) +async def join_player(game_name: str, game_data: GameInformationIn): + game = services.join_player(game_name, game_data) + + json_msg = { + "event": utils.Events.PLAYER_JOINED, + "player_id": game_data.player_id, + "game_name": game.name + } + await player_connections.broadcast(json_msg) + + return game diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 6774e1b..90f8d51 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -4,8 +4,6 @@ from fastapi import HTTPException, status from .utils import find_game_by_name, list_of_games from ..cards import services as cards_services -from ..websockets.utils import player_connections -import asyncio def get_games() -> list[GameResponse]: @@ -58,12 +56,6 @@ def create_game(game_data: GameCreationIn) -> GameCreationOut: host.game = new_game.name - json_msg = { - "event": "game_created", - "game_name": new_game.name - } - asyncio.run(player_connections.broadcast(json_msg)) - return GameCreationOut( name=new_game.name, status=new_game.status, @@ -89,12 +81,6 @@ def update_game(game_name: str, request_data: GameUpdateIn) -> GameUpdateOut: game.max_players = request_data.max_players game.password = request_data.password - json_msg = { - "event": "game_updated", - "game_name": game.name - } - asyncio.run(player_connections.broadcast(json_msg)) - return GameUpdateOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -113,14 +99,7 @@ def delete_game(game_name: str): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid game name") - json_msg = { - "event": "game_deleted", - "game_name": game.name - } - asyncio.run(player_connections.broadcast(json_msg)) - game.delete() - return {"message": "Game deleted"} @db_session @@ -151,13 +130,6 @@ def join_player(game_name: str, game_data: GameInformationIn) -> GameInformation players_joined = game.players.select()[:] num_players_joined = len(players_joined) - json_msg = { - "event": "player_join", - "player_id": player.id, - "game_name": game.name - } - asyncio.run(player_connections.broadcast(json_msg)) - return GameInformationOut(name=game.name, min_players=game.min_players, max_players=game.max_players, @@ -187,12 +159,6 @@ def start_game(name: str) -> Game: game.status = GameStatus.STARTED game.turn = 0 - json_msg = { - "event": "game_started", - "game_name": game.name - } - asyncio.run(player_connections.broadcast(json_msg)) - return GameStartOut( list_of_players=game.players, status=game.status, diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 563d5d2..385ff25 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -5,6 +5,14 @@ from .schemas import * +class Events(str, Enum): + GAME_CREATED = 'game_created' + GAME_UPDATED = 'game_updated' + GAME_DELETED = 'game_deleted' + GAME_STARTED = 'game_started' + PLAYER_JOINED = 'player_joined' + + @db_session def find_game_by_name(game_name: str): game = Game.get(name=game_name) From 18d7edd802377981dc829155573874154c38260a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 06:07:02 -0300 Subject: [PATCH 021/224] Resueltos conflictos de fusion con develop, y se arregla problema con json al iniciar partida. --- app/routers/games/games.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 22f4452..46926ae 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -43,7 +43,7 @@ async def start(name: str, host_player_id: int) -> GameStartOut: json_msg = { "event": utils.Events.GAME_STARTED, - "game_name": game.name + "game_name": name } await player_connections.broadcast(json_msg) @@ -56,7 +56,7 @@ async def update_game(game_name: str, game_data: GameUpdateIn): json_msg = { "event": utils.Events.GAME_UPDATED, - "game_name": game_updated.name + "game_name": game_name } await player_connections.broadcast(json_msg) From 5130e4161570447df859311885f3a5cdf610ff2c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 07:36:44 -0300 Subject: [PATCH 022/224] Se agrega endpoint para abandonar partida, solo host puede abandonar partida --- app/routers/games/games.py | 18 ++++++++++++++++++ app/routers/games/services.py | 6 ++++++ app/routers/games/utils.py | 21 +++++++++++++++++++++ app/routers/websockets/utils.py | 6 ++++++ 4 files changed, 51 insertions(+) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 46926ae..c807b6f 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -88,3 +88,21 @@ async def join_player(game_name: str, game_data: GameInformationIn): await player_connections.broadcast(json_msg) return game + + +@router.patch("/leave/{game_name}", status_code=status.HTTP_200_OK) +async def leave_game(game_name: str, player_id: int): + utils.verify_game_can_be_canceled(game_name, player_id) + + json_msg = { + "event": utils.Events.GAME_CANCELED, + "game_name": game_name + } + await player_connections.send_game_event(game_name, json_msg) + + services.leave_game(game_name) + + json_msg["event"] = utils.Events.GAME_DELETED + await player_connections.broadcast(json_msg) + + return {"message": "Game canceled"} diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 259210e..15a8d9d 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -164,3 +164,9 @@ def start_game(name: str) -> Game: status=game.status, top_card_face=list(game.draw_deck)[0].type ) + + +@db_session +def leave_game(game_name: str): + game = Game.get(name=game_name) + game.delete() diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 3721d5c..9b197ce 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -10,6 +10,7 @@ class Events(str, Enum): GAME_UPDATED = 'game_updated' GAME_DELETED = 'game_deleted' GAME_STARTED = 'game_started' + GAME_CANCELED = 'game_canceled' PLAYER_JOINED = 'player_joined' @@ -49,6 +50,26 @@ def verify_game_can_start(name: str, host_player_id: int): ) +@db_session +def verify_game_can_be_canceled(game_name: str, host_player_id: int): + game = find_game_by_name(game_name) + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Game not found" + ) + if game.status != GameStatus.UNSTARTED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The game is not in unstarted status" + ) + if game.host.id != host_player_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only the host player can canceled the game." + ) + + @db_session def list_of_unstarted_games() -> List[GameResponse]: games = Game.select(lambda game: game.status != diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 785a515..b5de158 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -38,6 +38,12 @@ async def send_message(self, player_id: int, message_from: str, message: str): except KeyError: pass + async def send_game_event(self, game_name: str, message): + players_to_send_message = get_players_id(game_name) + for player_id, websocket in self.active_connections.items(): + if player_id in players_to_send_message: + await websocket.send_json(message) + async def broadcast(self, message): for player_id, websocket in self.active_connections.items(): await websocket.send_json(message) From 1d8ee89e5bfa144c0dc33380fa0b3f3b1f088ded Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 10:24:31 -0300 Subject: [PATCH 023/224] Intento de implementacion para que un jugador que no sea host pueda abandonar la partida --- app/routers/games/games.py | 37 ++++++++++++++++++++++++++++++++--- app/routers/games/services.py | 7 ++++++- app/routers/games/utils.py | 28 +++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index c807b6f..eb18ede 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -90,8 +90,8 @@ async def join_player(game_name: str, game_data: GameInformationIn): return game -@router.patch("/leave/{game_name}", status_code=status.HTTP_200_OK) -async def leave_game(game_name: str, player_id: int): +@router.patch("/cancel/{game_name}", status_code=status.HTTP_200_OK) +async def cancel_game(game_name: str, player_id: int): utils.verify_game_can_be_canceled(game_name, player_id) json_msg = { @@ -100,9 +100,40 @@ async def leave_game(game_name: str, player_id: int): } await player_connections.send_game_event(game_name, json_msg) - services.leave_game(game_name) + services.cancel_game(game_name) json_msg["event"] = utils.Events.GAME_DELETED await player_connections.broadcast(json_msg) return {"message": "Game canceled"} + + +@router.patch("/leave/{game_name}", status_code=status.HTTP_200_OK) +async def leave_game(game_name: str, player_id: int): + utils.verify_game_can_be_abandon(game_name, player_id) + json_msg = { + "event": utils.Events.PLAYER_LEFT, + "game_name": game_name + } + await player_connections.send_game_event(game_name, json_msg) + + services.leave_game(game_name, player_id) + + json_msg["event"] = utils.Events.GAME_UPDATED + + await player_connections.broadcast(json_msg) + + return {"message": "Player left game"} + + + + + + + + + + + + + diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 15a8d9d..1af6a04 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -167,6 +167,11 @@ def start_game(name: str) -> Game: @db_session -def leave_game(game_name: str): +def cancel_game(game_name: str): game = Game.get(name=game_name) game.delete() + +@db_session +def leave_game(game_name: str, player_id: int): + #Falta implementacion para sacar al jugador de la partida + pass diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 9b197ce..c65aa71 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -1,6 +1,6 @@ from typing import List from fastapi import WebSocket, HTTPException, status -from app.database.models import Game +from app.database.models import Game, Player from pony.orm import * from .schemas import * @@ -12,6 +12,7 @@ class Events(str, Enum): GAME_STARTED = 'game_started' GAME_CANCELED = 'game_canceled' PLAYER_JOINED = 'player_joined' + PLAYER_LEFT = 'player_left' @db_session @@ -69,6 +70,31 @@ def verify_game_can_be_canceled(game_name: str, host_player_id: int): detail="Only the host player can canceled the game." ) +@db_session +def verify_game_can_be_abandon(game_name: str, player_id: int): + game = find_game_by_name(game_name) + player = Player.select(id=player_id) + + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Game not found" + ) + if game.status != GameStatus.UNSTARTED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The game is not in unstarted status" + ) + if not player: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player not found" + ) + if player not in game.players: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Player not in the game" + ) @db_session def list_of_unstarted_games() -> List[GameResponse]: From 4b6a1dd79d397b78838c2be986bee4c484fe5e5b Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Thu, 12 Oct 2023 15:16:50 -0300 Subject: [PATCH 024/224] se agrega servicio para finalizar la partida --- app/routers/cards/services.py | 6 ++++++ app/routers/games/services.py | 18 ++++++++++++++++-- app/routers/games/utils.py | 13 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index d0d984b..b5e0fc7 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -100,3 +100,9 @@ def deal_cards_to_players(game: Game, deck: List[Card]): for player in game.players: card = deck.pop(0) player.hand.add(card) + + +@db_session +def card_is_in_player_hand(card_name: str, hand_player: list[Card]) -> bool: + card_names = map(lambda card: card.name, hand_player) + return card_name in card_names diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 259210e..50d0ae1 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,8 +1,9 @@ from pony.orm import * from app.database.models import Game, Player from .schemas import * +from ..players.schemas import PlayerRol from fastapi import HTTPException, status -from .utils import find_game_by_name, list_of_unstarted_games +from .utils import * from ..cards import services as cards_services @@ -152,9 +153,13 @@ def start_game(name: str) -> Game: cards_services.deal_cards_to_players(game, draw_deck) game.draw_deck.add(draw_deck) - # setting the position of the players + # setting the position and rol of the players for idx, player in enumerate(game.players): player.position = idx + if cards_services.card_is_in_player_hand('La Cosa', player.hand): + player.rol = PlayerRol.THE_THING + else: + player.rol = PlayerRol.HUMAN game.status = GameStatus.STARTED game.turn = 0 @@ -164,3 +169,12 @@ def start_game(name: str) -> Game: status=game.status, top_card_face=list(game.draw_deck)[0].type ) + + +@db_session +def finish_game(name: str) -> Game: + game: Game = find_game_by_name(name) + verify_game_can_finish(game) + game.status = GameStatus.ENDED + + return game diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 3721d5c..236da7a 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -3,6 +3,7 @@ from app.database.models import Game from pony.orm import * from .schemas import * +from ..players.schemas import PlayerRol class Events(str, Enum): @@ -49,6 +50,18 @@ def verify_game_can_start(name: str, host_player_id: int): ) +@db_session +def verify_game_can_finish(game: Game): + players_eliminated = game.players.select( + lambda player: player.rol == PlayerRol.ELIMINATED) + + if count(players_eliminated) < count(game.players) - 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"There must be exactly one player not eliminated." + ) + + @db_session def list_of_unstarted_games() -> List[GameResponse]: games = Game.select(lambda game: game.status != From c5714a465e845241470d0cc090e964e3666e349b Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 17:25:47 -0300 Subject: [PATCH 025/224] Se termino con la implementacion de los 2 endpoints para abandonar partida y cancelar partida --- app/routers/games/games.py | 33 ++++++++++++--------------------- app/routers/games/services.py | 21 ++++++++++++++++++--- app/routers/games/utils.py | 4 +++- app/routers/websockets/utils.py | 5 +++-- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index eb18ede..447a10b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -90,7 +90,7 @@ async def join_player(game_name: str, game_data: GameInformationIn): return game -@router.patch("/cancel/{game_name}", status_code=status.HTTP_200_OK) +@router.delete("/cancel/{game_name}", status_code=status.HTTP_200_OK) async def cancel_game(game_name: str, player_id: int): utils.verify_game_can_be_canceled(game_name, player_id) @@ -98,42 +98,33 @@ async def cancel_game(game_name: str, player_id: int): "event": utils.Events.GAME_CANCELED, "game_name": game_name } - await player_connections.send_game_event(game_name, json_msg) + await player_connections.send_event_to_other_players_in_game(game_name=game_name, + message=json_msg, + excluded_id=player_id) services.cancel_game(game_name) json_msg["event"] = utils.Events.GAME_DELETED - await player_connections.broadcast(json_msg) + await player_connections.broadcast(message=json_msg) return {"message": "Game canceled"} -@router.patch("/leave/{game_name}", status_code=status.HTTP_200_OK) +@router.patch("/leave/{game_name}", response_model=GameInformationOut, status_code=status.HTTP_200_OK) async def leave_game(game_name: str, player_id: int): utils.verify_game_can_be_abandon(game_name, player_id) json_msg = { "event": utils.Events.PLAYER_LEFT, "game_name": game_name } - await player_connections.send_game_event(game_name, json_msg) + await player_connections.send_event_to_other_players_in_game(game_name=game_name, + message=json_msg, + excluded_id=player_id) - services.leave_game(game_name, player_id) + gameInformation = services.leave_game(game_name, player_id) json_msg["event"] = utils.Events.GAME_UPDATED - await player_connections.broadcast(json_msg) - - return {"message": "Player left game"} - - - - - - - - - - - - + await player_connections.broadcast(message=json_msg) + return gameInformation diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 1af6a04..34e207c 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -171,7 +171,22 @@ def cancel_game(game_name: str): game = Game.get(name=game_name) game.delete() + @db_session -def leave_game(game_name: str, player_id: int): - #Falta implementacion para sacar al jugador de la partida - pass +def leave_game(game_name: str, player_id: int) -> GameInformationOut: + game = Game.get(name=game_name) + player = Player.get(id=player_id) + game.players.remove(player) + players_joined = game.players.select()[:] + num_players_joined = len(players_joined) + return GameInformationOut(name=game.name, + min_players=game.min_players, + max_players=game.max_players, + is_private=game.password is not None, + status=game.status, + host_player_name=game.host.name, + host_player_id=game.host.id, + num_of_players=num_players_joined, + list_of_players=[PlayerResponse.model_validate( + p) for p in players_joined] + ) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index c65aa71..436b468 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -70,10 +70,11 @@ def verify_game_can_be_canceled(game_name: str, host_player_id: int): detail="Only the host player can canceled the game." ) + @db_session def verify_game_can_be_abandon(game_name: str, player_id: int): game = find_game_by_name(game_name) - player = Player.select(id=player_id) + player = Player.get(id=player_id) if not game: raise HTTPException( @@ -96,6 +97,7 @@ def verify_game_can_be_abandon(game_name: str, player_id: int): detail="Player not in the game" ) + @db_session def list_of_unstarted_games() -> List[GameResponse]: games = Game.select(lambda game: game.status != diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index b5de158..c7eb6d5 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -38,11 +38,12 @@ async def send_message(self, player_id: int, message_from: str, message: str): except KeyError: pass - async def send_game_event(self, game_name: str, message): + async def send_event_to_other_players_in_game(self, game_name: str, message, excluded_id: int): players_to_send_message = get_players_id(game_name) for player_id, websocket in self.active_connections.items(): if player_id in players_to_send_message: - await websocket.send_json(message) + if player_id != excluded_id: + await websocket.send_json(message) async def broadcast(self, message): for player_id, websocket in self.active_connections.items(): From 6d689609eb42de986ef32a6cc6882bd99ab3d07a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 22:37:36 -0300 Subject: [PATCH 026/224] Se agregan tests para el endpoint de cancelar partida --- app/tests/game_tests/test_cancel_games.py | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/tests/game_tests/test_cancel_games.py diff --git a/app/tests/game_tests/test_cancel_games.py b/app/tests/game_tests/test_cancel_games.py new file mode 100644 index 0000000..7736dea --- /dev/null +++ b/app/tests/game_tests/test_cancel_games.py @@ -0,0 +1,112 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player +from pony.orm import db_session + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Game.select().delete() + Player.select().delete() + + +def test_cancel_game_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.delete( + f"/games/cancel/TestGame?player_id={players[0].id}") + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + cleanup_database() + + +def test_cancel_inexistent_game(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.delete( + f"/games/cancel/BadGameName?player_id={players[0].id}") + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (Not Found)" + cleanup_database() + + +def test_cancel_game_not_in_unstarted_state(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + # This change to STARTED status + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + response = client.delete( + f"/games/cancel/TestGame?player_id={players[0].id}") + assert response.status_code == 400, f"El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() + + +def test_cancel_game_bad_host_id(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + # This change to STARTED status + response = client.delete( + f"/games/cancel/TestGame?player_id={players[1].id}") + assert response.status_code == 400, f"El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() From aa7eda070133698322b276701eea2a8747ae9201 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 22:52:59 -0300 Subject: [PATCH 027/224] Agregado tests para el endpoint de abandonar partida. --- app/tests/game_tests/test_leave_games.py | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 app/tests/game_tests/test_leave_games.py diff --git a/app/tests/game_tests/test_leave_games.py b/app/tests/game_tests/test_leave_games.py new file mode 100644 index 0000000..41097a2 --- /dev/null +++ b/app/tests/game_tests/test_leave_games.py @@ -0,0 +1,132 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player +from pony.orm import db_session + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Game.select().delete() + Player.select().delete() + + +def test_leave_game_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/leave/TestGame?player_id={players[2].id}") + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + cleanup_database() + + +def test_leave_inexistent_game(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/leave/BadGameName?player_id={players[2].id}") + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (Not Found)" + cleanup_database() + + +def test_leave_game_not_in_unstarted_state(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + + # Change to STARTED status + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + response = client.patch( + f"/games/leave/TestGame?player_id={players[2].id}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() + + +def test_leave_game_with_bad_player_id(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch( + f"/games/leave/TestGame?player_id={players[1].id * 7}") + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (Not Found)" + cleanup_database() + + +def test_leave_game_twice(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + client.patch(f"/games/leave/TestGame?player_id={players[2].id}") + response = client.patch(f"/games/leave/TestGame?player_id={players[2].id}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() \ No newline at end of file From 3cdf02ef7cfc53c41cd4fdf5edc16795bbdc0e60 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 12 Oct 2023 22:53:46 -0300 Subject: [PATCH 028/224] Se arregla formato de archivo para que cumpla con standard pep8 --- app/tests/game_tests/test_leave_games.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/game_tests/test_leave_games.py b/app/tests/game_tests/test_leave_games.py index 41097a2..3f00e2e 100644 --- a/app/tests/game_tests/test_leave_games.py +++ b/app/tests/game_tests/test_leave_games.py @@ -83,7 +83,7 @@ def test_leave_game_not_in_unstarted_state(): "password": "secret" } client.patch("/games/join/TestGame", json=game_data) - + # Change to STARTED status client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") response = client.patch( @@ -129,4 +129,4 @@ def test_leave_game_twice(): client.patch(f"/games/leave/TestGame?player_id={players[2].id}") response = client.patch(f"/games/leave/TestGame?player_id={players[2].id}") assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" - cleanup_database() \ No newline at end of file + cleanup_database() From a411b63f9edd87908c8a33eb646c4bde3603d1ae Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 13 Oct 2023 16:11:46 -0300 Subject: [PATCH 029/224] Se quita carta de mas del archivo csv --- app/resources/cartas.csv | 1 - 1 file changed, 1 deletion(-) diff --git a/app/resources/cartas.csv b/app/resources/cartas.csv index c8760b0..250ef52 100644 --- a/app/resources/cartas.csv +++ b/app/resources/cartas.csv @@ -27,7 +27,6 @@ 5;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 6;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 9;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -11;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 4;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. 9;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. 4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. From 86af02c01cb3172390f56dae1626a19a74436f3c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 13 Oct 2023 21:25:33 -0300 Subject: [PATCH 030/224] Se implemento correctamente el reparto de cartas para que no incluya cartas panico ni infectado --- app/resources/cartas.csv | 1 - app/routers/cards/services.py | 25 ++++++++++++++++++------- app/routers/games/services.py | 7 +++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/resources/cartas.csv b/app/resources/cartas.csv index c8760b0..250ef52 100644 --- a/app/resources/cartas.csv +++ b/app/resources/cartas.csv @@ -27,7 +27,6 @@ 5;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 6;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 9;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -11;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. 4;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. 9;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. 4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index d0d984b..664f75e 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -73,9 +73,11 @@ def delete_card(card_id: int): @db_session -def build_deck(players: int) -> List[Card]: - deck = list(Card.select(lambda c: c.number <= players and - c.name != 'La Cosa')) +def build_deal_deck(players: int) -> list[Card]: + deal_deck = list(Card.select(lambda c: c.number <= players and + c.name != 'La Cosa' and + c.name != '¡Infectado!' and + c.type != 'PANIC')) the_thing = Card.get(name='La Cosa') if the_thing is None: @@ -84,18 +86,27 @@ def build_deck(players: int) -> List[Card]: detail='The card "The Thing" not found.' ) - random.shuffle(deck) + random.shuffle(deal_deck) # Insert the The Thing card making sure that # it will go to a player's hand on the first deal. random_index = random.randint(0, players - 1) - deck.insert(random_index, the_thing) + deal_deck.insert(random_index, the_thing) - return deck + return deal_deck @db_session -def deal_cards_to_players(game: Game, deck: List[Card]): +def build_draw_deck(deal_deck: list[Card], players: int) -> list[Card]: + draw_deck = list(Card.select(lambda c: c.number <= players and + (c.name == '¡Infectado!' or c.type == 'PANIC'))) + draw_deck.extend(deal_deck) + random.shuffle(draw_deck) + return draw_deck + + +@db_session +def deal_cards_to_players(game: Game, deck: list[Card]): for _ in range(4): for player in game.players: card = deck.pop(0) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 259210e..9cccb36 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -148,8 +148,11 @@ def start_game(name: str) -> Game: game: Game = find_game_by_name(name) players_joined = count(game.players) - draw_deck = cards_services.build_deck(players_joined) - cards_services.deal_cards_to_players(game, draw_deck) + deal_deck = cards_services.build_deal_deck(players_joined) + cards_services.deal_cards_to_players(game, deal_deck) + + draw_deck = cards_services.build_draw_deck( + deal_deck=deal_deck, players=players_joined) game.draw_deck.add(draw_deck) # setting the position of the players From 7e7eb75d6f28492016b55a43b6338c188bdc6daf Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 13 Oct 2023 22:42:37 -0300 Subject: [PATCH 031/224] Se implemento que al iniciar una partida a cada jugador se manda pow websocket los ids de las cartas que tiene en la mano --- app/routers/games/games.py | 7 ++++--- app/routers/games/utils.py | 20 +++++++++++++++++++- app/routers/websockets/utils.py | 6 ++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 46926ae..9312ab8 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -1,9 +1,8 @@ -from fastapi import APIRouter, status, HTTPException, WebSocket -from typing import List +from fastapi import APIRouter, status +from app.database.models import Player from . import services from . import utils from .schemas import * -from ..players.schemas import PlayerResponse from ..websockets.utils import player_connections @@ -41,6 +40,8 @@ async def start(name: str, host_player_id: int) -> GameStartOut: utils.verify_game_can_start(name, host_player_id) game = services.start_game(name) + await utils.send_initial_cards(name) + json_msg = { "event": utils.Events.GAME_STARTED, "game_name": name diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 3721d5c..8cee01f 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -1,8 +1,9 @@ from typing import List from fastapi import WebSocket, HTTPException, status -from app.database.models import Game +from app.database.models import Game, Player from pony.orm import * from .schemas import * +from ..websockets.utils import player_connections, get_players_id class Events(str, Enum): @@ -11,6 +12,7 @@ class Events(str, Enum): GAME_DELETED = 'game_deleted' GAME_STARTED = 'game_started' PLAYER_JOINED = 'player_joined' + PLAYER_INIT_HAND = 'player_init_hand' @db_session @@ -78,3 +80,19 @@ def list_of_games() -> List[GameResponse]: num_of_players=len(game.players) ) for game in games] return games_list + + +@db_session +async def send_initial_cards(game_name: str): + playersID = get_players_id(game_name) + + for idx in playersID: + player = Player.get(id=idx) + hand_cards = [card.id for card in player.hand] + json_msg = { + "event": Events.PLAYER_INIT_HAND, + "game_name": game_name, + "player_id": player.id, + "hand_cards": hand_cards + } + await player_connections.send_event_to(player_id=player.id, message=json_msg) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 785a515..a04120b 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -38,6 +38,12 @@ async def send_message(self, player_id: int, message_from: str, message: str): except KeyError: pass + async def send_event_to(self, player_id: int, message): + try: + await self.active_connections[player_id].send_json(message) + except KeyError: + pass + async def broadcast(self, message): for player_id, websocket in self.active_connections.items(): await websocket.send_json(message) From 9be38425e8e49f474248c36ad638a7e00975ecab Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 13 Oct 2023 23:00:16 -0300 Subject: [PATCH 032/224] Se agrega restriccion de que el host no puede abandonar una partida, solo cancelarla --- app/routers/games/utils.py | 7 ++++++- app/tests/game_tests/test_leave_games.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 436b468..8fe24fe 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -67,7 +67,7 @@ def verify_game_can_be_canceled(game_name: str, host_player_id: int): if game.host.id != host_player_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Only the host player can canceled the game." + detail="Only the host player can cancel the game." ) @@ -96,6 +96,11 @@ def verify_game_can_be_abandon(game_name: str, player_id: int): status_code=status.HTTP_400_BAD_REQUEST, detail="Player not in the game" ) + if player == game.host: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Host of the game can only cancel the game" + ) @db_session diff --git a/app/tests/game_tests/test_leave_games.py b/app/tests/game_tests/test_leave_games.py index 3f00e2e..942bbb7 100644 --- a/app/tests/game_tests/test_leave_games.py +++ b/app/tests/game_tests/test_leave_games.py @@ -130,3 +130,22 @@ def test_leave_game_twice(): response = client.patch(f"/games/leave/TestGame?player_id={players[2].id}") assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" cleanup_database() + + +def test_leave_game_host_try(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + response = client.patch(f"/games/leave/TestGame?player_id={players[0].id}") + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + cleanup_database() From 31e5c12f1887fe65513fa2190bf3b0c365444fe6 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 14 Oct 2023 12:58:15 -0300 Subject: [PATCH 033/224] Al ser borrado un juego, se limpia la mano del jugador. --- app/routers/games/services.py | 3 +++ app/tests/game_tests/test_delete_of_games.py | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 34e207c..5b8818b 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -99,6 +99,9 @@ def delete_game(game_name: str): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid game name") + for player in game.players: + player.hand.clear() + game.delete() diff --git a/app/tests/game_tests/test_delete_of_games.py b/app/tests/game_tests/test_delete_of_games.py index dbd3099..a7240a2 100644 --- a/app/tests/game_tests/test_delete_of_games.py +++ b/app/tests/game_tests/test_delete_of_games.py @@ -6,9 +6,10 @@ class FakePlayer: - def __init__(self, id, username): + def __init__(self, id, username, hand): self.id = id self.username = username + self.hand = hand class FakeGame: @@ -38,9 +39,9 @@ def delete(): pass -ignacio = FakePlayer(1, "Ignacio") -anelio = FakePlayer(2, "Anelio") -ezequiel = FakePlayer(3, "Ezequiel") +ignacio = FakePlayer(1, "Ignacio", []) +anelio = FakePlayer(2, "Anelio", []) +ezequiel = FakePlayer(3, "Ezequiel", []) games = [ From 5418d2d832bef55e216182800eb15407e38110c9 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 14 Oct 2023 21:46:28 -0300 Subject: [PATCH 034/224] Se agrega endpoint implementado para el descarte de cartas --- app/routers/games/games.py | 7 ++ app/routers/games/schemas.py | 7 ++ app/routers/games/services.py | 13 ++- app/routers/games/utils.py | 36 +++++++- app/routers/websockets/utils.py | 2 +- app/tests/game_tests/test_game_discard.py | 101 ++++++++++++++++++++++ 6 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 app/tests/game_tests/test_game_discard.py diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 812cfbe..9156081 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -129,3 +129,10 @@ async def leave_game(game_name: str, player_id: int): await player_connections.broadcast(message=json_msg) return gameInformation + + +@router.patch("/{game_name}/discard", status_code=status.HTTP_200_OK) +async def discard_card(game_name: str, game_data: DiscardInformationIn): + utils.verify_discard_can_be_done(game_name, game_data) + services.discard_card(game_name, game_data) + return {"message": "Card discarded"} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 68826ef..6e1c3f5 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -95,3 +95,10 @@ class GameStartOut(BaseModel): list_of_players: List[PlayerResponse] status: GameStatus top_card_face: CardType + + +class DiscardInformationIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + card_id: int diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 56cd168..0537c55 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,5 +1,5 @@ from pony.orm import * -from app.database.models import Game, Player +from app.database.models import Game, Player, Card from .schemas import * from fastapi import HTTPException, status from .utils import find_game_by_name, list_of_unstarted_games @@ -196,3 +196,14 @@ def leave_game(game_name: str, player_id: int) -> GameInformationOut: list_of_players=[PlayerResponse.model_validate( p) for p in players_joined] ) + + +@db_session +def discard_card(game_name: str, game_data: DiscardInformationIn): + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) + card = Card.get(id=game_data.card_id) + if card in player.hand: + player.hand.remove(card) + if game and card: + game.discard_deck.add(card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 871a91a..e244f5d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -1,6 +1,6 @@ from typing import List from fastapi import WebSocket, HTTPException, status -from app.database.models import Game, Player +from app.database.models import Game, Player, Card from pony.orm import * from .schemas import * from ..websockets.utils import player_connections, get_players_id @@ -150,3 +150,37 @@ async def send_initial_cards(game_name: str): "hand_cards": hand_cards } await player_connections.send_event_to(player_id=player.id, message=json_msg) + + +@db_session +def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) + card = Card.get(id=game_data.card_id) + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Game not found" + ) + if not player: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player not found" + ) + if not card: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Card not found" + ) + is_card_in_hand = select( + c for c in player.hand if (c.id == card.id)).exists() + if not is_card_in_hand: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The card doesn't belong to the player" + ) + if card.type == CardType.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The Thing cannot be discarded" + ) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 36931f1..1b358ec 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -43,7 +43,7 @@ async def send_event_to(self, player_id: int, message): await self.active_connections[player_id].send_json(message) except KeyError: pass - + async def send_event_to_other_players_in_game(self, game_name: str, message, excluded_id: int): players_to_send_message = get_players_id(game_name) for player_id, websocket in self.active_connections.items(): diff --git a/app/tests/game_tests/test_game_discard.py b/app/tests/game_tests/test_game_discard.py new file mode 100644 index 0000000..284dbce --- /dev/null +++ b/app/tests/game_tests/test_game_discard.py @@ -0,0 +1,101 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player, Card +from pony.orm import db_session + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Game.select().delete() + Player.select().delete() + + +def test_discard_card_for_inexistent_game(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + game_data = { + "player_id": players[0].id, + "card_id": 4 + } + response = client.patch("/games/BadGame/discard", json=game_data) + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (NOT FOUND)" + cleanup_database() + + +def test_discard_card_for_inexistent_player(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + game_data = { + "player_id": 123, + "card_id": 4 + } + response = client.patch("/games/TestGame/discard", json=game_data) + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (NOT FOUND)" + cleanup_database() + + +def test_discard_card_for_inexistent_card(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + client.patch(f"/games/TestGame/init?host_player_id={players[0].id}") + game_data = { + "player_id": players[0].id, + "card_id": 123 + } + response = client.patch("/games/TestGame/discard", json=game_data) + assert response.status_code == 404, "El codigo de estado de la respuesta no es 404 (NOT FOUND)" + cleanup_database() From a29310f68d988f55a3494e352d78fda298b9a1e0 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 14 Oct 2023 22:42:56 -0300 Subject: [PATCH 035/224] Se arreglan mas condiciones para validar que una carta pueda ser descartada. --- app/routers/games/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index e244f5d..bbc31c4 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -4,6 +4,7 @@ from pony.orm import * from .schemas import * from ..websockets.utils import player_connections, get_players_id +from ..players.schemas import PlayerRol class Events(str, Enum): @@ -157,6 +158,7 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): game = Game.get(name=game_name) player = Player.get(id=game_data.player_id) card = Card.get(id=game_data.card_id) + if not game: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -172,8 +174,17 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_404_NOT_FOUND, detail="Card not found" ) + + is_player_in_game = select( + p for p in game.players if (p.id == player.id)).exists() is_card_in_hand = select( c for c in player.hand if (c.id == card.id)).exists() + + if not is_player_in_game: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player is not in the game" + ) if not is_card_in_hand: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -184,3 +195,16 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="The Thing cannot be discarded" ) + if game.turn != player.position: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="It's not the turn of the player" + ) + if player.role == PlayerRol.INFECTED and card.name == '¡Infectado!': + infected_count = select(count(c) + for c in player.hand if c.name == '¡Infectado!') + if infected_count <= 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player is infected and cannot discard the card" + ) From 663a7f8aed1a3f628f3bec245fa2352a917d10c1 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sun, 15 Oct 2023 02:15:40 -0300 Subject: [PATCH 036/224] Tests CRUD cards finalizado y cambio en validacion --- app/routers/cards/services.py | 7 +- app/tests/card_tests/test_delete_cards.py | 88 +++++++++ app/tests/card_tests/test_get_cards.py | 47 +++++ app/tests/card_tests/test_patch_cards.py | 229 ++++++++++++++++++++++ 4 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 app/tests/card_tests/test_delete_cards.py create mode 100644 app/tests/card_tests/test_get_cards.py create mode 100644 app/tests/card_tests/test_patch_cards.py diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 664f75e..d4f5064 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -34,6 +34,11 @@ def create_card(card_data: CardCreationIn) -> Card: @db_session def find_card_by_id(id: int) -> Card: + if id < 1 or id > 109: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid card id, must be between 1 and 109." + ) + card = Card.get(id=id) if not card: @@ -43,7 +48,7 @@ def find_card_by_id(id: int) -> Card: ) if id != card.id: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid card id" + status_code=status.HTTP_400_BAD_REQUEST, detail="Something went wrong with the card id" ) return card diff --git a/app/tests/card_tests/test_delete_cards.py b/app/tests/card_tests/test_delete_cards.py new file mode 100644 index 0000000..efbba11 --- /dev/null +++ b/app/tests/card_tests/test_delete_cards.py @@ -0,0 +1,88 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Card +from app.routers.cards.schemas import * +from fastapi import status +from pony.orm import db_session + +client = TestClient(app) + +# Database functions + + +@db_session +def create_card_for_testing(id: int) -> Card: + return Card(id=id, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone") + + +@db_session +def cleanup_database() -> None: + Card.select().delete() + + +# Fake classes +class FakeCard: + def __init__(self, id, number, type, name, description): + self.id = id + self.number = number + self.type = type + self.name = name + self.description = description + + def delete(): + pass + + +card = FakeCard(id=1, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone") + + +# Parametrized functions +def perform_a_valid_delete_test(card_id): + response = client.delete(f"/cards/{card_id}") + assert response.status_code == status.HTTP_200_OK, f"El código de estado de la respuesta no es 200 (OK). El codigo de estado obtenido es {response.status_code}." + assert response.json() == { + "message": "Card deleted"}, f"El mensaje de la respuesta no es el esperado. El mensaje de la respuesta obtenida es {response.json()}." + + +def perform_an_invalid_delete_test(card_id, expected_status_code): + response = client.delete(f"/cards/{card_id}") + assert response.status_code == expected_status_code, f"El código de estado de la respuesta no es {expected_status_code}. El codigo de estado obtenido es {response.status_code}." + + +# Test cases +def test_delete_card_succesfully(mocker): + mocker.patch.object(Card, "get", return_value=card) + mocker.patch.object(FakeCard, "delete", return_value=None) + + perform_a_valid_delete_test("1") + + +def test_delete_card_with_invalid_id(mocker): + mocker.patch.object(Card, "get", return_value=card) + + perform_an_invalid_delete_test( + "error", status.HTTP_422_UNPROCESSABLE_ENTITY) + perform_an_invalid_delete_test("110", status.HTTP_400_BAD_REQUEST) + perform_an_invalid_delete_test("0", status.HTTP_400_BAD_REQUEST) + perform_an_invalid_delete_test("-4", status.HTTP_400_BAD_REQUEST) + + +def test_delete_non_existing_card(mocker): + mocker.patch.object(Card, "get", return_value=None) + + perform_an_invalid_delete_test("4", status.HTTP_404_NOT_FOUND) + + +def test_delete_an_already_deleted_card(): + cleanup_database() + create_card_for_testing(1) + + response = client.delete("/cards/1") + assert response.status_code == status.HTTP_200_OK, f"En la primer eliminación, el código de estado de la respuesta esperado es 200 (OK). El código de estado de la respuesta obtenida es {response.status_code}." + assert response.json() == { + "message": "Card deleted"}, f"En la primer eliminación, el mensaje de la respuesta no es el esperado. El mensaje de la respuesta obtenida es {response.json()}." + + perform_an_invalid_delete_test("1", status.HTTP_404_NOT_FOUND) + cleanup_database() diff --git a/app/tests/card_tests/test_get_cards.py b/app/tests/card_tests/test_get_cards.py new file mode 100644 index 0000000..de926fe --- /dev/null +++ b/app/tests/card_tests/test_get_cards.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Card +from app.routers.cards.schemas import * +from fastapi import status +from pony.orm import db_session + +client = TestClient(app) + +# Database functions +@db_session +def create_card_for_testing(id: int) -> Card: + return Card(id=id, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone") + + +@db_session +def cleanup_database() -> None: + Card.select().delete() + + +# Test cases +def test_empty_get_cards(): + response = client.get("/cards") + assert response.status_code == status.HTTP_200_OK, "El código de estado de la respuesta no es 200 (OK)." + assert response.json() == [ + ], f"El mensaje de la respuesta no es []. El mensaje de la respuesta obtenido es {response.json()}." + + +def test_get_one_card(mocker): + cleanup_database() + create_card_for_testing(1) + response = client.get("/cards") + + expected_response = [ + { + "id": 1, + "number": 4, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + } + ] + + assert response.status_code == 200, f"El código de estado de la respuesta no es 200 (OK). El codigo de estado obtenido es {response.status_code}." + assert response.json() == expected_response, "El mensaje de la respuesta no es el esperado." + cleanup_database() diff --git a/app/tests/card_tests/test_patch_cards.py b/app/tests/card_tests/test_patch_cards.py new file mode 100644 index 0000000..5447b1e --- /dev/null +++ b/app/tests/card_tests/test_patch_cards.py @@ -0,0 +1,229 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Card +from app.routers.cards.schemas import * +from fastapi import status +from pony.orm import db_session + +client = TestClient(app) + +# Database functions + + +@db_session +def create_card_for_testing(id: int) -> Card: + return Card(id=id, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone") + + +@db_session +def cleanup_database() -> None: + Card.select().delete() + + +# Fake classes +class FakeCard: + def __init__(self, id, number, type, name, description): + self.id = id + self.number = number + self.type = type + self.name = name + self.description = description + + def delete(): + pass + + +cards = [FakeCard(id=1, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone")] + + +# Parametrized functions +def perform_a_valid_update_test(card_id, success_request_data): + response = client.patch(f"/cards/{card_id}", json=success_request_data) + + assert response.status_code == status.HTTP_200_OK, f"El código de estado de la respuesta no es 200 (OK). El codigo de estado obtenido es {response.status_code}." + assert response.json() == { + "number": 10, + "type": "PANIC", + "name": "PANIC", + "description": "PANIC", + }, "El contenido de la respuesta no coincide con los datos actualizados." + + +def perform_an_invalid_update_test(card_id, expected_status_code, json_data): + response = client.patch(f"/cards/{card_id}", json=json_data) + assert response.status_code == expected_status_code, f"El código de estado de la respuesta no es {expected_status_code}. El codigo de estado obtenido es {response.status_code}." + + +# Test cases +def test_patch_card_succesfully(): + cleanup_database() + create_card_for_testing(1) + success_request_data = { + "number": 10, + "type": "PANIC", + "name": "PANIC", + "description": "PANIC" + } + + perform_a_valid_update_test("1", success_request_data) + cleanup_database() + + +def test_patch_card_greater_number(): + cleanup_database() + create_card_for_testing(1) + + greater_number_request_data = { + "number": 13, + "type": "PANIC", + "name": "PANIC", + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, greater_number_request_data) + cleanup_database() + + +def test_patch_card_lower_number(): + cleanup_database + lower_number_request_data = { + "number": 2, + "type": "PANIC", + "name": "PANIC", + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, lower_number_request_data) + cleanup_database() + + +def test_patch_card_invalid_number(): + cleanup_database() + invalid_number_request_data = { + "number": "error", + "type": "PANIC", + "name": "PANIC", + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, invalid_number_request_data) + cleanup_database() + + +def test_patch_card_invalid_type(): + cleanup_database() + + create_card_for_testing(1) + + invalid_type_request_data = { + "number": 4, + "type": "error", + "name": "PANIC", + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, invalid_type_request_data) + + cleanup_database() + + +def test_patch_card_invalid_name(): + cleanup_database() + + create_card_for_testing(1) + + invalid_name_request_data = { + "number": 4, + "type": "PANIC", + "name": 123, + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, invalid_name_request_data) + + cleanup_database() + + +def test_patch_card_short_name(): + cleanup_database() + + create_card_for_testing(1) + + short_name_request_data = { + "number": 4, + "type": "PANIC", + "name": "P", + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, short_name_request_data) + + cleanup_database() + + +def test_patch_card_long_name(): + cleanup_database() + + create_card_for_testing(1) + + long_name_request_data = { + "number": 4, + "name": "PANIC"*100, + "description": "PANIC" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, long_name_request_data) + + cleanup_database() + + +def test_patch_card_invalid_description(): + cleanup_database() + + create_card_for_testing(1) + + invalid_description_request_data = { + "number": 4, + "type": "PANIC", + "name": "PANIC", + "description": 123 + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, invalid_description_request_data) + + cleanup_database() + +def test_patch_card_short_description(): + cleanup_database() + + create_card_for_testing(1) + + short_description_request_data = { + "number": 4, + "type": "PANIC", + "name": "PANIC", + "description": "P" + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, short_description_request_data) + + cleanup_database() + + +def test_patch_card_long_description(): + cleanup_database() + + create_card_for_testing(1) + + long_description_request_data = { + "number": 4, + "type": "PANIC", + "name": "PANIC", + "description": "PANIC"*1000 + } + perform_an_invalid_update_test( + "1", status.HTTP_422_UNPROCESSABLE_ENTITY, long_description_request_data) + + cleanup_database() From db6241ae2c0a727942bb4f0ce527169e6a749e04 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sun, 15 Oct 2023 04:15:26 -0300 Subject: [PATCH 037/224] =?UTF-8?q?Agregados=20test=20para=20la=20creaci?= =?UTF-8?q?=C3=B3n=20de=20mazos=20y=20reparto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/websockets/utils.py | 2 +- app/tests/card_tests/test_cards_services.py | 126 ++++++++++++++++++++ app/tests/card_tests/test_delete_cards.py | 2 - app/tests/card_tests/test_get_cards.py | 2 +- app/tests/card_tests/test_patch_cards.py | 20 +--- 5 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 app/tests/card_tests/test_cards_services.py diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 36931f1..1b358ec 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -43,7 +43,7 @@ async def send_event_to(self, player_id: int, message): await self.active_connections[player_id].send_json(message) except KeyError: pass - + async def send_event_to_other_players_in_game(self, game_name: str, message, excluded_id: int): players_to_send_message = get_players_id(game_name) for player_id, websocket in self.active_connections.items(): diff --git a/app/tests/card_tests/test_cards_services.py b/app/tests/card_tests/test_cards_services.py new file mode 100644 index 0000000..e4129aa --- /dev/null +++ b/app/tests/card_tests/test_cards_services.py @@ -0,0 +1,126 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import * +from app.routers.cards.schemas import * +from fastapi import status +from pony.orm import db_session +from app.database.initialize_data import populate_card_table +from app.routers.cards.services import * + +client = TestClient(app) + + +@db_session +def create_card_for_testing(id: int) -> Card: + return Card(id=id, number=4, type="THE_THING", name="The Thing", + description="You are the thing, infect or kill everyone") + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + host = Player.get(id=host_player_id) + game = Game( + name=name, + min_players=min_players, + max_players=max_players, + password=password, + host=host + ) + commit() + return game + + +@db_session +def cleanup_database() -> None: + Card.select().delete() + Game.select().delete() + Player.select().delete() + + +# Test cases +@db_session +def test_build_deal_deck_without_the_thing_4_players() -> None: + cleanup_database() + populate_card_table() + Card.get(type="THE_THING").delete() + try: + build_deal_deck(4) + except HTTPException as e: + assert e.status_code == status.HTTP_404_NOT_FOUND + assert e.detail == "The card \"The Thing\" not found." + + cleanup_database() + + +def test_build_deal_deck_with_the_thing_on_the_first_hand_4_players() -> None: + cleanup_database() + populate_card_table() + list = build_deal_deck(4) + + for i in range(0, 3): + result = list[i].name == "The Thing" + + assert result == False + cleanup_database() + + +# Este está raro, renegué un toque y quedó muy hardcodeado +@db_session +def test_deal_cards_to_players_4_players() -> None: + cleanup_database() + populate_card_table() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + commit() + + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + + for player in players[0:]: + game.players.add(player) + + deal_deck = build_deal_deck(4) + deal_cards_to_players(game, deal_deck) + + for player in players: + assert len( + player.hand) == 4, f"El tamaño de la mano del jugador {player.name} esperado es 4. El tamaño de la mano del jugador {player.name} obtenido es {len(player.hand)}." + + cleanup_database() + + +# lo mismo que el anterior. +@db_session +def test_build_draw_deck_4_players() -> None: + cleanup_database() + + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + commit() + + game = create_test_game(name="TestGame", min_players=4, max_players=7, + password="secret", host_player_id=players[0].id) + + for player in players[0:]: + game.players.add(player) + + populate_card_table() + deal_deck = build_deal_deck(4) + deal_cards_to_players(game, deal_deck) + draw_deck = build_draw_deck(deal_deck, 4) + + assert len( + draw_deck) == 19, f"El tamaño del mazo de robo esperado es 19. El tamaño del mazo de robo obtenido es {len(draw_deck)}." + + cleanup_database() diff --git a/app/tests/card_tests/test_delete_cards.py b/app/tests/card_tests/test_delete_cards.py index efbba11..e614b00 100644 --- a/app/tests/card_tests/test_delete_cards.py +++ b/app/tests/card_tests/test_delete_cards.py @@ -7,8 +7,6 @@ client = TestClient(app) -# Database functions - @db_session def create_card_for_testing(id: int) -> Card: diff --git a/app/tests/card_tests/test_get_cards.py b/app/tests/card_tests/test_get_cards.py index de926fe..1a70e11 100644 --- a/app/tests/card_tests/test_get_cards.py +++ b/app/tests/card_tests/test_get_cards.py @@ -7,7 +7,7 @@ client = TestClient(app) -# Database functions + @db_session def create_card_for_testing(id: int) -> Card: return Card(id=id, number=4, type="THE_THING", name="The Thing", diff --git a/app/tests/card_tests/test_patch_cards.py b/app/tests/card_tests/test_patch_cards.py index 5447b1e..cadd35c 100644 --- a/app/tests/card_tests/test_patch_cards.py +++ b/app/tests/card_tests/test_patch_cards.py @@ -7,8 +7,6 @@ client = TestClient(app) -# Database functions - @db_session def create_card_for_testing(id: int) -> Card: @@ -21,23 +19,6 @@ def cleanup_database() -> None: Card.select().delete() -# Fake classes -class FakeCard: - def __init__(self, id, number, type, name, description): - self.id = id - self.number = number - self.type = type - self.name = name - self.description = description - - def delete(): - pass - - -cards = [FakeCard(id=1, number=4, type="THE_THING", name="The Thing", - description="You are the thing, infect or kill everyone")] - - # Parametrized functions def perform_a_valid_update_test(card_id, success_request_data): response = client.patch(f"/cards/{card_id}", json=success_request_data) @@ -195,6 +176,7 @@ def test_patch_card_invalid_description(): cleanup_database() + def test_patch_card_short_description(): cleanup_database() From 972dc05d2dad36eea3989453cf7a9c898ebfa910 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sun, 15 Oct 2023 04:51:56 -0300 Subject: [PATCH 038/224] =?UTF-8?q?Se=20borr=C3=B3=20c=C3=B3digo=20repetid?= =?UTF-8?q?o=20en=20los=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/utils.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 871a91a..90196cc 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -56,11 +56,7 @@ def verify_game_can_start(name: str, host_player_id: int): @db_session def verify_game_can_be_canceled(game_name: str, host_player_id: int): game = find_game_by_name(game_name) - if not game: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Game not found" - ) + if game.status != GameStatus.UNSTARTED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -78,11 +74,6 @@ def verify_game_can_be_abandon(game_name: str, player_id: int): game = find_game_by_name(game_name) player = Player.get(id=player_id) - if not game: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Game not found" - ) if game.status != GameStatus.UNSTARTED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From a33b6d62027b5491ffb785983015e69d0052bc53 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sun, 15 Oct 2023 10:44:51 -0300 Subject: [PATCH 039/224] =?UTF-8?q?saqu=C3=A9=20comentarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/card_tests/test_cards_services.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/tests/card_tests/test_cards_services.py b/app/tests/card_tests/test_cards_services.py index e4129aa..56b7eda 100644 --- a/app/tests/card_tests/test_cards_services.py +++ b/app/tests/card_tests/test_cards_services.py @@ -69,7 +69,6 @@ def test_build_deal_deck_with_the_thing_on_the_first_hand_4_players() -> None: cleanup_database() -# Este está raro, renegué un toque y quedó muy hardcodeado @db_session def test_deal_cards_to_players_4_players() -> None: cleanup_database() @@ -97,7 +96,6 @@ def test_deal_cards_to_players_4_players() -> None: cleanup_database() -# lo mismo que el anterior. @db_session def test_build_draw_deck_4_players() -> None: cleanup_database() From 533a5f92354b344f47b3630f363b6af575ad47ee Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 16 Oct 2023 20:22:16 -0300 Subject: [PATCH 040/224] Se agrega endpoint para obtener los ids de las cartas de un jugador --- app/routers/players/players.py | 5 +++++ app/routers/players/schemas.py | 4 ++++ app/routers/players/services.py | 13 +++++++++++++ 3 files changed, 22 insertions(+) diff --git a/app/routers/players/players.py b/app/routers/players/players.py index 4f95e34..1240dd3 100644 --- a/app/routers/players/players.py +++ b/app/routers/players/players.py @@ -37,3 +37,8 @@ def delete(id: int) -> None: def update(id: str, update_data: PlayerUpdateIn) -> PlayerResponse: player = services.update(id, update_data) return PlayerResponse.model_validate(player) + + +@router.get("/{player_id}/hand") +def get_player_hand(player_id: int) -> HandPlayer: + return services.get_player_hand(player_id) diff --git a/app/routers/players/schemas.py b/app/routers/players/schemas.py index ecf4452..3993ed5 100644 --- a/app/routers/players/schemas.py +++ b/app/routers/players/schemas.py @@ -31,3 +31,7 @@ class PlayerUpdateIn(BaseModel): class PlayerResponse(BasePlayer): id: int position: int + + +class HandPlayer(BasePlayer): + cards_id: List[int] diff --git a/app/routers/players/services.py b/app/routers/players/services.py index 5bb1c3b..9e9c8f4 100644 --- a/app/routers/players/services.py +++ b/app/routers/players/services.py @@ -38,3 +38,16 @@ def update(id: int, update_data: PlayerUpdateIn) -> Player: player = find_player_by_id(id) player.set(name=update_data.name) return player + + +@db_session +def get_player_hand(player_id: int) -> HandPlayer: + player = Player.get(id=player_id) + if not player: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Player with id '{id}' not found." + ) + return HandPlayer(name=player.name, + cards_id=[card.id for card in player.hand] + ) From d235a7c7ea95bd75aeea3bda970c417f84aa906c Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Tue, 17 Oct 2023 12:21:45 -0300 Subject: [PATCH 041/224] Se agrega mas informacion a la mano del jugador --- app/routers/players/players.py | 6 ++++-- app/routers/players/schemas.py | 4 ---- app/routers/players/services.py | 15 ++++----------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/routers/players/players.py b/app/routers/players/players.py index 1240dd3..66da7b3 100644 --- a/app/routers/players/players.py +++ b/app/routers/players/players.py @@ -2,6 +2,7 @@ from typing import List from . import services from .schemas import * +from ..cards.schemas import CardResponse router = APIRouter( @@ -40,5 +41,6 @@ def update(id: str, update_data: PlayerUpdateIn) -> PlayerResponse: @router.get("/{player_id}/hand") -def get_player_hand(player_id: int) -> HandPlayer: - return services.get_player_hand(player_id) +def get_player_hand(player_id: int) -> List[CardResponse]: + cards = services.get_player_hand(player_id) + return [CardResponse.model_validate(c) for c in cards] diff --git a/app/routers/players/schemas.py b/app/routers/players/schemas.py index 3993ed5..ecf4452 100644 --- a/app/routers/players/schemas.py +++ b/app/routers/players/schemas.py @@ -31,7 +31,3 @@ class PlayerUpdateIn(BaseModel): class PlayerResponse(BasePlayer): id: int position: int - - -class HandPlayer(BasePlayer): - cards_id: List[int] diff --git a/app/routers/players/services.py b/app/routers/players/services.py index 9e9c8f4..2dc1483 100644 --- a/app/routers/players/services.py +++ b/app/routers/players/services.py @@ -1,6 +1,6 @@ from pony.orm import * from fastapi import HTTPException, status -from app.database.models import Player +from app.database.models import Player, Card from .schemas import * @@ -41,13 +41,6 @@ def update(id: int, update_data: PlayerUpdateIn) -> Player: @db_session -def get_player_hand(player_id: int) -> HandPlayer: - player = Player.get(id=player_id) - if not player: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Player with id '{id}' not found." - ) - return HandPlayer(name=player.name, - cards_id=[card.id for card in player.hand] - ) +def get_player_hand(player_id: int) -> List[Card]: + player = find_player_by_id(player_id) + return player.hand.select()[:] From 999a218cc62c6f8ec21f3802e6b89cdf8dd1a271 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 15:20:23 -0300 Subject: [PATCH 042/224] Se agrega el subtipo a la tabla de cartas (CONTAGION, ACTION, DEFENSE, OBSTACLE, PANIC) y se modifico los tests para que se adapten al nuevo esquema. --- app/database/initialize_data.py | 4 +- app/database/models.py | 1 + app/resources/cartas.csv | 216 +++++++++++----------- app/routers/cards/schemas.py | 15 +- app/routers/cards/services.py | 4 + app/tests/card_tests/test_create_cards.py | 2 + app/tests/card_tests/test_delete_cards.py | 7 +- app/tests/card_tests/test_get_cards.py | 3 +- app/tests/card_tests/test_patch_cards.py | 15 +- 9 files changed, 149 insertions(+), 118 deletions(-) diff --git a/app/database/initialize_data.py b/app/database/initialize_data.py index 7fc6c17..df38274 100644 --- a/app/database/initialize_data.py +++ b/app/database/initialize_data.py @@ -12,6 +12,6 @@ def populate_card_table(): with open(settings.CARDS_CSV_FILE_PTAH, newline='') as csvfile: csvreader = csv.reader(csvfile, delimiter=';') for row in csvreader: - number, card_type, name, description = row - Card(number=number, type=card_type, + number, card_type, card_subtype, name, description = row + Card(number=number, type=card_type, subtype=card_subtype, name=name, description=description) diff --git a/app/database/models.py b/app/database/models.py index 6658036..f637eb0 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -31,6 +31,7 @@ class Card(db.Entity): id = PrimaryKey(int, auto=True) number = Required(int) type = Required(str) + subtype = Required(str) name = Required(str) description = Required(str) games_discard_deck = Set(Game, reverse='discard_deck') diff --git a/app/resources/cartas.csv b/app/resources/cartas.csv index 250ef52..a93a570 100644 --- a/app/resources/cartas.csv +++ b/app/resources/cartas.csv @@ -1,108 +1,108 @@ -4;THE_THING;La Cosa;Sos La Cosa. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -6;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -6;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -7;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -7;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -8;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -9;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -9;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -10;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -10;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -11;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -11;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -11;GET_AWAY;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. -4;GET_AWAY;Lanzallamas;Elimina de la partida a un jugador adyacente. -4;GET_AWAY;Lanzallamas;Elimina de la partida a un jugador adyacente. -6;GET_AWAY;Lanzallamas;Elimina de la partida a un jugador adyacente. -9;GET_AWAY;Lanzallamas;Elimina de la partida a un jugador adyacente. -11;GET_AWAY;Lanzallamas;Elimina de la partida a un jugador adyacente. -5;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -6;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -9;GET_AWAY;Análisis;Mira la mano de cartas de un jugador adyacente. -4;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. -9;GET_AWAY;Hacha;Retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. -4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -4;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -7;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -8;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -9;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -10;GET_AWAY;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. -4;GET_AWAY;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. -6;GET_AWAY;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. -10;GET_AWAY;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. -4;GET_AWAY;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. -4;GET_AWAY;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. -6;GET_AWAY;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. -9;GET_AWAY;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. -10;GET_AWAY;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. -4;GET_AWAY;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. -4;GET_AWAY;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. -7;GET_AWAY;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. -9;GET_AWAY;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. -11;GET_AWAY;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. -4;GET_AWAY;Vigila tus espaldas;Invierte el orden de juego. Ahora, tanto el orden de turnos como los intercambios de cartas van en el sentido contrario. -9;GET_AWAY;Vigila tus espaldas;Invierte el orden de juego. Ahora, tanto el orden de turnos como los intercambios de cartas van en el sentido contrario. -4;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -4;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -6;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -7;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -8;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -10;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -11;GET_AWAY;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. -4;GET_AWAY;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. -7;GET_AWAY;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. -11;GET_AWAY;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. -4;GET_AWAY;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. -4;GET_AWAY;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. -7;GET_AWAY;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. -9;GET_AWAY;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. -11;GET_AWAY;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. -4;GET_AWAY;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -6;GET_AWAY;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -11;GET_AWAY;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -5;GET_AWAY;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. -6;GET_AWAY;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. -8;GET_AWAY;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. -11;GET_AWAY;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. -4;GET_AWAY;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -6;GET_AWAY;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -8;GET_AWAY;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -11;GET_AWAY;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -4;GET_AWAY;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -6;GET_AWAY;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -11;GET_AWAY;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -4;GET_AWAY;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -6;GET_AWAY;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -11;GET_AWAY;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. -5;GET_AWAY;Cuarentena;Durante 2 rondas, un jugador adyacente debe robar, descartar e intercambiar cartas boca arriba. No puede eliminar jugadores ni cambiar de sitio. -9;GET_AWAY;Cuarentena;Durante 2 rondas, un jugador adyacente debe robar, descartar e intercambiar cartas boca arriba. No puede eliminar jugadores ni cambiar de sitio. -8;PANIC;Revelaciones;Empezando por ti y siguiedo el orden del juego, cada jugador elige si revela o no su mano. La ronda de “Revelaciones” termina cuando un jugador muestre una carta “!Infectado!”, sin que tenga que revelar el resto de su mano. -6;PANIC;Cuerdas podridas;¡Las viejas cuerdas que usaste son fáciles de romper! Todas las cartas “Cuarentena” que haya en juego son descartadas. -9;PANIC;Cuerdas podridas;¡Las viejas cuerdas que usaste son fáciles de romper! Todas las cartas “Cuarentena” que haya en juego son descartadas. -5;PANIC;¡Sal de aquí!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena. -4;PANIC;Olvidadizo;Descarta 3 cartas de tu mano y roba 3 nuevas cartas “¡Aléjate!”, descartando cualquier carta de “¡Pánico!” robada. -5;PANIC;Uno, dos…;… con La Cosa ve diciendo adiós. Cámbiate de sitio con el tercer jugador que tengas a tu izquierda o a tu derecha (a tu elección=, ignorando cualquier carta “Puerta atrancada” que haya en juego. Si tú o ese jugador está en Cuarentena, el cambio no tiene lugar. -9;PANIC;Uno, dos…;… con La Cosa ve diciendo adiós. Cámbiate de sitio con el tercer jugador que tengas a tu izquierda o a tu derecha (a tu elección=, ignorando cualquier carta “Puerta atrancada” que haya en juego. Si tú o ese jugador está en Cuarentena, el cambio no tiene lugar. -4;PANIC;Tres, cuatro…;… se cuela sin trabajo. Todas las cartas “Puerta atrancada” que haya en juego son descartadas. -9;PANIC;Tres, cuatro…;… se cuela sin trabajo. Todas las cartas “Puerta atrancada” que haya en juego son descartadas. -5;PANIC;¿Es aquí la fiesta?;Descarta todas las cartas “Cuarentena” y “Puerta atrancada” que haya en juego. A continuación, empezando por ti, todos los jugadores cambian de sitio por parejas, en el sentido de las agujas del reloj. Si hay un número impar de jugadores, el último jugador no se mueve. -9;PANIC;¿Es aquí la fiesta?;Descarta todas las cartas “Cuarentena” y “Puerta atrancada” que haya en juego. A continuación, empezando por ti, todos los jugadores cambian de sitio por parejas, en el sentido de las agujas del reloj. Si hay un número impar de jugadores, el último jugador no se mueve. -7;PANIC;Que quede entre nosotros…;Muéstrate todas las cartas de tu mano a un jugador adyacente de tu elección. -9;PANIC;Que quede entre nosotros…;Muéstrate todas las cartas de tu mano a un jugador adyacente de tu elección. -4;PANIC;Vuelta y vuelta;Todos los jugadores deben darle 1 carta al siguiente jugador que tengan al lado, simultáneamente y en el sentido de juego actual, ignorando cualquier carta “Puerta atrancada” y “Cuarentena” que haya en juego. No puedes usar ninguna carta para evitar este intercambio. La Cosa puede infectar a otro jugador de esta forma, pasándole una carta “¡Infectado!”. Tu turno termina. -9;PANIC;Vuelta y vuelta;Todos los jugadores deben darle 1 carta al siguiente jugador que tengan al lado, simultáneamente y en el sentido de juego actual, ignorando cualquier carta “Puerta atrancada” y “Cuarentena” que haya en juego. No puedes usar ninguna carta para evitar este intercambio. La Cosa puede infectar a otro jugador de esta forma, pasándole una carta “¡Infectado!”. Tu turno termina. -7;PANIC;¿No podemos ser amigos?;Intercambia 1 carta con cualquier jugador de tu elección que no esté en Cuarentena. -9;PANIC;¿No podemos ser amigos?;Intercambia 1 carta con cualquier jugador de tu elección que no esté en Cuarentena. -4;PANIC;Cita a ciegas;Intercambia una carta de tu mano con la primera carta del mazo, descartando cualquier carta de “¡Pánico!” robada. Tu turno termina. -9;PANIC;Cita a ciegas;Intercambia una carta de tu mano con la primera carta del mazo, descartando cualquier carta de “¡Pánico!” robada. Tu turno termina. -10;PANIC;¡Ups!;Muéstrales todas las cartas de tu mano a todos los jugadores. +4;THE_THING;CONTAGION;La Cosa;Sos La Cosa. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +6;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +6;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +7;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +7;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +8;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +9;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +9;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +10;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +10;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +11;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +11;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +11;STAY_AWAY;CONTAGION;¡Infectado!;Si recibes esta carta de otro jugador, quedas Infectado y debes quedarte esta carta hasta el final de la partida. +4;STAY_AWAY;ACTION;Lanzallamas;Elimina de la partida a un jugador adyacente. +4;STAY_AWAY;ACTION;Lanzallamas;Elimina de la partida a un jugador adyacente. +6;STAY_AWAY;ACTION;Lanzallamas;Elimina de la partida a un jugador adyacente. +9;STAY_AWAY;ACTION;Lanzallamas;Elimina de la partida a un jugador adyacente. +11;STAY_AWAY;ACTION;Lanzallamas;Elimina de la partida a un jugador adyacente. +5;STAY_AWAY;ACTION;Análisis;Mira la mano de cartas de un jugador adyacente. +6;STAY_AWAY;ACTION;Análisis;Mira la mano de cartas de un jugador adyacente. +9;STAY_AWAY;ACTION;Análisis;Mira la mano de cartas de un jugador adyacente. +4;STAY_AWAY;ACTION;Hacha;retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. +9;STAY_AWAY;ACTION;Hacha;retira una carta “Puerta atrancada” o “Cuarentena” de ti mismo o de un jugador adyacente. +4;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +4;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +4;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +4;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +7;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +8;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +9;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +10;STAY_AWAY;ACTION;Sospecha;Mira 1 carta aleatoria de la mano de un jugador adyacente. +4;STAY_AWAY;ACTION;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. +6;STAY_AWAY;ACTION;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. +10;STAY_AWAY;ACTION;Whisky;Muestra todas tus cartas a todos los jugadores. Sólo puedes jugar esta carta sobre ti mismo. +4;STAY_AWAY;ACTION;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. +4;STAY_AWAY;ACTION;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. +6;STAY_AWAY;ACTION;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. +9;STAY_AWAY;ACTION;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. +10;STAY_AWAY;ACTION;Determinación;Roba 3 cartas “¡Aléjate!”, elige 1 para quedártela y descarta las demás. A continuación, juega o descarta 1 carta. +4;STAY_AWAY;ACTION;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. +4;STAY_AWAY;ACTION;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. +7;STAY_AWAY;ACTION;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. +9;STAY_AWAY;ACTION;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. +11;STAY_AWAY;ACTION;¡Cambio de lugar!;Cámbiate de sitio con un jugador adyacente que no esté en Cuarentena o tras una Puerta atrancada. +4;STAY_AWAY;ACTION;Vigila tus espaldas;Invierte el orden de juego. Ahora, tanto el orden de turnos como los intercambios de cartas van en el sentido contrario. +9;STAY_AWAY;ACTION;Vigila tus espaldas;Invierte el orden de juego. Ahora, tanto el orden de turnos como los intercambios de cartas van en el sentido contrario. +4;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +4;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +6;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +7;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +8;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +10;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +11;STAY_AWAY;ACTION;Seducción;Intercambia una carta con cualquier jugador de tu elección que no esté en Cuarentena. Tu turno termina. +4;STAY_AWAY;OBSTACLE;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. +7;STAY_AWAY;OBSTACLE;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. +11;STAY_AWAY;OBSTACLE;Puerta atrancada;Coloca esta carta entre un jugador adyacente y tú. No se permiten acciones entre este jugador y tú. +4;STAY_AWAY;ACTION;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. +4;STAY_AWAY;ACTION;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. +7;STAY_AWAY;ACTION;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. +9;STAY_AWAY;ACTION;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. +11;STAY_AWAY;ACTION;¡Más vale que corras!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena, ignorando cualquier puerta atrancada. +4;STAY_AWAY;DEFENSE;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +6;STAY_AWAY;DEFENSE;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +11;STAY_AWAY;DEFENSE;Aquí estoy bien;Cancela una carta “¡Cambio de lugar!” o “¡Más vale que corras!” de la que seas objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +5;STAY_AWAY;DEFENSE;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. +6;STAY_AWAY;DEFENSE;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. +8;STAY_AWAY;DEFENSE;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. +11;STAY_AWAY;DEFENSE;Aterrador;Niégate a un ofrecimiento de intercambio de cartas y mira la carta que te has negado a recibir. Roba 1 carta “Aléjate!” en sustitución de ésta. +4;STAY_AWAY;DEFENSE;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +6;STAY_AWAY;DEFENSE;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +8;STAY_AWAY;DEFENSE;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +11;STAY_AWAY;DEFENSE;¡No, gracias!;Niégate a un ofrecimiento de intercambio de cartas. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +4;STAY_AWAY;DEFENSE;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +6;STAY_AWAY;DEFENSE;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +11;STAY_AWAY;DEFENSE;¡Fallaste!;El siguiente jugador después de ti realiza el intercambio de cartas en lugar de hacerlo tú. No queda Infectado si recibe una carta “¡Infectado!”. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +4;STAY_AWAY;DEFENSE;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +6;STAY_AWAY;DEFENSE;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +11;STAY_AWAY;DEFENSE;¡Nada de barbacoas!;Cancela una carta “Lanzallamas” que te tenga como objetivo. Roba 1 carta “¡Aléjate!” en sustitución de ésta. +5;STAY_AWAY;OBSTACLE;Cuarentena;Durante 2 rondas, un jugador adyacente debe robar, descartar e intercambiar cartas boca arriba. No puede eliminar jugadores ni cambiar de sitio. +9;STAY_AWAY;OBSTACLE;Cuarentena;Durante 2 rondas, un jugador adyacente debe robar, descartar e intercambiar cartas boca arriba. No puede eliminar jugadores ni cambiar de sitio. +8;PANIC;PANIC;Revelaciones;Empezando por ti y siguiedo el orden del juego, cada jugador elige si revela o no su mano. La ronda de “Revelaciones” termina cuando un jugador muestre una carta “!Infectado!”, sin que tenga que revelar el resto de su mano. +6;PANIC;PANIC;Cuerdas podridas;¡Las viejas cuerdas que usaste son fáciles de romper! Todas las cartas “Cuarentena” que haya en juego son descartadas. +9;PANIC;PANIC;Cuerdas podridas;¡Las viejas cuerdas que usaste son fáciles de romper! Todas las cartas “Cuarentena” que haya en juego son descartadas. +5;PANIC;PANIC;¡Sal de aquí!;Cámbiate de sitio con cualquier jugador de tu elección que no esté en Cuarentena. +4;PANIC;PANIC;Olvidadizo;Descarta 3 cartas de tu mano y roba 3 nuevas cartas “¡Aléjate!”, descartando cualquier carta de “¡Pánico!” robada. +5;PANIC;PANIC;Uno, dos…;… con La Cosa ve diciendo adiós. Cámbiate de sitio con el tercer jugador que tengas a tu izquierda o a tu derecha (a tu elección=, ignorando cualquier carta “Puerta atrancada” que haya en juego. Si tú o ese jugador está en Cuarentena, el cambio no tiene lugar. +9;PANIC;PANIC;Uno, dos…;… con La Cosa ve diciendo adiós. Cámbiate de sitio con el tercer jugador que tengas a tu izquierda o a tu derecha (a tu elección=, ignorando cualquier carta “Puerta atrancada” que haya en juego. Si tú o ese jugador está en Cuarentena, el cambio no tiene lugar. +4;PANIC;PANIC;Tres, cuatro…;… se cuela sin trabajo. Todas las cartas “Puerta atrancada” que haya en juego son descartadas. +9;PANIC;PANIC;Tres, cuatro…;… se cuela sin trabajo. Todas las cartas “Puerta atrancada” que haya en juego son descartadas. +5;PANIC;PANIC;¿Es aquí la fiesta?;Descarta todas las cartas “Cuarentena” y “Puerta atrancada” que haya en juego. A continuación, empezando por ti, todos los jugadores cambian de sitio por parejas, en el sentido de las agujas del reloj. Si hay un número impar de jugadores, el último jugador no se mueve. +9;PANIC;PANIC;¿Es aquí la fiesta?;Descarta todas las cartas “Cuarentena” y “Puerta atrancada” que haya en juego. A continuación, empezando por ti, todos los jugadores cambian de sitio por parejas, en el sentido de las agujas del reloj. Si hay un número impar de jugadores, el último jugador no se mueve. +7;PANIC;PANIC;Que quede entre nosotros…;Muéstrate todas las cartas de tu mano a un jugador adyacente de tu elección. +9;PANIC;PANIC;Que quede entre nosotros…;Muéstrate todas las cartas de tu mano a un jugador adyacente de tu elección. +4;PANIC;PANIC;Vuelta y vuelta;Todos los jugadores deben darle 1 carta al siguiente jugador que tengan al lado, simultáneamente y en el sentido de juego actual, ignorando cualquier carta “Puerta atrancada” y “Cuarentena” que haya en juego. No puedes usar ninguna carta para evitar este intercambio. La Cosa puede infectar a otro jugador de esta forma, pasándole una carta “¡Infectado!”. Tu turno termina. +9;PANIC;PANIC;Vuelta y vuelta;Todos los jugadores deben darle 1 carta al siguiente jugador que tengan al lado, simultáneamente y en el sentido de juego actual, ignorando cualquier carta “Puerta atrancada” y “Cuarentena” que haya en juego. No puedes usar ninguna carta para evitar este intercambio. La Cosa puede infectar a otro jugador de esta forma, pasándole una carta “¡Infectado!”. Tu turno termina. +7;PANIC;PANIC;¿No podemos ser amigos?;Intercambia 1 carta con cualquier jugador de tu elección que no esté en Cuarentena. +9;PANIC;PANIC;¿No podemos ser amigos?;Intercambia 1 carta con cualquier jugador de tu elección que no esté en Cuarentena. +4;PANIC;PANIC;Cita a ciegas;Intercambia una carta de tu mano con la primera carta del mazo, descartando cualquier carta de “¡Pánico!” robada. Tu turno termina. +9;PANIC;PANIC;Cita a ciegas;Intercambia una carta de tu mano con la primera carta del mazo, descartando cualquier carta de “¡Pánico!” robada. Tu turno termina. +10;PANIC;PANIC;¡Ups!;Muéstrales todas las cartas de tu mano a todos los jugadores. diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 90e3018..ae09806 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -5,9 +5,16 @@ class CardType(str, Enum): PANIC = 'PANIC' - GET_AWAY = 'GET_AWAY' + STAY_AWAY = 'STAY_AWAY' THE_THING = 'THE_THING' +class CardSubtype(str, Enum): + CONTACION = 'CONTAGION' + ACTION = 'ACTION' + DEFENSE = 'DEFENSE' + OBSTACLE = 'OBSTACLE' + PANIC = 'PANIC' + class BaseCard(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -15,6 +22,7 @@ class BaseCard(BaseModel): number: int = Field( ge=4, le=12) type: CardType + subtype: CardSubtype name: str = Field( min_length=3, max_length=50) description: str = Field( @@ -37,6 +45,9 @@ class CardUpdateIn(BaseModel): type: Optional[CardType] = Field( None, description="Optional card type." ) + subtype: Optional[CardSubtype] = Field( + None, description="Optional card subtype." + ) name: Optional[str] = Field( None, min_length=3, max_length=50, description="Optional name of the card." ) @@ -51,5 +62,5 @@ class CardUpdateOut(BaseCard): class CardResponse(BaseCard): id: int = Field( - ge=1, le=110 + ge=1, le=108 ) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index d4f5064..3b7aa09 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -14,6 +14,7 @@ def get_cards() -> list[CardResponse]: id=card.id, number=card.number, type=card.type, + subtype=card.subtype, name=card.name, description=card.description ) for card in cards] @@ -25,6 +26,7 @@ def create_card(card_data: CardCreationIn) -> Card: new_card = Card( number=card_data.number, type=card_data.type, + subtype=card_data.subtype, name=card_data.name, description=card_data.description ) @@ -60,11 +62,13 @@ def update_card(card_id: int, request_data: CardUpdateIn) -> CardUpdateOut: card.number = request_data.number card.type = request_data.type + card.subtype = request_data.subtype card.name = request_data.name card.description = request_data.description return CardUpdateOut( number=card.number, type=card.type, + subtype=card.subtype, name=card.name, description=card.description ) diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index d75f68c..7721875 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -20,6 +20,7 @@ def test_create_cards_succesfully(): card_data = { "number": i, "type": 'THE_THING', + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" } @@ -35,6 +36,7 @@ def test_create_cards_succesfully(): assert response.json() == { "number": i, "type": "THE_THING", + "subtype": "CONTAGION", "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, "El contenido de la respuesta no coincide con los datos de la carta creada." diff --git a/app/tests/card_tests/test_delete_cards.py b/app/tests/card_tests/test_delete_cards.py index e614b00..f69c0be 100644 --- a/app/tests/card_tests/test_delete_cards.py +++ b/app/tests/card_tests/test_delete_cards.py @@ -10,7 +10,7 @@ @db_session def create_card_for_testing(id: int) -> Card: - return Card(id=id, number=4, type="THE_THING", name="The Thing", + return Card(id=id, number=4, type="THE_THING", subtype="CONTAGION", name="The Thing", description="You are the thing, infect or kill everyone") @@ -21,10 +21,11 @@ def cleanup_database() -> None: # Fake classes class FakeCard: - def __init__(self, id, number, type, name, description): + def __init__(self, id, number, type, subtype, name, description): self.id = id self.number = number self.type = type + self.subtype = subtype self.name = name self.description = description @@ -32,7 +33,7 @@ def delete(): pass -card = FakeCard(id=1, number=4, type="THE_THING", name="The Thing", +card = FakeCard(id=1, number=4, type="THE_THING", subtype="CONTAGION", name="The Thing", description="You are the thing, infect or kill everyone") diff --git a/app/tests/card_tests/test_get_cards.py b/app/tests/card_tests/test_get_cards.py index 1a70e11..bf90616 100644 --- a/app/tests/card_tests/test_get_cards.py +++ b/app/tests/card_tests/test_get_cards.py @@ -10,7 +10,7 @@ @db_session def create_card_for_testing(id: int) -> Card: - return Card(id=id, number=4, type="THE_THING", name="The Thing", + return Card(id=id, number=4, type="THE_THING", subtype="CONTAGION", name="The Thing", description="You are the thing, infect or kill everyone") @@ -37,6 +37,7 @@ def test_get_one_card(mocker): "id": 1, "number": 4, "type": "THE_THING", + "subtype": "CONTAGION", "name": "The Thing", "description": "You are the thing, infect or kill everyone" } diff --git a/app/tests/card_tests/test_patch_cards.py b/app/tests/card_tests/test_patch_cards.py index cadd35c..4804a63 100644 --- a/app/tests/card_tests/test_patch_cards.py +++ b/app/tests/card_tests/test_patch_cards.py @@ -10,7 +10,7 @@ @db_session def create_card_for_testing(id: int) -> Card: - return Card(id=id, number=4, type="THE_THING", name="The Thing", + return Card(id=id, number=4, type="THE_THING", subtype="CONTAGION", name="The Thing", description="You are the thing, infect or kill everyone") @@ -27,6 +27,7 @@ def perform_a_valid_update_test(card_id, success_request_data): assert response.json() == { "number": 10, "type": "PANIC", + "subtype": "CONTAGION", "name": "PANIC", "description": "PANIC", }, "El contenido de la respuesta no coincide con los datos actualizados." @@ -44,6 +45,7 @@ def test_patch_card_succesfully(): success_request_data = { "number": 10, "type": "PANIC", + "subtype": "CONTAGION", "name": "PANIC", "description": "PANIC" } @@ -59,6 +61,7 @@ def test_patch_card_greater_number(): greater_number_request_data = { "number": 13, "type": "PANIC", + "subtype": "PANIC", "name": "PANIC", "description": "PANIC" } @@ -72,6 +75,7 @@ def test_patch_card_lower_number(): lower_number_request_data = { "number": 2, "type": "PANIC", + "subtype": "PANIC", "name": "PANIC", "description": "PANIC" } @@ -85,6 +89,7 @@ def test_patch_card_invalid_number(): invalid_number_request_data = { "number": "error", "type": "PANIC", + "subtype": "PANIC", "name": "PANIC", "description": "PANIC" } @@ -101,7 +106,8 @@ def test_patch_card_invalid_type(): invalid_type_request_data = { "number": 4, "type": "error", - "name": "PANIC", + "subtype": "CONTAGION", + "name": "La Cosa", "description": "PANIC" } perform_an_invalid_update_test( @@ -118,6 +124,7 @@ def test_patch_card_invalid_name(): invalid_name_request_data = { "number": 4, "type": "PANIC", + "subtype": "CONTAGION", "name": 123, "description": "PANIC" } @@ -135,6 +142,7 @@ def test_patch_card_short_name(): short_name_request_data = { "number": 4, "type": "PANIC", + "subtype": "CONTAGION", "name": "P", "description": "PANIC" } @@ -168,6 +176,7 @@ def test_patch_card_invalid_description(): invalid_description_request_data = { "number": 4, "type": "PANIC", + "subtype": "CONTAGION", "name": "PANIC", "description": 123 } @@ -185,6 +194,7 @@ def test_patch_card_short_description(): short_description_request_data = { "number": 4, "type": "PANIC", + "subtype": "CONTAGION", "name": "PANIC", "description": "P" } @@ -202,6 +212,7 @@ def test_patch_card_long_description(): long_description_request_data = { "number": 4, "type": "PANIC", + "subtype": "CONTAGION", "name": "PANIC", "description": "PANIC"*1000 } From 38508d86c453f2e3531471a320f41893cd09bce7 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 18:03:33 -0300 Subject: [PATCH 043/224] Primer intento de implementacion para jugar cartas de accion, solo lanzallamas por ahora --- app/routers/cards/schemas.py | 14 ++++++++ app/routers/cards/utils.py | 23 ++++++++++++ app/routers/games/games.py | 6 ++++ app/routers/games/schemas.py | 8 +++++ app/routers/games/services.py | 67 +++++++++++++++++++++++++++++++++++ app/routers/games/utils.py | 41 ++++++++++++++++++++- app/routers/players/utils.py | 25 +++++++++++++ 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 app/routers/cards/utils.py create mode 100644 app/routers/players/utils.py diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index ae09806..ea0f275 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -8,6 +8,7 @@ class CardType(str, Enum): STAY_AWAY = 'STAY_AWAY' THE_THING = 'THE_THING' + class CardSubtype(str, Enum): CONTACION = 'CONTAGION' ACTION = 'ACTION' @@ -16,6 +17,19 @@ class CardSubtype(str, Enum): PANIC = 'PANIC' +class CardActionName(str, Enum): + FLAMETHROWER = 'Lanzallamas' + ANALYSIS = 'Análisis' + AXE = 'Hacha' + SUSPICIOUS = 'Sospecha' + WHISKEY = 'Whisky' + RESOLUTE = 'Determinación' + WATCH_YOUR_BACK = 'Vigila tus espaldas' + CHANGE_PLACES = '¡Cambio de lugar!' + BETTER_RUN = '¡Más vale que corras!' + SEDUCTION = 'Seducción' + + class BaseCard(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py new file mode 100644 index 0000000..0a5037b --- /dev/null +++ b/app/routers/cards/utils.py @@ -0,0 +1,23 @@ +from pony.orm import db_session +from fastapi import HTTPException, status +from app.database.models import Card +from ..cards.schemas import CardSubtype + + +@db_session +def find_card_by_id(card_id: int): + card = Card.get(id=card_id) + if not card: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Card not found" + ) + return card + + +def verify_action_card(card: Card): + if card.subtype != CardSubtype.ACTION: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Card is not an ACTION card" + ) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 9156081..802984b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -136,3 +136,9 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): utils.verify_discard_can_be_done(game_name, game_data) services.discard_card(game_name, game_data) return {"message": "Card discarded"} + + +@router.post("/{game_name}/play-action-card", status_code=status.HTTP_200_OK) +async def play_action_card(game_name: str, play_info: PlayInformation): + services.play_action_card(game_name, play_info) + return {"message": "Action card played"} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 6e1c3f5..67b62d2 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -102,3 +102,11 @@ class DiscardInformationIn(BaseModel): player_id: int card_id: int + + +class PlayInformation(BaseModel): + model_config = ConfigDict(from_attributes=True) + + card_id: int + player_id: int + objective_player_id: int diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 0537c55..cb43528 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -3,7 +3,12 @@ from .schemas import * from fastapi import HTTPException, status from .utils import find_game_by_name, list_of_unstarted_games +from .utils import verify_player_in_game, verify_adjacent_players from ..cards import services as cards_services +from ..cards.utils import find_card_by_id, verify_action_card +from ..players.utils import find_player_by_id, verify_card_in_hand +from ..cards.schemas import CardActionName +from ..players.schemas import PlayerRol def get_unstarted_games() -> List[GameResponse]: @@ -207,3 +212,65 @@ def discard_card(game_name: str, game_data: DiscardInformationIn): player.hand.remove(card) if game and card: game.discard_deck.add(card) + + +@db_session +def play_action_card(game_name: str, play_info: PlayInformation): + verify_player_in_game(play_info.player_id, game_name) + verify_player_in_game(play_info.objective_player_id, game_name) + + game = find_game_by_name(game_name) + + player = find_player_by_id(play_info.player_id) + objective_player = find_player_by_id(play_info.objective_player_id) + + card = find_card_by_id(play_info.card_id) + verify_action_card(card) + + verify_card_in_hand(player, card) + + # Lanzallamas + if card.name == CardActionName.FLAMETHROWER: + verify_adjacent_players(play_info.player_id, + play_info.objective_player_id, + len(game.players)-1) + objective_player.rol = PlayerRol.ELIMINATED + + # Analisis + if card.name == CardActionName.ANALYSIS: + pass + + # Hacha + if card.name == CardActionName.AXE: + pass + + # Sospecha + if card.name == CardActionName.SUSPICIOUS: + pass + + # Whisky + if card.name == CardActionName.WHISKEY: + pass + + # Determinacion + if card.name == CardActionName.RESOLUTE: + pass + + # Vigila tus espaldas + if card.name == CardActionName.WATCH_YOUR_BACK: + pass + + # Cambio de lugar + if card.name == CardActionName.CHANGE_PLACES: + pass + + # Mas vale que corras + if card.name == CardActionName.BETTER_RUN: + pass + + # Seduccion + if card.name == CardActionName.SEDUCTION: + pass + + game.discard_deck.add(card) + player.hand.remove(card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 6e8c199..ac454d7 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -5,6 +5,7 @@ from .schemas import * from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol +from ..players.utils import find_player_by_id class Events(str, Enum): @@ -24,7 +25,8 @@ def find_game_by_name(game_name: str): if not game: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Game not found" + status_code=status.HTTP_404_NOT_FOUND, + detail="Game not found" ) return game @@ -199,3 +201,40 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="The player is infected and cannot discard the card" ) + + +@db_session +def verify_player_in_game(player_id: int, game_name: str): + player = Player.get(id=player_id) + game = Game.get(name=game_name) + if player and game: + if player in game.players: + pass + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player is not in the game" + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player or game not found" + ) + + +@db_session +def verify_adjacent_players(player_id: int, other_player_id: int, max_position: int): + player = find_player_by_id(player_id) + other_player = find_player_by_id(other_player_id) + + are_adjacent = ( + abs(player.position - other_player.position) == 1 or + (player.position == 0 and other_player.position == max_position) or + (other_player.position == 0 and player.position == max_position) + ) + + if not are_adjacent: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Players are not adjacent" + ) diff --git a/app/routers/players/utils.py b/app/routers/players/utils.py new file mode 100644 index 0000000..f3af310 --- /dev/null +++ b/app/routers/players/utils.py @@ -0,0 +1,25 @@ +from pony.orm import db_session +from fastapi import HTTPException, status +from app.database.models import Player, Card + + +@db_session +def find_player_by_id(player_id: int): + player = Player.get(id=player_id) + if not player: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player not found" + ) + return player + + +@db_session +def verify_card_in_hand(player: Player, card: Card): + if card in player.hand: + pass + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Card not in player hand" + ) From b6f831759321e20a55ad09058d6620fe3900df42 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 18:40:50 -0300 Subject: [PATCH 044/224] Implementado el envio por websocket del evento played_card cuando un jugador juego una carta de accion --- app/routers/cards/utils.py | 6 ++++++ app/routers/games/games.py | 8 ++++++++ app/routers/games/utils.py | 1 + app/routers/players/utils.py | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 0a5037b..2d678d8 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -21,3 +21,9 @@ def verify_action_card(card: Card): status_code=status.HTTP_400_BAD_REQUEST, detail="Card is not an ACTION card" ) + + +@db_session +def get_card_name_by_id(card_id: int) -> str: + card = find_card_by_id(card_id) + return card.name diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 802984b..3e7c8b1 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -4,6 +4,8 @@ from . import utils from .schemas import * from ..websockets.utils import player_connections +from ..players.utils import get_player_name_by_id +from ..cards.utils import get_card_name_by_id router = APIRouter( @@ -141,4 +143,10 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): @router.post("/{game_name}/play-action-card", status_code=status.HTTP_200_OK) async def play_action_card(game_name: str, play_info: PlayInformation): services.play_action_card(game_name, play_info) + json_msg = { + "event": utils.Events.PLAYED_CARD, + "player_name": get_player_name_by_id(play_info.player_id), + "card_name": get_card_name_by_id(play_info.card_id) + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) return {"message": "Action card played"} diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index ac454d7..f319340 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -17,6 +17,7 @@ class Events(str, Enum): PLAYER_JOINED = 'player_joined' PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' + PLAYED_CARD = 'played_card' @db_session diff --git a/app/routers/players/utils.py b/app/routers/players/utils.py index f3af310..bdca50b 100644 --- a/app/routers/players/utils.py +++ b/app/routers/players/utils.py @@ -23,3 +23,9 @@ def verify_card_in_hand(player: Player, card: Card): status_code=status.HTTP_400_BAD_REQUEST, detail="Card not in player hand" ) + + +@db_session +def get_player_name_by_id(player_id: int) -> str: + player = find_player_by_id(player_id) + return player.name From 90906b8c6c3912ef631a530fb5231c9e814be9d3 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 17 Oct 2023 18:45:53 -0300 Subject: [PATCH 045/224] Cambios en test_create_cards --- app/tests/card_tests/test_create_cards.py | 81 ++++++++++++++++++----- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index 7721875..438f940 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -3,6 +3,7 @@ from app.database.models import Game, Player, Card from app.routers.cards.schemas import * from pony.orm import db_session +from fastapi import status client = TestClient(app) @@ -27,7 +28,7 @@ def test_create_cards_succesfully(): response = client.post("/cards", json=card_data) - assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." + assert response.status_code == status.HTTP_201_CREATED, f"El código de estado de la respuesta no es 201 (Created). El código de estado obtenido es {response.status_code}." with db_session: created_card = Card.get(name=card_data["name"]) @@ -49,7 +50,7 @@ def test_create_card_bad_body(): "/cards", json={"bad": "body"}, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -60,11 +61,12 @@ def test_create_card_missing_number(): "/cards", json={ "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -75,11 +77,12 @@ def test_create_card_bad_number(): json={ "number": "bad", "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -90,11 +93,12 @@ def test_create_card_low_number(): json={ "number": 3, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -105,11 +109,12 @@ def test_create_card_high_number(): json={ "number": 13, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -120,12 +125,13 @@ def test_create_card_missing_type(): "/cards", json={ "number": 4, + "subtype": 'CONTAGION', "name": "The Thing", "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -136,16 +142,15 @@ def test_create_card_bad_type(): json={ "number": 4, "type": "THE_THIN", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() # Test card names - - def test_create_card_missing_name(): cleanup_database() response = client.post( @@ -153,10 +158,11 @@ def test_create_card_missing_name(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -167,11 +173,12 @@ def test_create_card_bad_name(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": 3, "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -182,11 +189,12 @@ def test_create_card_short_name(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "a", "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -197,11 +205,12 @@ def test_create_card_large_name(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "a"*51, "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -213,10 +222,11 @@ def test_create_card_missing_description(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -227,11 +237,12 @@ def test_create_card_bad_description(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": 22 }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -242,11 +253,12 @@ def test_create_card_short_description(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "ee" }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() @@ -257,9 +269,42 @@ def test_create_card_large_description(): json={ "number": 4, "type": "THE_THING", + "subtype": 'CONTAGION', "name": "The Thing", "description": "You are the thing, infect or kill everyone"*1000 }, ) - assert response.status_code == 422, "El código de estado de la respuesta no es 422 (Unprocessable Entity)." + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + cleanup_database() + + +# Test card subtype +def test_create_card_missing_subtype(): + cleanup_database() + response = client.post( + "/cards", + json={ + "number": 4, + "type": "THE_THING", + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + cleanup_database() + + +def test_create_card_bad_subtype(): + cleanup_database() + response = client.post( + "/cards", + json={ + "number": 4, + "type": "THE_THING", + "subtype": 'CONTAGIOOO', + "name": "The Thing", + "description": "You are the thing, infect or kill everyone" + }, + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " cleanup_database() From 4411d8b47cb556fc06bd352484ccc9ddfbdbf6bb Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 17 Oct 2023 20:50:44 -0300 Subject: [PATCH 046/224] endpoint robar carta, se rompe cuando no hay mazo --- app/routers/games/games.py | 22 +++++++ app/routers/games/schemas.py | 15 ++++- app/routers/games/services.py | 24 ++++++++ app/routers/games/utils.py | 72 +++++++++++++++++++++++ app/tests/card_tests/test_create_cards.py | 32 +++++----- 5 files changed, 148 insertions(+), 17 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 9156081..77bb741 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -136,3 +136,25 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): utils.verify_discard_can_be_done(game_name, game_data) services.discard_card(game_name, game_data) return {"message": "Card discarded"} + + + +@router.patch("/{game_name}/draw-card", status_code=status.HTTP_200_OK, response_model=DrawInformationOut) +async def draw_card(game_name: str, game_data: DrawInformationIn): + utils.verify_draw_can_be_done(game_name, game_data) + draw_card_information = services.draw_card(game_name, game_data) + + #mandar por ws que se robo la carta y el dorso de la misma. + json_msg = { + "event": utils.Events.PLAYER_DRAW_CARD, + "game_name": game_name, + "player_id": game_data.player_id, + "next_card": draw_card_information.top_card_face + } + + await player_connections.send_event_to_other_players_in_game(game_name=game_name, + message=json_msg, + excluded_id=game_data.player_id) + + return draw_card_information + \ No newline at end of file diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 6e1c3f5..4433688 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -2,7 +2,7 @@ from typing import List, Optional from enum import Enum from ..players.schemas import PlayerResponse -from ..cards.schemas import CardType +from ..cards.schemas import CardType, CardResponse class GameStatus(str, Enum): @@ -102,3 +102,16 @@ class DiscardInformationIn(BaseModel): player_id: int card_id: int + + +class DrawInformationIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + +class DrawInformationOut(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + card: CardResponse + top_card_face: CardType diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 0537c55..a491229 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -4,6 +4,7 @@ from fastapi import HTTPException, status from .utils import find_game_by_name, list_of_unstarted_games from ..cards import services as cards_services +from app.routers.games import utils def get_unstarted_games() -> List[GameResponse]: @@ -207,3 +208,26 @@ def discard_card(game_name: str, game_data: DiscardInformationIn): player.hand.remove(card) if game and card: game.discard_deck.add(card) + + + + + +@db_session +def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOut: + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) + draw_deck = list(game.draw_deck) + card = draw_deck.pop(0) + + player.hand.add(card) + game.draw_deck.remove(card) + + if len(game.draw_deck) == 0: + utils.re_build_draw_deck(game) + + return DrawInformationOut(player_id=player.id, + card=CardResponse.model_validate(card), + top_card_face=list(game.draw_deck)[0].type + ) + diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 6e8c199..841617a 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -1,3 +1,4 @@ +import random from typing import List from fastapi import WebSocket, HTTPException, status from app.database.models import Game, Player, Card @@ -16,6 +17,7 @@ class Events(str, Enum): PLAYER_JOINED = 'player_joined' PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' + PLAYER_DRAW_CARD = 'player_draw_card' @db_session @@ -199,3 +201,73 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="The player is infected and cannot discard the card" ) + + + +@db_session +def re_build_draw_deck(game: Game) -> list[Card]: + if len(game.draw_deck) != 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The draw deck is not empty.' + ) + game.draw_deck = game.discard_deck + game.discard_deck = [] + + random.shuffle(game.draw_deck) + return game.draw_deck + + +@db_session +def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): + game = Game.get(name=game_name) + player = Player.get(id=game_data.player_id) + #traer el mazo de robo + + if not game: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Game not found" + ) + + if not game.draw_deck: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Draw deck not found" + ) + + #verify game status + + if len(game.draw_deck) == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The draw deck is empty" + ) + + + if not player: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Player not found" + ) + + + is_player_in_game = select( + p for p in game.players if (p.id == player.id)).exists() + + + if not is_player_in_game: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player is not in the game" + ) + + if game.turn != player.position: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="It's not the turn of the player" + ) + + + + diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index 438f940..a714261 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -66,7 +66,7 @@ def test_create_card_missing_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -82,7 +82,7 @@ def test_create_card_bad_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -98,7 +98,7 @@ def test_create_card_low_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -114,7 +114,7 @@ def test_create_card_high_number(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -131,7 +131,7 @@ def test_create_card_missing_type(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -147,7 +147,7 @@ def test_create_card_bad_type(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() # Test card names @@ -162,7 +162,7 @@ def test_create_card_missing_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -178,7 +178,7 @@ def test_create_card_bad_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -194,7 +194,7 @@ def test_create_card_short_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -210,7 +210,7 @@ def test_create_card_large_name(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -226,7 +226,7 @@ def test_create_card_missing_description(): "name": "The Thing" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -242,7 +242,7 @@ def test_create_card_bad_description(): "description": 22 }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -258,7 +258,7 @@ def test_create_card_short_description(): "description": "ee" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -274,7 +274,7 @@ def test_create_card_large_description(): "description": "You are the thing, infect or kill everyone"*1000 }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -290,7 +290,7 @@ def test_create_card_missing_subtype(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() @@ -306,5 +306,5 @@ def test_create_card_bad_subtype(): "description": "You are the thing, infect or kill everyone" }, ) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es { response} " + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, f"El código de estado de la respuesta no es 422(Unprocessable Entity). El código obtenido es {response.status_code}." cleanup_database() From 7dec7d91830516cdd39e83d8db6dae7ce96bb98b Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 21:19:48 -0300 Subject: [PATCH 047/224] Se hace implementacon parcial cartas de: Analisis, Sospecha, Vigila tus espaldas, Cambio de lugar, Mas vale que corras y Seduccion --- app/routers/games/games.py | 4 +- app/routers/games/schemas.py | 4 +- app/routers/games/services.py | 109 ++++++++++++++++++++++++++++------ 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 3e7c8b1..645105e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -142,11 +142,11 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): @router.post("/{game_name}/play-action-card", status_code=status.HTTP_200_OK) async def play_action_card(game_name: str, play_info: PlayInformation): - services.play_action_card(game_name, play_info) + result = services.play_action_card(game_name, play_info) json_msg = { "event": utils.Events.PLAYED_CARD, "player_name": get_player_name_by_id(play_info.player_id), "card_name": get_card_name_by_id(play_info.card_id) } await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) - return {"message": "Action card played"} + return result diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 67b62d2..27ccb81 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -109,4 +109,6 @@ class PlayInformation(BaseModel): card_id: int player_id: int - objective_player_id: int + objective_player_id: Optional[int] = Field( + None, description="Optional objective player." + ) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index cb43528..a93db37 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -9,6 +9,8 @@ from ..players.utils import find_player_by_id, verify_card_in_hand from ..cards.schemas import CardActionName from ..players.schemas import PlayerRol +import json +import random def get_unstarted_games() -> List[GameResponse]: @@ -216,29 +218,53 @@ def discard_card(game_name: str, game_data: DiscardInformationIn): @db_session def play_action_card(game_name: str, play_info: PlayInformation): - verify_player_in_game(play_info.player_id, game_name) - verify_player_in_game(play_info.objective_player_id, game_name) - + result = {"message": "Action card played"} game = find_game_by_name(game_name) - + verify_player_in_game(play_info.player_id, game_name) player = find_player_by_id(play_info.player_id) - objective_player = find_player_by_id(play_info.objective_player_id) - card = find_card_by_id(play_info.card_id) verify_action_card(card) - verify_card_in_hand(player, card) # Lanzallamas if card.name == CardActionName.FLAMETHROWER: + verify_player_in_game(play_info.objective_player_id, game_name) verify_adjacent_players(play_info.player_id, play_info.objective_player_id, len(game.players)-1) + objective_player = find_player_by_id(play_info.objective_player_id) objective_player.rol = PlayerRol.ELIMINATED + game.players.remove(objective_player) + for player in game.players: + if player.position > objective_player.position: + player.position -= 1 + + game.discard_deck.add(card) + player.hand.remove(card) # Analisis if card.name == CardActionName.ANALYSIS: - pass + verify_player_in_game(play_info.objective_player_id, game_name) + verify_adjacent_players(play_info.player_id, + play_info.objective_player_id, + len(game.players)-1) + objective_player = find_player_by_id(play_info.objective_player_id) + + result = [] + for card in objective_player.hand: + card_info = { + 'id': card.id, + 'number': card.number, + 'type': card.type, + 'subtype': card.subtype, + 'name': card.name, + 'description': card.description + } + result.append(card_info) + result = json.dumps(result) + + game.discard_deck.add(card) + player.hand.remove(card) # Hacha if card.name == CardActionName.AXE: @@ -246,7 +272,26 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Sospecha if card.name == CardActionName.SUSPICIOUS: - pass + verify_player_in_game(play_info.objective_player_id, game_name) + verify_adjacent_players(play_info.player_id, + play_info.objective_player_id, + len(game.players)-1) + objective_player = find_player_by_id(play_info.objective_player_id) + + objective_player_hand_list = list(objective_player.hand) + random_card = random.choice(objective_player_hand_list) + card_info = { + 'id': random_card.id, + 'number': random_card.number, + 'type': random_card.type, + 'subtype': random_card.subtype, + 'name': random_card.name, + 'description': random_card.description + } + result = json.dumps(card_info) + + game.discard_deck.add(card) + player.hand.remove(card) # Whisky if card.name == CardActionName.WHISKEY: @@ -258,19 +303,49 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Vigila tus espaldas if card.name == CardActionName.WATCH_YOUR_BACK: - pass + if game.round_direction == RoundDirection.CLOCKWISE: + game.round_direction = RoundDirection.COUNTERCLOCKWISE + else: + game.round_direction = RoundDirection.CLOCKWISE + + game.discard_deck.add(card) + player.hand.remove(card) # Cambio de lugar if card.name == CardActionName.CHANGE_PLACES: - pass + verify_player_in_game(play_info.objective_player_id, game_name) + verify_adjacent_players(play_info.player_id, + play_info.objective_player_id, + len(game.players)-1) + objective_player = find_player_by_id(play_info.objective_player_id) + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition + + game.discard_deck.add(card) + player.hand.remove(card) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: - pass + verify_player_in_game(play_info.objective_player_id, game_name) + objective_player = find_player_by_id(play_info.objective_player_id) + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition - # Seduccion - if card.name == CardActionName.SEDUCTION: - pass + game.discard_deck.add(card) + player.hand.remove(card) - game.discard_deck.add(card) - player.hand.remove(card) + # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) + if card.name == CardActionName.SEDUCTION: + verify_player_in_game(play_info.objective_player_id, game_name) + objective_player = find_player_by_id(play_info.objective_player_id) + objective_player_hand_list = list(objective_player.hand) + eligible_cards = [ + card for card in objective_player_hand_list if card.type != CardType.THE_THING] + random_card = random.choice(eligible_cards) + player.hand.add(random_card) + objective_player.hand.remove(random_card) + objective_player.hand.add(card) + + return result From 602bb689876c9b55307d1fbadaf571555d52567a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 21:28:19 -0300 Subject: [PATCH 048/224] Modificacion menor --- app/routers/games/services.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index a93db37..015a262 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -344,7 +344,9 @@ def play_action_card(game_name: str, play_info: PlayInformation): eligible_cards = [ card for card in objective_player_hand_list if card.type != CardType.THE_THING] random_card = random.choice(eligible_cards) + player.hand.add(random_card) + player.hand.remove(card) objective_player.hand.remove(random_card) objective_player.hand.add(card) From 4deed5b7276b869a9e7168a0a81c2f9c86f15edf Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 21:42:57 -0300 Subject: [PATCH 049/224] Se comenta un poco el codigo para entender que hace el endpoint de jugar carta de accion --- app/routers/games/services.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 015a262..c7eb73f 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -234,6 +234,16 @@ def play_action_card(game_name: str, play_info: PlayInformation): len(game.players)-1) objective_player = find_player_by_id(play_info.objective_player_id) objective_player.rol = PlayerRol.ELIMINATED + + '''Aca tengo que chequear si el jugador eliminado + es La Cosa, si lo es, debo terminar el juego avisando + de los ganadores.''' + + # Las cartas del jugador eliminado van al mazo de descarte + for card in objective_player.hand: + game.discard_deck.add(card) + + # Saco al jugador de la partida y reacomodo posiciones game.players.remove(objective_player) for player in game.players: if player.position > objective_player.position: @@ -250,6 +260,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): len(game.players)-1) objective_player = find_player_by_id(play_info.objective_player_id) + # Armo listado de cartas del jugador objetivo para enviar en el body response result = [] for card in objective_player.hand: card_info = { @@ -278,6 +289,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): len(game.players)-1) objective_player = find_player_by_id(play_info.objective_player_id) + # Elijo una carta al azar del jugador objetivo y armo JSON para pasar por body response objective_player_hand_list = list(objective_player.hand) random_card = random.choice(objective_player_hand_list) card_info = { @@ -303,6 +315,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Vigila tus espaldas if card.name == CardActionName.WATCH_YOUR_BACK: + # Cambio direccion de la ronda if game.round_direction == RoundDirection.CLOCKWISE: game.round_direction = RoundDirection.COUNTERCLOCKWISE else: @@ -318,6 +331,8 @@ def play_action_card(game_name: str, play_info: PlayInformation): play_info.objective_player_id, len(game.players)-1) objective_player = find_player_by_id(play_info.objective_player_id) + + # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition @@ -329,6 +344,8 @@ def play_action_card(game_name: str, play_info: PlayInformation): if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) objective_player = find_player_by_id(play_info.objective_player_id) + + # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition From 3f0b6f56a75bd4320f344ada0b5d11c3c922470a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 21:56:15 -0300 Subject: [PATCH 050/224] Modificacion menor --- app/routers/games/services.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index c7eb73f..e11cc6f 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -235,19 +235,18 @@ def play_action_card(game_name: str, play_info: PlayInformation): objective_player = find_player_by_id(play_info.objective_player_id) objective_player.rol = PlayerRol.ELIMINATED - '''Aca tengo que chequear si el jugador eliminado - es La Cosa, si lo es, debo terminar el juego avisando - de los ganadores.''' - - # Las cartas del jugador eliminado van al mazo de descarte - for card in objective_player.hand: - game.discard_deck.add(card) - - # Saco al jugador de la partida y reacomodo posiciones - game.players.remove(objective_player) - for player in game.players: - if player.position > objective_player.position: - player.position -= 1 + if objective_player.rol == PlayerRol.THE_THING: + game.status = GameStatus.ENDED + else: + # Las cartas del jugador eliminado van al mazo de descarte + for card in objective_player.hand: + game.discard_deck.add(card) + + # Saco al jugador de la partida y reacomodo posiciones + game.players.remove(objective_player) + for player in game.players: + if player.position > objective_player.position: + player.position -= 1 game.discard_deck.add(card) player.hand.remove(card) From dbb1649858cad8c4759c2de97e6e9c7fc9214a73 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 17 Oct 2023 22:22:31 -0300 Subject: [PATCH 051/224] Se mejora implementacion del endpoint --- app/routers/games/services.py | 43 +++++++++++++++-------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index e11cc6f..5882dd5 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -7,9 +7,8 @@ from ..cards import services as cards_services from ..cards.utils import find_card_by_id, verify_action_card from ..players.utils import find_player_by_id, verify_card_in_hand -from ..cards.schemas import CardActionName +from ..cards.schemas import CardActionName, CardResponse from ..players.schemas import PlayerRol -import json import random @@ -260,18 +259,13 @@ def play_action_card(game_name: str, play_info: PlayInformation): objective_player = find_player_by_id(play_info.objective_player_id) # Armo listado de cartas del jugador objetivo para enviar en el body response - result = [] - for card in objective_player.hand: - card_info = { - 'id': card.id, - 'number': card.number, - 'type': card.type, - 'subtype': card.subtype, - 'name': card.name, - 'description': card.description - } - result.append(card_info) - result = json.dumps(result) + result = [CardResponse(id=card.id, + number=card.number, + type=card.type, + subtype=card.subtype, + name=card.name, + description=card.description + ) for card in objective_player.hand] game.discard_deck.add(card) player.hand.remove(card) @@ -288,18 +282,17 @@ def play_action_card(game_name: str, play_info: PlayInformation): len(game.players)-1) objective_player = find_player_by_id(play_info.objective_player_id) - # Elijo una carta al azar del jugador objetivo y armo JSON para pasar por body response + # Elijo una carta al azar del jugador objetivo y armo el body response objective_player_hand_list = list(objective_player.hand) random_card = random.choice(objective_player_hand_list) - card_info = { - 'id': random_card.id, - 'number': random_card.number, - 'type': random_card.type, - 'subtype': random_card.subtype, - 'name': random_card.name, - 'description': random_card.description - } - result = json.dumps(card_info) + result = CardResponse( + id=random_card.id, + number=random_card.number, + type=random_card.type, + subtype=random_card.subtype, + name=random_card.name, + description=random_card.description + ) game.discard_deck.add(card) player.hand.remove(card) @@ -360,7 +353,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): eligible_cards = [ card for card in objective_player_hand_list if card.type != CardType.THE_THING] random_card = random.choice(eligible_cards) - + player.hand.add(random_card) player.hand.remove(card) objective_player.hand.remove(random_card) From 4dfae857cc3d74f316e5c608c7de4709885096da Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 18 Oct 2023 00:03:17 -0300 Subject: [PATCH 052/224] se agregan 2 casos para verificar la finalizacion --- app/routers/cards/services.py | 6 +++--- app/routers/games/services.py | 13 +++++++++---- app/routers/games/utils.py | 26 ++++++++++++++++++-------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index b5e0fc7..59d7b54 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -103,6 +103,6 @@ def deal_cards_to_players(game: Game, deck: List[Card]): @db_session -def card_is_in_player_hand(card_name: str, hand_player: list[Card]) -> bool: - card_names = map(lambda card: card.name, hand_player) - return card_name in card_names +def card_is_in_player_hand(card_name: str, player: Player) -> bool: + card = player.hand.select(lambda c: c.name == card_name).count() + return card > 0 diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 50d0ae1..c06205e 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -156,7 +156,7 @@ def start_game(name: str) -> Game: # setting the position and rol of the players for idx, player in enumerate(game.players): player.position = idx - if cards_services.card_is_in_player_hand('La Cosa', player.hand): + if cards_services.card_is_in_player_hand('La Cosa', player): player.rol = PlayerRol.THE_THING else: player.rol = PlayerRol.HUMAN @@ -174,7 +174,12 @@ def start_game(name: str) -> Game: @db_session def finish_game(name: str) -> Game: game: Game = find_game_by_name(name) - verify_game_can_finish(game) - game.status = GameStatus.ENDED - return game + try: + verify_game_can_be_finished(game) + game.status = GameStatus.ENDED + # enviar por ws los resultados + + return game + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 236da7a..0835ab2 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -51,15 +51,25 @@ def verify_game_can_start(name: str, host_player_id: int): @db_session -def verify_game_can_finish(game: Game): - players_eliminated = game.players.select( - lambda player: player.rol == PlayerRol.ELIMINATED) +def verify_game_can_be_finished(game: Game): + if not the_thing_is_eliminated(game): + raise Exception('The Thing is still alive') - if count(players_eliminated) < count(game.players) - 1: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"There must be exactly one player not eliminated." - ) + if not no_human_remains(game): + raise Exception('There are living Humans') + + +@db_session +def the_thing_is_eliminated(game: Game) -> bool: + the_thing = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING).count() + return the_thing == 0 + + +@db_session +def no_human_remains(game: Game) -> bool: + humans = game.players.select(lambda p: p.rol == PlayerRol.HUMAN).count() + return humans == 0 @db_session From e7057a430b1b81e4e4d755ab8394a9897b05b391 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 18 Oct 2023 15:43:41 -0300 Subject: [PATCH 053/224] Cambios menores --- app/routers/games/services.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 5882dd5..a6ed27a 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -228,24 +228,25 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Lanzallamas if card.name == CardActionName.FLAMETHROWER: verify_player_in_game(play_info.objective_player_id, game_name) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, - len(game.players)-1) + players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) objective_player.rol = PlayerRol.ELIMINATED - if objective_player.rol == PlayerRol.THE_THING: - game.status = GameStatus.ENDED - else: - # Las cartas del jugador eliminado van al mazo de descarte - for card in objective_player.hand: + # Las cartas del jugador eliminado van al mazo de descarte salvo sea carta la Cosa + for card in objective_player.hand: + if card.type != CardType.THE_THING: game.discard_deck.add(card) - # Saco al jugador de la partida y reacomodo posiciones - game.players.remove(objective_player) - for player in game.players: - if player.position > objective_player.position: - player.position -= 1 + # Reacomodo las posiciones + for player in game.players: + if player.position > objective_player.position: + player.position -= 1 + + objective_player.position = -1 game.discard_deck.add(card) player.hand.remove(card) @@ -253,9 +254,11 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Analisis if card.name == CardActionName.ANALYSIS: verify_player_in_game(play_info.objective_player_id, game_name) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, - len(game.players)-1) + players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) # Armo listado de cartas del jugador objetivo para enviar en el body response @@ -277,9 +280,11 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Sospecha if card.name == CardActionName.SUSPICIOUS: verify_player_in_game(play_info.objective_player_id, game_name) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, - len(game.players)-1) + players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) # Elijo una carta al azar del jugador objetivo y armo el body response @@ -319,9 +324,11 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Cambio de lugar if card.name == CardActionName.CHANGE_PLACES: verify_player_in_game(play_info.objective_player_id, game_name) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, - len(game.players)-1) + players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) # Intercambio de posiciones entre los jugadores From e20ed59ad35dc27b3c2b782afb347292785f70b2 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 18 Oct 2023 18:00:05 -0300 Subject: [PATCH 054/224] Se refactoriza codigo para el endpoint de jugar cartas de accion --- app/routers/games/action_functions.py | 138 ++++++++++++++++++++++ app/routers/games/schemas.py | 3 + app/routers/games/services.py | 103 ++++------------ app/routers/games/utils.py | 2 + app/tests/card_tests/test_create_cards.py | 2 + 5 files changed, 165 insertions(+), 83 deletions(-) create mode 100644 app/routers/games/action_functions.py diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py new file mode 100644 index 0000000..ce03df3 --- /dev/null +++ b/app/routers/games/action_functions.py @@ -0,0 +1,138 @@ +from pony.orm import db_session +from app.database.models import Game, Card, Player +from ..players.schemas import PlayerRol +from ..cards.schemas import CardType, CardResponse +from ..websockets.utils import player_connections +from .utils import Events +from .schemas import RoundDirection +import random + + +@db_session +async def process_flamethrower_card(game: Game, player: Player, + card: Card, objective_player: Player): + objective_player.rol = PlayerRol.ELIMINATED + + # Las cartas del jugador eliminado van al mazo de descarte + # salvo sea que sea la carta 'La Cosa' + for card in objective_player.hand: + if card.type != CardType.THE_THING: + game.discard_deck.add(card) + objective_player.hand.remove(card) + + # Reacomodo las posiciones + for player in game.players: + if player.position > objective_player.position: + player.position -= 1 + objective_player.position = -1 + + json_msg = { + "event": Events.PLAYER_ELIMINATED, + "player_id": objective_player.id, + "player_name": objective_player.name + } + for p in game.players: + player_connections.send_event_to(p.id, json_msg) + + game.discard_deck.add(card) + player.hand.remove(card) + + +@db_session +def process_analysis_card(game: Game, player: Player, + card: Card, objective_player: Player): + result = [CardResponse(id=card.id, + number=card.number, + type=card.type, + subtype=card.subtype, + name=card.name, + description=card.description + ) for card in objective_player.hand] + game.discard_deck.add(card) + player.hand.remove(card) + return result + + +@db_session +def process_suspicious_card(game: Game, player: Player, + card: Card, objective_player: Player): + objective_player_hand_list = list(objective_player.hand) + random_card = random.choice(objective_player_hand_list) + result = CardResponse( + id=random_card.id, + number=random_card.number, + type=random_card.type, + subtype=random_card.subtype, + name=random_card.name, + description=random_card.description + ) + game.discard_deck.add(card) + player.hand.remove(card) + return result + + +@db_session +async def process_whiskey_card(game: Game, player: Player, card: Card): + json_msg = { + "event": Events.WHISKEY_CARD_PLAYED, + "player_id": player.id, + "player_name": player.name + } + player_connections.send_event_to_other_players_in_game( + game.name, json_msg, player.id) + + game.discard_deck.add(card) + player.hand.remove(card) + + +@db_session +def process_watch_your_back_card(game: Game, player: Player, card: Card): + if game.round_direction == RoundDirection.CLOCKWISE: + game.round_direction = RoundDirection.COUNTERCLOCKWISE + else: + game.round_direction = RoundDirection.CLOCKWISE + + game.discard_deck.add(card) + player.hand.remove(card) + + +@db_session +def process_change_places_card(game: Game, player: Player, + card: Card, objective_player: Player): + # Intercambio de posiciones entre los jugadores + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition + + game.discard_deck.add(card) + player.hand.remove(card) + + +@db_session +def process_better_run_card(game: Game, player: Player, + card: Card, objective_player: Player): + # Intercambio de posiciones entre los jugadores + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition + + game.discard_deck.add(card) + player.hand.remove(card) + + +@db_session +def process_seduction_card(game: Game, player: Player, + card: Card, objective_player: Player, + card_to_exchange: Card): + objective_player_hand_list = list(objective_player.hand) + eligible_cards = [ + card for card in objective_player_hand_list if card.type != CardType.THE_THING] + random_card = random.choice(eligible_cards) + + player.hand.add(random_card) + player.hand.remove(card_to_exchange) + objective_player.hand.remove(random_card) + objective_player.hand.add(card_to_exchange) + + game.discard_deck.add(card) + player.hand.remove(card) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 27ccb81..52f33a0 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -112,3 +112,6 @@ class PlayInformation(BaseModel): objective_player_id: Optional[int] = Field( None, description="Optional objective player." ) + card_to_exchange: Optional[int] = Field( + None, description="Optional card to exchange." + ) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 5e404f0..5e9594e 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -9,6 +9,7 @@ from ..players.utils import find_player_by_id, verify_card_in_hand from ..cards.schemas import CardActionName, CardResponse from ..players.schemas import PlayerRol +from .action_functions import * import random @@ -244,53 +245,28 @@ def play_action_card(game_name: str, play_info: PlayInformation): verify_action_card(card) verify_card_in_hand(player, card) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() + # Lanzallamas if card.name == CardActionName.FLAMETHROWER: verify_player_in_game(play_info.objective_player_id, game_name) - players_not_eliminated = select( - p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) - objective_player.rol = PlayerRol.ELIMINATED - - # Las cartas del jugador eliminado van al mazo de descarte salvo sea carta la Cosa - for card in objective_player.hand: - if card.type != CardType.THE_THING: - game.discard_deck.add(card) - - # Reacomodo las posiciones - for player in game.players: - if player.position > objective_player.position: - player.position -= 1 - - objective_player.position = -1 - - game.discard_deck.add(card) - player.hand.remove(card) + process_flamethrower_card(game, player, objective_player) # Analisis if card.name == CardActionName.ANALYSIS: verify_player_in_game(play_info.objective_player_id, game_name) - players_not_eliminated = select( - p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) # Armo listado de cartas del jugador objetivo para enviar en el body response - result = [CardResponse(id=card.id, - number=card.number, - type=card.type, - subtype=card.subtype, - name=card.name, - description=card.description - ) for card in objective_player.hand] - - game.discard_deck.add(card) - player.hand.remove(card) + result = process_analysis_card(game, player, card, objective_player) # Hacha if card.name == CardActionName.AXE: @@ -299,31 +275,15 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Sospecha if card.name == CardActionName.SUSPICIOUS: verify_player_in_game(play_info.objective_player_id, game_name) - players_not_eliminated = select( - p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) - - # Elijo una carta al azar del jugador objetivo y armo el body response - objective_player_hand_list = list(objective_player.hand) - random_card = random.choice(objective_player_hand_list) - result = CardResponse( - id=random_card.id, - number=random_card.number, - type=random_card.type, - subtype=random_card.subtype, - name=random_card.name, - description=random_card.description - ) - - game.discard_deck.add(card) - player.hand.remove(card) + result = process_suspicious_card(game, player, card, objective_player) # Whisky if card.name == CardActionName.WHISKEY: - pass + process_whiskey_card(game, player, card) # Determinacion if card.name == CardActionName.RESOLUTE: @@ -331,58 +291,35 @@ def play_action_card(game_name: str, play_info: PlayInformation): # Vigila tus espaldas if card.name == CardActionName.WATCH_YOUR_BACK: - # Cambio direccion de la ronda - if game.round_direction == RoundDirection.CLOCKWISE: - game.round_direction = RoundDirection.COUNTERCLOCKWISE - else: - game.round_direction = RoundDirection.CLOCKWISE - - game.discard_deck.add(card) - player.hand.remove(card) + process_watch_your_back_card(game, player, card) # Cambio de lugar if card.name == CardActionName.CHANGE_PLACES: verify_player_in_game(play_info.objective_player_id, game_name) - players_not_eliminated = select( - p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) - - # Intercambio de posiciones entre los jugadores - tempPosition = player.position - player.position = objective_player.position - objective_player.position = tempPosition - - game.discard_deck.add(card) - player.hand.remove(card) + process_change_places_card(game, player, card, objective_player) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) objective_player = find_player_by_id(play_info.objective_player_id) - - # Intercambio de posiciones entre los jugadores - tempPosition = player.position - player.position = objective_player.position - objective_player.position = tempPosition - - game.discard_deck.add(card) - player.hand.remove(card) + process_better_run_card(game, player, card, objective_player) # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) if card.name == CardActionName.SEDUCTION: verify_player_in_game(play_info.objective_player_id, game_name) objective_player = find_player_by_id(play_info.objective_player_id) - objective_player_hand_list = list(objective_player.hand) - eligible_cards = [ - card for card in objective_player_hand_list if card.type != CardType.THE_THING] - random_card = random.choice(eligible_cards) - - player.hand.add(random_card) - player.hand.remove(card) - objective_player.hand.remove(random_card) - objective_player.hand.add(card) + card_to_exchange = find_card_by_id(play_info.card_to_exchange) + if card_to_exchange.type == CardType.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The card to exchange cannot be The Thing" + ) + verify_card_in_hand(player, card_to_exchange) + process_seduction_card( + game, player, card, objective_player, card_to_exchange) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 783cd2a..d273fa1 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -18,6 +18,8 @@ class Events(str, Enum): PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' PLAYED_CARD = 'played_card' + PLAYER_ELIMINATED = 'player_eliminated' + WHISKEY_CARD_PLAYED = 'whiskey_card_played' @db_session diff --git a/app/tests/card_tests/test_create_cards.py b/app/tests/card_tests/test_create_cards.py index 438f940..36e81d4 100644 --- a/app/tests/card_tests/test_create_cards.py +++ b/app/tests/card_tests/test_create_cards.py @@ -151,6 +151,8 @@ def test_create_card_bad_type(): cleanup_database() # Test card names + + def test_create_card_missing_name(): cleanup_database() response = client.post( From 3a5293af18e01a4a4a42e6d018778b3e5269d9ba Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 18 Oct 2023 19:09:07 -0300 Subject: [PATCH 055/224] Se arregla problema que habia al querer eliminar carta de lanzallamas --- app/routers/games/action_functions.py | 57 ++++++++++----------------- app/routers/games/services.py | 2 +- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index ce03df3..b83c652 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -9,31 +9,22 @@ @db_session -async def process_flamethrower_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_flamethrower_card(game: Game, player: Player, + card: Card, objective_player: Player): objective_player.rol = PlayerRol.ELIMINATED # Las cartas del jugador eliminado van al mazo de descarte # salvo sea que sea la carta 'La Cosa' - for card in objective_player.hand: - if card.type != CardType.THE_THING: - game.discard_deck.add(card) - objective_player.hand.remove(card) + for c in objective_player.hand: + if c.type != CardType.THE_THING: + game.discard_deck.add(c) + objective_player.hand.remove(c) # Reacomodo las posiciones - for player in game.players: - if player.position > objective_player.position: - player.position -= 1 - objective_player.position = -1 - - json_msg = { - "event": Events.PLAYER_ELIMINATED, - "player_id": objective_player.id, - "player_name": objective_player.name - } for p in game.players: - player_connections.send_event_to(p.id, json_msg) - + if p.position > objective_player.position: + p.position -= 1 + objective_player.position = -1 game.discard_deck.add(card) player.hand.remove(card) @@ -41,13 +32,13 @@ async def process_flamethrower_card(game: Game, player: Player, @db_session def process_analysis_card(game: Game, player: Player, card: Card, objective_player: Player): - result = [CardResponse(id=card.id, - number=card.number, - type=card.type, - subtype=card.subtype, - name=card.name, - description=card.description - ) for card in objective_player.hand] + result = [CardResponse(id=c.id, + number=c.number, + type=c.type, + subtype=c.subtype, + name=c.name, + description=c.description + ) for c in objective_player.hand] game.discard_deck.add(card) player.hand.remove(card) return result @@ -72,15 +63,8 @@ def process_suspicious_card(game: Game, player: Player, @db_session -async def process_whiskey_card(game: Game, player: Player, card: Card): - json_msg = { - "event": Events.WHISKEY_CARD_PLAYED, - "player_id": player.id, - "player_name": player.name - } - player_connections.send_event_to_other_players_in_game( - game.name, json_msg, player.id) - +def process_whiskey_card(game: Game, player: Player, card: Card): + # Falta propagar el evento por WS game.discard_deck.add(card) player.hand.remove(card) @@ -97,8 +81,7 @@ def process_watch_your_back_card(game: Game, player: Player, card: Card): @db_session -def process_change_places_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_change_places_card(game: Game, player: Player, card: Card, objective_player: Player): # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position @@ -126,7 +109,7 @@ def process_seduction_card(game: Game, player: Player, card_to_exchange: Card): objective_player_hand_list = list(objective_player.hand) eligible_cards = [ - card for card in objective_player_hand_list if card.type != CardType.THE_THING] + c for c in objective_player_hand_list if c.type != CardType.THE_THING] random_card = random.choice(eligible_cards) player.hand.add(random_card) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 5e9594e..a622822 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -255,7 +255,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) - process_flamethrower_card(game, player, objective_player) + process_flamethrower_card(game, player, card, objective_player) # Analisis if card.name == CardActionName.ANALYSIS: From 3fe8a954fddca9c686ad7565b99676623987d914 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 18 Oct 2023 19:24:25 -0300 Subject: [PATCH 056/224] Cambios menores --- app/routers/games/action_functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index b83c652..5176ae1 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -25,6 +25,9 @@ def process_flamethrower_card(game: Game, player: Player, if p.position > objective_player.position: p.position -= 1 objective_player.position = -1 + + # Falta implementar Evento por WS + game.discard_deck.add(card) player.hand.remove(card) @@ -64,7 +67,7 @@ def process_suspicious_card(game: Game, player: Player, @db_session def process_whiskey_card(game: Game, player: Player, card: Card): - # Falta propagar el evento por WS + # Falta implementar evento por WS game.discard_deck.add(card) player.hand.remove(card) From 1e17f9be87d5f4ec47810d6ddd1ad11c4f2ccd69 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 18 Oct 2023 21:28:46 -0300 Subject: [PATCH 057/224] Se implemento que se mande los eventos cuando un jugador es eliminado y cuando se juega la carta whiskey --- app/routers/games/action_functions.py | 29 ++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 5176ae1..14adfe1 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -6,6 +6,26 @@ from .utils import Events from .schemas import RoundDirection import random +import asyncio + + +async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminated_name: str): + json_msg = { + "event": Events.PLAYER_ELIMINATED, + "player_id": eliminated_id, + "player_name": eliminated_name + } + for p in game.players: + await player_connections.send_event_to(p.id, json_msg) + + +async def send_players_whiskey_event(game: Game, player_id: int, player_name: str): + json_msg = { + "event": Events.WHISKEY_CARD_PLAYED, + "player_id": player_id, + "player_name": player_name + } + await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) @db_session @@ -26,11 +46,13 @@ def process_flamethrower_card(game: Game, player: Player, p.position -= 1 objective_player.position = -1 - # Falta implementar Evento por WS - game.discard_deck.add(card) player.hand.remove(card) + asyncio.ensure_future(send_players_eliminated_event(game=game, + eliminated_id=objective_player.id, + eliminated_name=objective_player.name)) + @db_session def process_analysis_card(game: Game, player: Player, @@ -67,9 +89,10 @@ def process_suspicious_card(game: Game, player: Player, @db_session def process_whiskey_card(game: Game, player: Player, card: Card): - # Falta implementar evento por WS game.discard_deck.add(card) player.hand.remove(card) + asyncio.ensure_future(send_players_whiskey_event( + game, player.id, player.name)) @db_session From 877d5bfbae7a75f37507c4fc6604a25ba314e7bf Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Thu, 19 Oct 2023 10:45:08 -0300 Subject: [PATCH 058/224] cambios finales --- app/routers/games/games.py | 11 +++++------ app/routers/games/utils.py | 16 ++++++++++------ app/routers/websockets/utils.py | 7 +++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 77bb741..b146bfa 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -139,7 +139,7 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): -@router.patch("/{game_name}/draw-card", status_code=status.HTTP_200_OK, response_model=DrawInformationOut) +@router.patch("/{game_name}/draw-card", status_code=status.HTTP_200_OK, response_model=CardResponse) async def draw_card(game_name: str, game_data: DrawInformationIn): utils.verify_draw_can_be_done(game_name, game_data) draw_card_information = services.draw_card(game_name, game_data) @@ -151,10 +151,9 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): "player_id": game_data.player_id, "next_card": draw_card_information.top_card_face } - - await player_connections.send_event_to_other_players_in_game(game_name=game_name, - message=json_msg, - excluded_id=game_data.player_id) - return draw_card_information + await player_connections.send_event_to_all_players_in_game(game_name=game_name, + message=json_msg) + + return draw_card_information.card \ No newline at end of file diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 841617a..5c49e1c 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -193,7 +193,7 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="It's not the turn of the player" ) - if player.role == PlayerRol.INFECTED and card.name == '¡Infectado!': + if player.rol == PlayerRol.INFECTED and card.name == '¡Infectado!': infected_count = select(count(c) for c in player.hand if c.name == '¡Infectado!') if infected_count <= 1: @@ -214,7 +214,7 @@ def re_build_draw_deck(game: Game) -> list[Card]: game.draw_deck = game.discard_deck game.discard_deck = [] - random.shuffle(game.draw_deck) + random.shuffle(list(game.draw_deck)) return game.draw_deck @@ -222,7 +222,6 @@ def re_build_draw_deck(game: Game) -> list[Card]: def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): game = Game.get(name=game_name) player = Player.get(id=game_data.player_id) - #traer el mazo de robo if not game: raise HTTPException( @@ -236,7 +235,12 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): detail="Draw deck not found" ) - #verify game status + + if game.status != GameStatus.STARTED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The game status is not started" + ) if len(game.draw_deck) == 0: raise HTTPException( @@ -262,10 +266,10 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): detail="The player is not in the game" ) - if game.turn != player.position: + if player.hand > 4: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="It's not the turn of the player" + detail="The player already has 5 cards" ) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 1b358ec..7009fc4 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -44,6 +44,13 @@ async def send_event_to(self, player_id: int, message): except KeyError: pass + async def send_event_to_all_players_in_game(self, game_name: str, message): + players_to_send_message = get_players_id(game_name) + for player_id, websocket in self.active_connections.items(): + if player_id in players_to_send_message: + await websocket.send_json(message) + + async def send_event_to_other_players_in_game(self, game_name: str, message, excluded_id: int): players_to_send_message = get_players_id(game_name) for player_id, websocket in self.active_connections.items(): From 6d725288276d17c55e1b6fa305ce11deb3043865 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Thu, 19 Oct 2023 11:04:41 -0300 Subject: [PATCH 059/224] hotfix --- app/routers/games/games.py | 1 - app/routers/games/utils.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index f33d3e3..06a3431 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -157,7 +157,6 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): utils.verify_draw_can_be_done(game_name, game_data) draw_card_information = services.draw_card(game_name, game_data) - # mandar por ws que se robo la carta y el dorso de la misma. json_msg = { "event": utils.Events.PLAYER_DRAW_CARD, "game_name": game_name, diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b7e521a..7b03eef 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -325,7 +325,7 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): detail="The player is not in the game" ) - if player.hand > 4: + if len(player.hand) > 4: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="The player already has 5 cards" From 28b43a99120ef92e67a7f3df2e5642c2ab428c38 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 15:58:50 -0300 Subject: [PATCH 060/224] Se agrega seed para crear 6 jugadores, y una partida con los 6 unidos --- Makefile | 3 +++ app/database/utils.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/database/utils.py diff --git a/Makefile b/Makefile index 32f8304..65a9167 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,9 @@ help: run: install poetry run $(UVICORN_CMD) +create-seed-data: install + poetry run python -c "from app.database.utils import create_seed_data; create_seed_data()" + # Define the 'delete-db' target to delete the database file delete-db: @if [ -f $(DB_FILE) ]; then \ diff --git a/app/database/utils.py b/app/database/utils.py new file mode 100644 index 0000000..1c289fc --- /dev/null +++ b/app/database/utils.py @@ -0,0 +1,23 @@ +from pony.orm import db_session, commit +from .models import Player, Game + + +@db_session +def create_seed_data(): + players = [] + for i in range(1, 7): + player = Player(name=f"Player{i}") + players.append(player) + commit() + + game = Game(name="TestGame", + min_players=4, + max_players=6, + password='secure', + host=players[0], + ) + commit() + players[0].game = game.name + commit() + for i in range(1, 6): + game.players.add(players[i]) From a9fc5afecfb22fd7ad2b33161ed0e981d896e64f Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 17:30:19 -0300 Subject: [PATCH 061/224] Se modifica al iniciar partida y se agrega un atributo mas con el orden para dar las cartas de draw_deck --- app/database/models.py | 3 ++- app/routers/games/services.py | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index f637eb0..96e6d1e 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1,4 +1,4 @@ -from pony.orm import PrimaryKey, Required, Set, Optional +from pony.orm import PrimaryKey, Required, Set, Optional, Json from app.database import db from app.routers.games.schemas import GameStatus, RoundDirection @@ -24,6 +24,7 @@ class Game(db.Entity): status = Required(str, default=GameStatus.UNSTARTED) discard_deck = Set('Card', reverse='games_discard_deck') draw_deck = Set('Card', reverse='games_draw_deck') + draw_deck_order = Required(Json, default=[]) round_direction = Required(str, default=RoundDirection.CLOCKWISE) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 88d73b0..810862d 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -164,6 +164,12 @@ def start_game(name: str) -> Game: draw_deck = cards_services.build_draw_deck( deal_deck=deal_deck, players=players_joined) + + # Pongo los id de las cartas en draw_deck_order y luego hago el shuffle (mezclar) + for c in draw_deck: + game.draw_deck_order.append(c.id) + random.shuffle(game.draw_deck_order) + game.draw_deck.add(draw_deck) # setting the position and rol of the players @@ -177,10 +183,12 @@ def start_game(name: str) -> Game: game.status = GameStatus.STARTED game.turn = 0 + top_card_face = select(card for card in game.draw_deck if card.id == game.draw_deck_order[0]).first().type + return GameStartOut( list_of_players=game.players, status=game.status, - top_card_face=list(game.draw_deck)[0].type + top_card_face=top_card_face ) @@ -328,18 +336,6 @@ def play_action_card(game_name: str, play_info: PlayInformation): @db_session def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOut: - game = Game.get(name=game_name) - player = Player.get(id=game_data.player_id) - draw_deck = list(game.draw_deck) - card = draw_deck.pop(0) - - player.hand.add(card) - game.draw_deck.remove(card) - - if len(game.draw_deck) == 0: - utils.re_build_draw_deck(game) - - return DrawInformationOut(player_id=player.id, - card=CardResponse.model_validate(card), - top_card_face=list(game.draw_deck)[0].type - ) + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + pass \ No newline at end of file From 9671a6e98c2c6b4885e893050520635ab3ffe852 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 18:09:57 -0300 Subject: [PATCH 062/224] Funcionando el robo de cartas --- app/routers/games/services.py | 42 ++++++++++++++++++++++++++++++----- app/routers/games/utils.py | 14 ------------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 810862d..d8e9e33 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -164,12 +164,12 @@ def start_game(name: str) -> Game: draw_deck = cards_services.build_draw_deck( deal_deck=deal_deck, players=players_joined) - + # Pongo los id de las cartas en draw_deck_order y luego hago el shuffle (mezclar) - for c in draw_deck: - game.draw_deck_order.append(c.id) + for card in draw_deck: + game.draw_deck_order.append(card.id) random.shuffle(game.draw_deck_order) - + game.draw_deck.add(draw_deck) # setting the position and rol of the players @@ -183,7 +183,8 @@ def start_game(name: str) -> Game: game.status = GameStatus.STARTED game.turn = 0 - top_card_face = select(card for card in game.draw_deck if card.id == game.draw_deck_order[0]).first().type + top_card_face = select( + card for card in game.draw_deck if card.id == game.draw_deck_order[0]).first().type return GameStartOut( list_of_players=game.players, @@ -338,4 +339,33 @@ def play_action_card(game_name: str, play_info: PlayInformation): def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOut: game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(game_data.player_id) - pass \ No newline at end of file + + if len(game.draw_deck_order) == 1: + new_deck_list = list(game.draw_deck) + list(game.discard_deck) + game.draw_deck.clear() + game.discard_deck.clear() + game.draw_deck_order = [] + game.draw_deck.add(new_deck_list) + # A continuacion genero el orden aleatorio de como van a descartarse las cartas + for card in game.draw_deck: + game.draw_deck_order.append(card.id) + random.shuffle(game.draw_deck_order) + + top_card_id = game.draw_deck_order.pop(0) + card = select(card for card in game.draw_deck if card.id == + top_card_id).first() + + player.hand.add(card) + game.draw_deck.remove(card) + + top_card_face = select( + card for card in game.draw_deck if card.id == game.draw_deck_order[0]).first().type + + return DrawInformationOut(player_id=player.id, + card=CardResponse(number=card.number, + type=card.type, + subtype=card.subtype, + name=card.name, + description=card.description, + id=card.id), + top_card_face=top_card_face) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 7b03eef..d4f8c27 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -267,20 +267,6 @@ def verify_adjacent_players(player_id: int, other_player_id: int, max_position: ) -@db_session -def re_build_draw_deck(game: Game) -> list[Card]: - if len(game.draw_deck) != 0: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='The draw deck is not empty.' - ) - game.draw_deck = game.discard_deck - game.discard_deck = [] - - random.shuffle(list(game.draw_deck)) - return game.draw_deck - - @db_session def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): game = Game.get(name=game_name) From 093e6bde96a300d0d9c074a9251fd75211789855 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 19:34:26 -0300 Subject: [PATCH 063/224] Se implemento el evento de NEW_TURN cuando se descarta o juega una carta de accion --- Makefile | 1 + README.md | 2 ++ app/routers/games/games.py | 25 ++++++++++++++++++++++++- app/routers/games/services.py | 24 ++++++++++++++++++++---- app/routers/games/utils.py | 1 + 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 65a9167..34eb37f 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ help: @echo "Usage: make [target]" @echo "Targets:" @echo " run - Run the UVicorn server" + @echo " create-seed-data - Seed to plant 6 players and 1 Game in the Database" @echo " delete-db - Delete the database file" @echo " coverage-report - Generate coverage reports" @echo " coverage-clean - Remove coverage reports" diff --git a/README.md b/README.md index 351737a..0586c61 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ In the Makefile, you have the following targets: `make run` starts the Uvicorn server with the application. If it's not defined in the .env file, de default port is 8000. +`make create-seed-data` Populates the database with 6 players and 1 game. + `make delete-db` deletes the application's database file if it exists. It will request confirmation before deletion. `make test-all` runs the application's tests using pytest and tracks code coverage. diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 06a3431..6b7a8e7 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, status +from pony.orm import db_session, select from app.database.models import Player from . import services from . import utils from .schemas import * from ..websockets.utils import player_connections +from .utils import find_game_by_name from ..players.utils import get_player_name_by_id from ..cards.utils import get_card_name_by_id @@ -136,7 +138,16 @@ async def leave_game(game_name: str, player_id: int): @router.patch("/{game_name}/discard", status_code=status.HTTP_200_OK) async def discard_card(game_name: str, game_data: DiscardInformationIn): utils.verify_discard_can_be_done(game_name, game_data) - services.discard_card(game_name, game_data) + game = services.discard_card(game_name, game_data) + with db_session: + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return {"message": "Card discarded"} @@ -149,6 +160,18 @@ async def play_action_card(game_name: str, play_info: PlayInformation): "card_name": get_card_name_by_id(play_info.card_id) } await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + return result diff --git a/app/routers/games/services.py b/app/routers/games/services.py index d8e9e33..281d63b 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -220,15 +220,24 @@ def leave_game(game_name: str, player_id: int) -> GameInformationOut: @db_session -def discard_card(game_name: str, game_data: DiscardInformationIn): - game = Game.get(name=game_name) - player = Player.get(id=game_data.player_id) +def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: + game: Game = Game.get(name=game_name) + player: Player = Player.get(id=game_data.player_id) card = Card.get(id=game_data.card_id) if card in player.hand: player.hand.remove(card) if game and card: game.discard_deck.add(card) + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + game.turn = (game.turn - 1) % players_playing + else: + game.turn = (game.turn + 1) % players_playing + + return game + @db_session def finish_game(name: str) -> Game: @@ -246,7 +255,7 @@ def finish_game(name: str) -> Game: @db_session -def play_action_card(game_name: str, play_info: PlayInformation): +def play_action_card(game_name: str, play_info: PlayInformation) -> Game: result = {"message": "Action card played"} game = find_game_by_name(game_name) verify_player_in_game(play_info.player_id, game_name) @@ -332,6 +341,13 @@ def play_action_card(game_name: str, play_info: PlayInformation): process_seduction_card( game, player, card, objective_player, card_to_exchange) + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + game.turn = (game.turn - 1) % players_playing + else: + game.turn = (game.turn + 1) % players_playing + return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index d4f8c27..4096fba 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -22,6 +22,7 @@ class Events(str, Enum): PLAYER_ELIMINATED = 'player_eliminated' WHISKEY_CARD_PLAYED = 'whiskey_card_played' PLAYER_DRAW_CARD = 'player_draw_card' + NEW_TURN = 'new_turn' @db_session From 4f54cdb8e2d544a60cefa2c288d02372432cc152 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 19:47:46 -0300 Subject: [PATCH 064/224] Cambio menor en la funcion de la semilla de los datos --- app/database/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/database/utils.py b/app/database/utils.py index 1c289fc..7d4916d 100644 --- a/app/database/utils.py +++ b/app/database/utils.py @@ -12,7 +12,7 @@ def create_seed_data(): game = Game(name="TestGame", min_players=4, - max_players=6, + max_players=12, password='secure', host=players[0], ) From b7454b32ba2b3b54dc5d63bbd2abc16fc8c43162 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 19 Oct 2023 22:29:45 -0300 Subject: [PATCH 065/224] Se soluciono bug que estaba pendiente cuando se rearma el mazo de robo --- app/routers/games/games.py | 3 ++ app/routers/games/services.py | 10 +------ app/routers/games/utils.py | 53 +++++++++++++++++------------------ 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6b7a8e7..e11661f 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -144,6 +144,7 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): p for p in game.players if p.position == game.turn).first().id json_msg = { "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), "next_player_id": player_id_turn, "round_direction": game.round_direction } @@ -167,6 +168,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): p for p in game.players if p.position == game.turn).first().id json_msg = { "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), "next_player_id": player_id_turn, "round_direction": game.round_direction } @@ -183,6 +185,7 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): json_msg = { "event": utils.Events.PLAYER_DRAW_CARD, "game_name": game_name, + "player_name": get_player_name_by_id(game_data.player_id), "player_id": game_data.player_id, "next_card": draw_card_information.top_card_face } diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 281d63b..1f416d6 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -357,15 +357,7 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu player: Player = find_player_by_id(game_data.player_id) if len(game.draw_deck_order) == 1: - new_deck_list = list(game.draw_deck) + list(game.discard_deck) - game.draw_deck.clear() - game.discard_deck.clear() - game.draw_deck_order = [] - game.draw_deck.add(new_deck_list) - # A continuacion genero el orden aleatorio de como van a descartarse las cartas - for card in game.draw_deck: - game.draw_deck_order.append(card.id) - random.shuffle(game.draw_deck_order) + merge_decks_of_card(game_name) top_card_id = game.draw_deck_order.pop(0) card = select(card for card in game.draw_deck if card.id == diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 4096fba..f8713bb 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -269,26 +269,34 @@ def verify_adjacent_players(player_id: int, other_player_id: int, max_position: @db_session -def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): - game = Game.get(name=game_name) - player = Player.get(id=game_data.player_id) +def merge_decks_of_card(game_name: str): + game: Game = find_game_by_name(game_name) + top_card_id = game.draw_deck_order.pop(0) + new_deck_list = list(game.draw_deck) + list(game.discard_deck) + game.draw_deck.clear() + game.discard_deck.clear() + game.draw_deck_order = [] + game.draw_deck.add(new_deck_list) + + # A continuacion genero el orden aleatorio de como van a robarse las cartas + for card in game.draw_deck: + if card.id != top_card_id: + game.draw_deck_order.append(card.id) + random.shuffle(game.draw_deck_order) + # Inserto primera la carta que quedaba en draw_deck + game.draw_deck_order.insert(0, top_card_id) - if not game: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Game not found" - ) - if not game.draw_deck: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Draw deck not found" - ) +@db_session +def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): + game = find_game_by_name(game_name) + player = find_player_by_id(game_data.player_id) + verify_player_in_game(player_id=game_data.player_id, game_name=game_name) if game.status != GameStatus.STARTED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="The game status is not started" + detail="The game status isn't STARTED" ) if len(game.draw_deck) == 0: @@ -297,23 +305,14 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): detail="The draw deck is empty" ) - if not player: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Player not found" - ) - - is_player_in_game = select( - p for p in game.players if (p.id == player.id)).exists() - - if not is_player_in_game: + if len(player.hand) > 4: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="The player is not in the game" + detail="The player already has 5 cards" ) - if len(player.hand) > 4: + if game.turn != player.position: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="The player already has 5 cards" + detail="Is not the player turn" ) From def534a6e392a77a260e480efd875b34b181e66e Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 20 Oct 2023 12:02:41 -0300 Subject: [PATCH 066/224] se envia por ws el evento game_ended --- app/routers/games/services.py | 20 +++++++++++++------- app/routers/games/utils.py | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 281d63b..78bea12 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,4 +1,5 @@ from pony.orm import * +from typing import List from app.database.models import Game, Player, Card from .schemas import * from ..players.schemas import PlayerRol @@ -98,14 +99,13 @@ def update_game(game_name: str, request_data: GameUpdateIn) -> GameUpdateOut: @db_session def delete_game(game_name: str): - game = Game.get(name=game_name) + game: Game = find_game_by_name(game_name) - if not game: + if game.status != GameStatus.ENDED: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Game not found") - if game_name != game.name: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid game name") + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The game is not ended." + ) for player in game.players: player.hand.clear() @@ -246,7 +246,13 @@ def finish_game(name: str) -> Game: try: verify_game_can_be_finished(game) game.status = GameStatus.ENDED - # enviar por ws los resultados + + json_msg = { + "event": utils.Events.GAME_ENDED, + } + + asyncio.run(player_connections.send_event_to_all_players_in_game( + game.name, json_msg)) return game except Exception as e: diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 4096fba..879c6d7 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -15,6 +15,7 @@ class Events(str, Enum): GAME_DELETED = 'game_deleted' GAME_STARTED = 'game_started' GAME_CANCELED = 'game_canceled' + GAME_ENDED = 'game_ended' PLAYER_JOINED = 'player_joined' PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' From a115f8d03417f2f18948332b2bfd268777539f60 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 20 Oct 2023 12:07:11 -0300 Subject: [PATCH 067/224] se agregaga endpoint para obtener los resultados de una partida finalizada --- app/routers/cards/services.py | 13 +++++++++++++ app/routers/games/games.py | 5 +++++ app/routers/games/schemas.py | 9 ++++++++- app/routers/games/services.py | 24 ++++++++++++++++++++++++ app/routers/players/schemas.py | 14 +++++++++++++- 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index dbc8ca8..0e80918 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -56,6 +56,19 @@ def find_card_by_id(id: int) -> Card: return card +@db_session +def find_card_by_name(name: str) -> Card: + card = Card.get(name=name) + + if card is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Card not found." + ) + + return card + + @db_session def update_card(card_id: int, request_data: CardUpdateIn) -> CardUpdateOut: card = find_card_by_id(card_id) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index e11661f..357cf71 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -26,6 +26,11 @@ def get_game_information(game_name: str): return services.get_game_information(game_name) +@router.get("/{game_name}/result") +def get_game_result(game_name: str) -> GameResult: + return services.get_game_result(game_name) + + @router.post("/", response_model=GameCreationOut, status_code=status.HTTP_201_CREATED) async def create_game(game_data: GameCreationIn): new_game = services.create_game(game_data) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index c24568d..3b0dbcd 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from typing import List, Optional from enum import Enum -from ..players.schemas import PlayerResponse +from ..players.schemas import PlayerResponse, PlayerInfo from ..cards.schemas import CardType, CardResponse @@ -129,3 +129,10 @@ class DrawInformationOut(BaseModel): player_id: int card: CardResponse top_card_face: CardType + + +class GameResult(BaseModel): + model_config = ConfigDict(from_attributes=True) + + winners: List[PlayerInfo] + losers: List[PlayerInfo] diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 1f416d6..94d0d60 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -377,3 +377,27 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu description=card.description, id=card.id), top_card_face=top_card_face) + + +@db_session +def get_game_result(name: str) -> GameResult: + game: Game = find_game_by_name(name) + + if game.status != GameStatus.ENDED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The game is not ended." + ) + + winners = [] + losers = [] + + if the_thing_is_eliminated(game): + winners = game.players.select(lambda p: p.rol == PlayerRol.HUMAN)[:] + losers = game.players.select( + lambda p: p.rol in [PlayerRol.INFECTED, PlayerRol.ELIMINATED])[:] + + return GameResult( + winners=[PlayerInfo.model_validate(p) for p in winners], + losers=[PlayerInfo.model_validate(p) for p in losers] + ) diff --git a/app/routers/players/schemas.py b/app/routers/players/schemas.py index ecf4452..4c8f941 100644 --- a/app/routers/players/schemas.py +++ b/app/routers/players/schemas.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, computed_field from typing import List, Optional from enum import Enum +from ..cards.schemas import CardResponse class PlayerRol(str, Enum): @@ -31,3 +32,14 @@ class PlayerUpdateIn(BaseModel): class PlayerResponse(BasePlayer): id: int position: int + + +class PlayerInfo(PlayerResponse): + rol: PlayerRol + hand: List[CardResponse] + + @computed_field + @property + def was_the_thing(self) -> bool: + card_names = map(lambda card: card.name, self.hand) + return 'La Cosa' in card_names From 857e3aa65bb1f9f2348cbf67149b82c8f96a987e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 20 Oct 2023 13:15:26 -0300 Subject: [PATCH 068/224] Se arreglo problema en test de delete --- app/routers/games/games.py | 41 +++++++++++--------- app/routers/games/utils.py | 8 ++++ app/tests/game_tests/test_delete_of_games.py | 18 +++++++-- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 357cf71..be1b1be 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -5,7 +5,7 @@ from . import utils from .schemas import * from ..websockets.utils import player_connections -from .utils import find_game_by_name +from .utils import find_game_by_name, is_the_game_finished from ..players.utils import get_player_name_by_id from ..cards.utils import get_card_name_by_id @@ -160,24 +160,27 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): @router.post("/{game_name}/play-action-card", status_code=status.HTTP_200_OK) async def play_action_card(game_name: str, play_info: PlayInformation): result = services.play_action_card(game_name, play_info) - json_msg = { - "event": utils.Events.PLAYED_CARD, - "player_name": get_player_name_by_id(play_info.player_id), - "card_name": get_card_name_by_id(play_info.card_id) - } - await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) - - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + if is_the_game_finished(game_name): + services.finish_game(game_name) + else: + json_msg = { + "event": utils.Events.PLAYED_CARD, + "player_name": get_player_name_by_id(play_info.player_id), + "card_name": get_card_name_by_id(play_info.card_id) + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b98be34..a597a83 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -317,3 +317,11 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="Is not the player turn" ) + +@db_session +def is_the_game_finished(game_name: str) -> bool: + is_finish = False + game: Game = find_game_by_name(game_name) + if no_human_remains(game) or the_thing_is_eliminated(game): + is_finish = True + return is_finish \ No newline at end of file diff --git a/app/tests/game_tests/test_delete_of_games.py b/app/tests/game_tests/test_delete_of_games.py index a7240a2..39f5df1 100644 --- a/app/tests/game_tests/test_delete_of_games.py +++ b/app/tests/game_tests/test_delete_of_games.py @@ -35,7 +35,7 @@ def __init__( self.status = status self.round_direction = round_direction - def delete(): + def delete(a): pass @@ -52,7 +52,7 @@ def delete(): max_players=6, password=None, turn=-1, - status="UNSTARTED", + status="ENDED", round_direction="CLOCKWISE" ) ] @@ -68,8 +68,18 @@ def test_delete_game_success(mocker): "message": "Game deleted"}, "El mensaje de la respuesta no es 'Game deleted' (Juego eliminado)." -def test_delete_game_with_invalid_name(mocker): - mocker.patch.object(Game, "get", return_value=games[0]) +def test_delete_game_with_unstarted_status(mocker): + game = FakeGame(name="game1", + players=[ignacio, anelio, ezequiel], + host=ignacio, + min_players=4, + max_players=6, + password=None, + turn=-1, + status="UNSTARTED", + round_direction="CLOCKWISE" + ) + mocker.patch.object(Game, "get", return_value=game) response = client.delete("/games/game2") assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." From 33714562768d88a1d01df40711532d9ef9e5484f Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 20 Oct 2023 14:30:49 -0300 Subject: [PATCH 069/224] Actualizacion temp --- app/routers/games/games.py | 4 +++- app/routers/games/services.py | 24 +++++++++--------------- app/routers/games/utils.py | 10 ++++------ 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index be1b1be..ffbb17b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -8,6 +8,7 @@ from .utils import find_game_by_name, is_the_game_finished from ..players.utils import get_player_name_by_id from ..cards.utils import get_card_name_by_id +from .services import finish_game router = APIRouter( @@ -160,8 +161,9 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): @router.post("/{game_name}/play-action-card", status_code=status.HTTP_200_OK) async def play_action_card(game_name: str, play_info: PlayInformation): result = services.play_action_card(game_name, play_info) + if is_the_game_finished(game_name): - services.finish_game(game_name) + finish_game() else: json_msg = { "event": utils.Events.PLAYED_CARD, diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 56c6a2d..40239cb 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -239,25 +239,19 @@ def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: return game -@db_session -def finish_game(name: str) -> Game: - game: Game = find_game_by_name(name) - - try: +async def finish_game(name: str) -> Game: + with db_session: + game: Game = find_game_by_name(name) verify_game_can_be_finished(game) game.status = GameStatus.ENDED - json_msg = { - "event": utils.Events.GAME_ENDED, - } + json_msg = { + "event": utils.Events.GAME_ENDED, + } - asyncio.run(player_connections.send_event_to_all_players_in_game( - game.name, json_msg)) - - return game - except Exception as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + player_connections.send_event_to_all_players_in_game(game.name, json_msg) + return game + @db_session diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index a597a83..0159fb1 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -108,12 +108,10 @@ def verify_game_can_be_abandon(game_name: str, player_id: int): @db_session def verify_game_can_be_finished(game: Game): - if not the_thing_is_eliminated(game): - raise Exception('The Thing is still alive') - - if not no_human_remains(game): - raise Exception('There are living Humans') - + if the_thing_is_eliminated(game) or no_human_remains(game): + pass + else: + raise Exception('The Thing and humans are still alive') @db_session def the_thing_is_eliminated(game: Game) -> bool: From b2ec7433d9eb953a7ec5c6a7c11ca91fee44d4bf Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 20 Oct 2023 18:18:36 -0300 Subject: [PATCH 070/224] Se agregan 2 endpoints para el intercambio de cartas --- app/routers/games/games.py | 39 ++++++++++++++++++++ app/routers/games/schemas.py | 15 ++++++++ app/routers/games/services.py | 22 +++++++++++ app/routers/games/utils.py | 27 ++++++++++++++ app/tests/game_tests/test_delete_of_games.py | 18 +++++++-- 5 files changed, 117 insertions(+), 4 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 357cf71..e5eac02 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -199,3 +199,42 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): message=json_msg) return draw_card_information.card + + +@router.patch("/{game_name}/intention-to-interchange-card", status_code=status.HTTP_200_OK) +async def intention_to_interchange_card(game_name: str, interchange_info: IntentionExchangeInformationIn): + utils.verify_if_interchange_can_be_done(game_name, interchange_info) + objective_player_id = utils.get_id_of_next_player_in_turn(game_name) + json_msg = { + "event": "exchange_intention", + "player_id": interchange_info.player_id, + "player_name": get_player_name_by_id(interchange_info.player_id), + "card_to_exchange": interchange_info.card_id + } + await player_connections.send_event_to(objective_player_id, json_msg) + return {"message": "Card interchange intention terminated."} + + +@router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) +async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): + services.card_interchange_response(game_name, game_data) + json_msg = { + "event": "exchange_done" + } + await player_connections.send_event_to(game_data.player_id, json_msg) + await player_connections.send_event_to(game_data.objective_player_id, json_msg) + + + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + return {"message": "Card interchange terminated."} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 3b0dbcd..9cc9521 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -136,3 +136,18 @@ class GameResult(BaseModel): winners: List[PlayerInfo] losers: List[PlayerInfo] + +class IntentionExchangeInformationIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int # ID jugador que inicia la intencion + card_id: int # Card ID del jugador que inicia la intencion + +class InterchangeInformationIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int # ID jugador que recibe la intencion + card_id: int # Card ID del jugador que recibe la intencion + objective_player_id: int # ID jugador que inicia la intencion + objective_card_id: int # Card ID del jugador que inicia la intencion + diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 56c6a2d..a00290a 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -407,3 +407,25 @@ def get_game_result(name: str) -> GameResult: winners=[PlayerInfo.model_validate(p) for p in winners], losers=[PlayerInfo.model_validate(p) for p in losers] ) + +@db_session +def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.objective_player_id) + player_card: Card = Card[game_data.objective_card_id] + + next_player: Player = find_player_by_id(game_data.player_id) + next_player_card: Card = Card[game_data.card_id] + + player.hand.remove(player_card) + next_player.hand.remove(next_player_card) + + player.hand.add(next_player_card) + next_player.hand.add(player_card) + + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + game.turn = (game.turn - 1) % players_playing + else: + game.turn = (game.turn + 1) % players_playing diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b98be34..7fdf404 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -317,3 +317,30 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): status_code=status.HTTP_400_BAD_REQUEST, detail="Is not the player turn" ) + + +@db_session +def verify_if_interchange_can_be_done(game_name: str, interchange_info: IntentionExchangeInformationIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(interchange_info.player_id) + + if game.turn != player.position: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Is not the player turn" + ) + + +@db_session +def get_id_of_next_player_in_turn(game_name): + game: Game = find_game_by_name(game_name) + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + next_turn = (game.turn - 1) % players_playing + else: + next_turn = (game.turn + 1) % players_playing + + next_player_id = select( + p.id for p in game.players if p.position == next_turn).first() + return next_player_id diff --git a/app/tests/game_tests/test_delete_of_games.py b/app/tests/game_tests/test_delete_of_games.py index a7240a2..6bb6e57 100644 --- a/app/tests/game_tests/test_delete_of_games.py +++ b/app/tests/game_tests/test_delete_of_games.py @@ -35,7 +35,7 @@ def __init__( self.status = status self.round_direction = round_direction - def delete(): + def delete(a): pass @@ -52,7 +52,7 @@ def delete(): max_players=6, password=None, turn=-1, - status="UNSTARTED", + status="ENDED", round_direction="CLOCKWISE" ) ] @@ -68,8 +68,18 @@ def test_delete_game_success(mocker): "message": "Game deleted"}, "El mensaje de la respuesta no es 'Game deleted' (Juego eliminado)." -def test_delete_game_with_invalid_name(mocker): - mocker.patch.object(Game, "get", return_value=games[0]) +def test_delete_game_with_unstarted_status(mocker): + game = FakeGame(name="game1", + players=[ignacio, anelio, ezequiel], + host=ignacio, + min_players=4, + max_players=6, + password=None, + turn=-1, + status="UNSTARTED", + round_direction="CLOCKWISE" + ) + mocker.patch.object(Game, "get", return_value=game) response = client.delete("/games/game2") assert response.status_code == 400, "El código de estado de la respuesta no es 400 (Bad Request)." From e443ae1f62b1b514e43766eb3f8cfb5f66e1a58e Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 20 Oct 2023 19:17:03 -0300 Subject: [PATCH 071/224] se agrega caso especial de finalizacion de partida y se mejora la logica --- app/routers/games/schemas.py | 1 + app/routers/games/services.py | 18 ++++++++++++++++++ app/routers/games/utils.py | 27 ++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 3b0dbcd..bead3ce 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -134,5 +134,6 @@ class DrawInformationOut(BaseModel): class GameResult(BaseModel): model_config = ConfigDict(from_attributes=True) + reason: str winners: List[PlayerInfo] losers: List[PlayerInfo] diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 94d0d60..1f559be 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -389,15 +389,33 @@ def get_game_result(name: str) -> GameResult: detail=f"The game is not ended." ) + reason = "" winners = [] losers = [] if the_thing_is_eliminated(game): + reason = "La Cosa fue eliminada de la partida." winners = game.players.select(lambda p: p.rol == PlayerRol.HUMAN)[:] losers = game.players.select( lambda p: p.rol in [PlayerRol.INFECTED, PlayerRol.ELIMINATED])[:] + elif no_human_remains(game): + reason = "No queda ningún Humano en la partida." + winners = game.players.select( + lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] + losers = game.players.select( + lambda p: p.rol == PlayerRol.ELIMINATED)[:] + + elif the_thing_infected_everyone(game): + reason = '''La Cosa ha logrado infectar a todos los demás jugadores + sin que haya sido eliminado ningún Humano de la partida.''' + winners = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING)[:] + losers = game.players.select( + lambda p: p.rol != PlayerRol.THE_THING)[:] + return GameResult( + reason=reason, winners=[PlayerInfo.model_validate(p) for p in winners], losers=[PlayerInfo.model_validate(p) for p in losers] ) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index f8713bb..d628145 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -116,15 +116,32 @@ def verify_game_can_be_finished(game: Game): @db_session def the_thing_is_eliminated(game: Game) -> bool: - the_thing = game.players.select( - lambda p: p.rol == PlayerRol.THE_THING).count() - return the_thing == 0 + the_thing_exists = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING).exists() + + return not the_thing_exists @db_session def no_human_remains(game: Game) -> bool: - humans = game.players.select(lambda p: p.rol == PlayerRol.HUMAN).count() - return humans == 0 + the_thing_exists = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING).exists() + + number_of_humans = game.players.select( + lambda p: p.rol == PlayerRol.HUMAN).count() + + return the_thing_exists and number_of_humans == 0 + + +@db_session +def the_thing_infected_everyone(game: Game) -> bool: + the_thing_exists = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING).exists() + + number_of_infecteds = game.players.select( + lambda p: p.rol == PlayerRol.INFECTED).count() + + return the_thing_exists and number_of_infecteds == count(game.players) - 1 @db_session From 5629df282af09c9cfdd27a69f641a3ad1d4829f2 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 20 Oct 2023 21:42:56 -0300 Subject: [PATCH 072/224] Se agregan verificaciones en los endpoints para el intercambio de cartas --- app/routers/games/games.py | 1 + app/routers/games/utils.py | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index e5eac02..462aa1e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -217,6 +217,7 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent @router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): + utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) json_msg = { "event": "exchange_done" diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 7fdf404..96aa159 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -323,12 +323,68 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): def verify_if_interchange_can_be_done(game_name: str, interchange_info: IntentionExchangeInformationIn): game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(interchange_info.player_id) + card: Card = Card[interchange_info.card_id] if game.turn != player.position: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Is not the player turn" ) + if interchange_info.card_id == 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The Thing cannot be interchange' + ) + if card not in player.hand: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card selected for the interchange is not in the player hand' + ) + + +@db_session +def verify_if_interchange_response_can_be_done(game_name: str, game_data: InterchangeInformationIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + player_card: Card = Card[game_data.card_id] + objective_player: Player = find_player_by_id(game_data.objective_player_id) + objective_player_card: Card = Card[game_data.objective_card_id] + + if not player_card: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Card of next player in turn not found' + ) + if not objective_player_card: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Card of the player in turn not found' + ) + if player_card.id == 1 or objective_player_card.id == 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The Thing cannot be interchange' + ) + if player_card not in player.hand: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card selected for the interchange is not in the next player hand' + ) + if objective_player_card not in objective_player.hand: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card selected for the interchange is not in the player hand' + ) + if not player in game.players: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The next player in turn is not in the game' + ) + if not objective_player in game.players: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The player in turn is not in the game' + ) @db_session From c55c9ba54cd8b379f6eac749374dd063aac7ed35 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 21 Oct 2023 12:47:42 -0300 Subject: [PATCH 073/224] se agrega endpoint para hardcodear eventos y enviarlos a los jugadores de una partida --- app/routers/websockets/websockets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/routers/websockets/websockets.py b/app/routers/websockets/websockets.py index 75dad8a..e7e06df 100644 --- a/app/routers/websockets/websockets.py +++ b/app/routers/websockets/websockets.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, WebSocket +from .utils import player_connections from . import services router = APIRouter( @@ -10,3 +11,8 @@ @router.websocket("/{player_id}") def websockets_games(player_id: int, websocket: WebSocket): return services.websocket_games(player_id, websocket) + + +@router.get('/{game_name}/send-self-event') +async def send_self_event(game_name: str, event: str): + await player_connections.send_event_to_all_players_in_game(game_name, {'event': event}) From 69de2a0f3b2914ca3ce3066703b99b98a3ecbe8a Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 21 Oct 2023 13:09:26 -0300 Subject: [PATCH 074/224] se remueve verificacion al endpoint de game_result --- app/routers/games/services.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 56c6a2d..0fb0700 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -389,11 +389,11 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu def get_game_result(name: str) -> GameResult: game: Game = find_game_by_name(name) - if game.status != GameStatus.ENDED: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"The game is not ended." - ) + # if game.status != GameStatus.ENDED: + # raise HTTPException( + # status_code=status.HTTP_400_BAD_REQUEST, + # detail=f"The game is not ended." + # ) winners = [] losers = [] From afedb742cc78ee6628a5629d00fd38685a6978ac Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 21 Oct 2023 15:43:43 -0300 Subject: [PATCH 075/224] Se corrige con sugerencia de Eze enviada por el comment de la PR --- app/routers/games/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 96aa159..d29b188 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -330,7 +330,7 @@ def verify_if_interchange_can_be_done(game_name: str, interchange_info: Intentio status_code=status.HTTP_400_BAD_REQUEST, detail="Is not the player turn" ) - if interchange_info.card_id == 1: + if card.type == CardType.THE_THING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='The Thing cannot be interchange' @@ -360,11 +360,16 @@ def verify_if_interchange_response_can_be_done(game_name: str, game_data: Interc status_code=status.HTTP_404_NOT_FOUND, detail='Card of the player in turn not found' ) - if player_card.id == 1 or objective_player_card.id == 1: + if player_card.type == CardType.THE_THING or objective_player_card.type == CardType.THE_THING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='The Thing cannot be interchange' ) + if player_card.type != CardType.STAY_AWAY: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card of the next player in turn must be a STAY_AWAY card' + ) if player_card not in player.hand: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From 977f31e0ed11fbf32799be37a5e4b94a3cab39eb Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 21 Oct 2023 15:56:34 -0300 Subject: [PATCH 076/224] Se modifo con mas cambios para verificacion de que ocurre cuando se intenta pasar una carta infectado --- app/routers/cards/schemas.py | 2 +- app/routers/games/utils.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index ea0f275..2e7af65 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -10,7 +10,7 @@ class CardType(str, Enum): class CardSubtype(str, Enum): - CONTACION = 'CONTAGION' + CONTAGION = 'CONTAGION' ACTION = 'ACTION' DEFENSE = 'DEFENSE' OBSTACLE = 'OBSTACLE' diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index d29b188..f85c8b7 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -7,6 +7,7 @@ from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol from ..players.utils import find_player_by_id +from ..cards.schemas import CardType, CardSubtype class Events(str, Enum): @@ -340,6 +341,11 @@ def verify_if_interchange_can_be_done(game_name: str, interchange_info: Intentio status_code=status.HTTP_400_BAD_REQUEST, detail='The card selected for the interchange is not in the player hand' ) + if card.subtype == CardSubtype.CONTAGION and player.rol != PlayerRol.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The player cannot pass the card Infected because is not The Thing' + ) @db_session @@ -390,6 +396,11 @@ def verify_if_interchange_response_can_be_done(game_name: str, game_data: Interc status_code=status.HTTP_400_BAD_REQUEST, detail='The player in turn is not in the game' ) + if player_card.subtype == CardSubtype.CONTAGION and player.rol != PlayerRol.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Player next in turn cannot pass an infected card because is not The Thing' + ) @db_session From da688289d8c29b857a318cb8f38818e2f15c569e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 21 Oct 2023 16:18:25 -0300 Subject: [PATCH 077/224] Se agrega que mande el evento que se descarto una carta a todos los jugadores --- app/routers/games/games.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 462aa1e..5fe5860 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -147,6 +147,15 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): with db_session: player_id_turn = select( p for p in game.players if p.position == game.turn).first().id + + json_msg = { + "event": "discard_card", + "player_name": get_player_name_by_id(game_data.player_id), + "card_id": game_data.card_id, + "card_name": get_card_name_by_id(game_data.card_id) + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + json_msg = { "event": utils.Events.NEW_TURN, "next_player_name": get_player_name_by_id(player_id_turn), @@ -154,6 +163,7 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): "round_direction": game.round_direction } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + return {"message": "Card discarded"} From baad53d2973a33511d80ee83e286e87ea4701274 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 21 Oct 2023 17:13:20 -0300 Subject: [PATCH 078/224] modificacion finish game --- app/routers/games/services.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 1f559be..ecb9112 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -239,20 +239,24 @@ def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: return game -@db_session -def finish_game(name: str) -> Game: - game: Game = find_game_by_name(name) +async def finish_game(name: str) -> Game: + with db_session: + game: Game = find_game_by_name(name) - try: - verify_game_can_be_finished(game) - game.status = GameStatus.ENDED - # enviar por ws los resultados + try: + verify_game_can_be_finished(game) + game.status = GameStatus.ENDED - return game - except Exception as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + json_msg = { + "event": utils.Events.GAME_ENDED, + } + player_connections.send_event_to_all_players_in_game(game.name, json_msg) + return game @db_session def play_action_card(game_name: str, play_info: PlayInformation) -> Game: From a6d1583d199bb05d069df41a283abba0f13e51ff Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sun, 22 Oct 2023 19:23:54 -0300 Subject: [PATCH 079/224] Esqueleto para la implementacion del endpoint de jugar cartas de panico --- app/routers/cards/schemas.py | 14 ++++++++ app/routers/cards/utils.py | 8 ++++- app/routers/games/games.py | 12 +++++++ app/routers/games/services.py | 68 +++++++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index ea0f275..3125089 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -29,6 +29,20 @@ class CardActionName(str, Enum): BETTER_RUN = '¡Más vale que corras!' SEDUCTION = 'Seducción' +class CardPanicName(str, Enum): + JUST_BETWEEN_US = 'Que quede entre nosotros...' + REVELATIONS = 'Revelaciones' + ROTTEN_ROPES = 'Cuerdas podridas' + ONE_TWO = 'Uno, dos...' + THREE_FOUR = 'Tres, cuatro...' + SO_THIS_IS_THE_PARTY = '¿Es aquí la fiesta?' + OOOPS = '¡Ups!' + FORGETFUL = 'Olvidadizo' + ROUND_AND_ROUND = 'Vuelta y vuelta' + CANT_WE_BE_FRIENDS = '¿No podemos ser amigos?' + BLIND_DATE = 'Cita a ciegas' + GETOUT = '¡Sal de aquí!' + class BaseCard(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 2d678d8..1a1f5d6 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -1,7 +1,7 @@ from pony.orm import db_session from fastapi import HTTPException, status from app.database.models import Card -from ..cards.schemas import CardSubtype +from ..cards.schemas import CardType, CardSubtype @db_session @@ -22,6 +22,12 @@ def verify_action_card(card: Card): detail="Card is not an ACTION card" ) +def verify_panic_card(card: Card): + if card.typej != CardType.PANIC: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Card is not a PANIC card" + ) @db_session def get_card_name_by_id(card_id: int) -> str: diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6309b0d..be3e02a 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -187,6 +187,18 @@ async def play_action_card(game_name: str, play_info: PlayInformation): return result +@router.post("/{game_name}/play-panic-card", status_code=status.HTTP_200_OK) +async def play_action_card(game_name: str, play_info: PlayInformation): + result = services.play_panic_card(game_name, play_info) + json_msg = { + "event": utils.Events.PLAYED_CARD, + "player_name": get_player_name_by_id(play_info.player_id), + "card_name": get_card_name_by_id(play_info.card_id) + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + return result + + @router.patch("/{game_name}/draw-card", status_code=status.HTTP_200_OK, response_model=CardResponse) async def draw_card(game_name: str, game_data: DrawInformationIn): utils.verify_draw_can_be_done(game_name, game_data) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index c09b8f9..6e7fca7 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -6,9 +6,9 @@ from fastapi import HTTPException, status from .utils import * from ..cards import services as cards_services -from ..cards.utils import find_card_by_id, verify_action_card +from ..cards.utils import find_card_by_id, verify_action_card, verify_panic_card from ..players.utils import find_player_by_id, verify_card_in_hand -from ..cards.schemas import CardActionName, CardResponse +from ..cards.schemas import CardActionName, CardResponse, CardPanicName from ..players.schemas import PlayerRol from .action_functions import * import random @@ -259,7 +259,7 @@ async def finish_game(name: str) -> Game: @db_session -def play_action_card(game_name: str, play_info: PlayInformation) -> Game: +def play_action_card(game_name: str, play_info: PlayInformation): result = {"message": "Action card played"} game = find_game_by_name(game_name) verify_player_in_game(play_info.player_id, game_name) @@ -423,3 +423,65 @@ def get_game_result(name: str) -> GameResult: winners=[PlayerInfo.model_validate(p) for p in winners], losers=[PlayerInfo.model_validate(p) for p in losers] ) + +@db_session +def play_panic_card(game_name: str, play_info: PlayInformation): + result = {"message": "Panic card played"} + game = find_game_by_name(game_name) + verify_player_in_game(play_info.player_id, game_name) + player = find_player_by_id(play_info.player_id) + card = find_card_by_id(play_info.card_id) + verify_panic_card(card) + verify_card_in_hand(player, card) + + # Que quede entre nosotros + if card.name == CardPanicName.JUST_BETWEEN_US: + pass + + # Revelaciones + if card.name == CardPanicName.REVELATIONS: + pass + + # Cuerdas podridas + if card.name == CardPanicName.ROTTEN_ROPES: + pass + + # Uno, dos... + if card.name == CardPanicName.ONE_TWO: + pass + + # Tres, cuatro + if card.name == CardPanicName.THREE_FOUR: + pass + + # ¿Es aquí la fiesta? + if card.name == CardPanicName.SO_THIS_IS_THE_PARTY: + pass + + # Ups + if card.name == CardPanicName.OOOPS: + pass + + # Olvidadizo + if card.name == CardPanicName.FORGETFUL: + pass + + # Vuelta y vuelta + if card.name == CardPanicName.ROUND_AND_ROUND: + pass + + # ¿No podemos ser amigos? + if card.name == CardPanicName.CANT_WE_BE_FRIENDS: + pass + + # Cita a ciegas + if card.name == CardPanicName.BLIND_DATE: + pass + + # ¡Sal de aquí! + if card.name == CardPanicName.GETOUT: + pass + + + return result + From b19cd0814892ccf54b924c0c8a25ce7a4986a98c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sun, 22 Oct 2023 22:34:58 -0300 Subject: [PATCH 080/224] Se modifico la tabla de jugadores para agregar el atributo isQuarentined, se implemento el proceso de 2 cartas de panico --- app/database/models.py | 1 + app/routers/cards/schemas.py | 2 +- app/routers/cards/utils.py | 2 +- app/routers/games/panic_functions.py | 38 ++++++++++++++++++++++++++++ app/routers/games/services.py | 25 +++++++++++++----- app/routers/games/utils.py | 1 + app/routers/players/utils.py | 8 ++++++ 7 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 app/routers/games/panic_functions.py diff --git a/app/database/models.py b/app/database/models.py index 96e6d1e..9e38578 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -9,6 +9,7 @@ class Player(db.Entity): game_hosting = Optional('Game', reverse='host', cascade_delete=True) name = Required(str) rol = Optional(str, nullable=True) + isQuatentined = Optional(bool, default=False) position = Required(int, default="-1") hand = Set('Card') diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 3125089..03722a4 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -41,7 +41,7 @@ class CardPanicName(str, Enum): ROUND_AND_ROUND = 'Vuelta y vuelta' CANT_WE_BE_FRIENDS = '¿No podemos ser amigos?' BLIND_DATE = 'Cita a ciegas' - GETOUT = '¡Sal de aquí!' + GETOUT_OF_HERE = '¡Sal de aquí!' class BaseCard(BaseModel): diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 1a1f5d6..4b2b670 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -23,7 +23,7 @@ def verify_action_card(card: Card): ) def verify_panic_card(card: Card): - if card.typej != CardType.PANIC: + if card.type != CardType.PANIC: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Card is not a PANIC card" diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py new file mode 100644 index 0000000..cd21e4b --- /dev/null +++ b/app/routers/games/panic_functions.py @@ -0,0 +1,38 @@ +from pony.orm import db_session +from app.database.models import Game, Card, Player +from ..players.schemas import PlayerRol +from ..cards.schemas import CardType, CardResponse +from ..websockets.utils import player_connections +from .utils import Events +from .schemas import RoundDirection +import random +import asyncio + + +async def send_player_between_us_event(to_player_id: int, player_id: int, player_name: str): + json_msg = { + "event": Events.BETWEEN_US_CARD_PLAYED, + "player_id": player_id, + "player_name": player_name + } + await player_connections.send_event_to(to_player_id, json_msg) + + +@db_session +def process_between_us_card(game: Game, player: Player, card: Card, objective_player: Player): + game.discard_deck.add(card) + player.hand.remove(card) + + asyncio.ensure_future(send_player_between_us_event( + objective_player.id, player.id, player.name)) + + +@db_session +def process_getout_of_here_card(game: Game, player: Player, card: Card, objective_player: Player): + # Intercambio de posiciones entre los jugadores + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition + + game.discard_deck.add(card) + player.hand.remove(card) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 6e7fca7..9474d31 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -7,10 +7,11 @@ from .utils import * from ..cards import services as cards_services from ..cards.utils import find_card_by_id, verify_action_card, verify_panic_card -from ..players.utils import find_player_by_id, verify_card_in_hand +from ..players.utils import find_player_by_id, verify_card_in_hand, verify_player_not_in_quarentine from ..cards.schemas import CardActionName, CardResponse, CardPanicName from ..players.schemas import PlayerRol from .action_functions import * +from .panic_functions import * import random from app.routers.games import utils @@ -424,6 +425,7 @@ def get_game_result(name: str) -> GameResult: losers=[PlayerInfo.model_validate(p) for p in losers] ) + @db_session def play_panic_card(game_name: str, play_info: PlayInformation): result = {"message": "Panic card played"} @@ -434,9 +436,18 @@ def play_panic_card(game_name: str, play_info: PlayInformation): verify_panic_card(card) verify_card_in_hand(player, card) + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() + # Que quede entre nosotros if card.name == CardPanicName.JUST_BETWEEN_US: - pass + verify_player_in_game(play_info.objective_player_id, game_name) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) + verify_adjacent_players(play_info.player_id, + play_info.objective_player_id, + players_not_eliminated - 1) + process_between_us_card(game, player, card) # Revelaciones if card.name == CardPanicName.REVELATIONS: @@ -479,9 +490,11 @@ def play_panic_card(game_name: str, play_info: PlayInformation): pass # ¡Sal de aquí! - if card.name == CardPanicName.GETOUT: - pass - + if card.name == CardPanicName.GETOUT_OF_HERE: + verify_player_in_game(play_info.objective_player_id, game_name) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) + verify_player_not_in_quarentine(objective_player) + process_getout_of_here_card(game, player, card, objective_player) return result - diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index eac16f2..30d3cea 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -24,6 +24,7 @@ class Events(str, Enum): WHISKEY_CARD_PLAYED = 'whiskey_card_played' PLAYER_DRAW_CARD = 'player_draw_card' NEW_TURN = 'new_turn' + BETWEEN_US_CARD_PLAYED = 'between_us_card_played' @db_session diff --git a/app/routers/players/utils.py b/app/routers/players/utils.py index bdca50b..abbe3b7 100644 --- a/app/routers/players/utils.py +++ b/app/routers/players/utils.py @@ -29,3 +29,11 @@ def verify_card_in_hand(player: Player, card: Card): def get_player_name_by_id(player_id: int) -> str: player = find_player_by_id(player_id) return player.name + +@db_session +def verify_player_not_in_quarentine(player: Player): + if player.isQuatentined: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player is in quarentined" + ) From f537083e2de7c29048989c7e095b2488658042cf Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 23 Oct 2023 02:46:44 -0300 Subject: [PATCH 081/224] Se agrega logica para cuando se juega la carta 'Vuelta y vuelta' --- app/routers/cards/schemas.py | 1 + app/routers/cards/utils.py | 4 ++- app/routers/games/games.py | 41 +++++++++++++++++++++---- app/routers/games/panic_functions.py | 15 +++++++++ app/routers/games/services.py | 12 +++++++- app/routers/games/utils.py | 46 ++++++++++++++++++++++++++++ app/routers/players/utils.py | 1 + 7 files changed, 112 insertions(+), 8 deletions(-) diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 03722a4..f9b3a4c 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -29,6 +29,7 @@ class CardActionName(str, Enum): BETTER_RUN = '¡Más vale que corras!' SEDUCTION = 'Seducción' + class CardPanicName(str, Enum): JUST_BETWEEN_US = 'Que quede entre nosotros...' REVELATIONS = 'Revelaciones' diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 4b2b670..787f21f 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -5,7 +5,7 @@ @db_session -def find_card_by_id(card_id: int): +def find_card_by_id(card_id: int) -> Card: card = Card.get(id=card_id) if not card: raise HTTPException( @@ -22,6 +22,7 @@ def verify_action_card(card: Card): detail="Card is not an ACTION card" ) + def verify_panic_card(card: Card): if card.type != CardType.PANIC: raise HTTPException( @@ -29,6 +30,7 @@ def verify_panic_card(card: Card): detail="Card is not a PANIC card" ) + @db_session def get_card_name_by_id(card_id: int) -> str: card = find_card_by_id(card_id) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index be3e02a..12d3a86 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -190,12 +190,27 @@ async def play_action_card(game_name: str, play_info: PlayInformation): @router.post("/{game_name}/play-panic-card", status_code=status.HTTP_200_OK) async def play_action_card(game_name: str, play_info: PlayInformation): result = services.play_panic_card(game_name, play_info) - json_msg = { - "event": utils.Events.PLAYED_CARD, - "player_name": get_player_name_by_id(play_info.player_id), - "card_name": get_card_name_by_id(play_info.card_id) - } - await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + if is_the_game_finished(game_name): + await finish_game(game_name) + else: + json_msg = { + "event": utils.Events.PLAYED_CARD, + "player_name": get_player_name_by_id(play_info.player_id), + "card_name": get_card_name_by_id(play_info.card_id) + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result @@ -216,3 +231,17 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): message=json_msg) return draw_card_information.card + + +@router.patch("/{game_name}/pass-card", status_code=status.HTTP_200_OK) +async def pass_card(game_name: str, play_info: PlayInformation): + result = {"message": "pass card completed"} + utils.verify_pass_card_can_be_done(game_name, play_info) + services.pass_card(play_info) + + json_msg = { + "event": utils.Events.ROUND_AND_ROUND_END + } + await player_connections.send_event_to(play_info.objective_player_id, json_msg) + + return result diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index cd21e4b..d9cb55c 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -18,6 +18,13 @@ async def send_player_between_us_event(to_player_id: int, player_id: int, player await player_connections.send_event_to(to_player_id, json_msg) +async def send_round_and_round_start_event(game_name: str): + json_msg = { + "event": Events.ROUND_AND_ROUND_START + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + @db_session def process_between_us_card(game: Game, player: Player, card: Card, objective_player: Player): game.discard_deck.add(card) @@ -27,6 +34,14 @@ def process_between_us_card(game: Game, player: Player, card: Card, objective_pl objective_player.id, player.id, player.name)) +@db_session +def process_round_and_round_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + + asyncio.ensure_future(send_round_and_round_start_event(game.name)) + + @db_session def process_getout_of_here_card(game: Game, player: Player, card: Card, objective_player: Player): # Intercambio de posiciones entre los jugadores diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 9474d31..0a86520 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -479,7 +479,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Vuelta y vuelta if card.name == CardPanicName.ROUND_AND_ROUND: - pass + process_round_and_round_card(game, player, card) # ¿No podemos ser amigos? if card.name == CardPanicName.CANT_WE_BE_FRIENDS: @@ -498,3 +498,13 @@ def play_panic_card(game_name: str, play_info: PlayInformation): process_getout_of_here_card(game, player, card, objective_player) return result + + +@db_session +def pass_card(play_info: PlayInformation): + player: Player = find_player_by_id(play_info.player_id) + objective_player: Player = find_player_by_id(play_info.objective_player_id) + card: Card = find_card_by_id(play_info.card_id) + + objective_player.hand.add(card) + player.hand.remove(card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 30d3cea..baf4aac 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -7,6 +7,7 @@ from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol from ..players.utils import find_player_by_id +from ..cards.utils import find_card_by_id class Events(str, Enum): @@ -25,6 +26,8 @@ class Events(str, Enum): PLAYER_DRAW_CARD = 'player_draw_card' NEW_TURN = 'new_turn' BETWEEN_US_CARD_PLAYED = 'between_us_card_played' + ROUND_AND_ROUND_START = 'round_and_round_start' + ROUND_AND_ROUND_END = 'round_and_round_end' @db_session @@ -334,6 +337,49 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): ) +@db_session +def verify_player_is_next_in_turn(game_name: str, player_id: int, other_player_id: int): + game = find_game_by_name(game_name) + player = find_player_by_id(player_id) + other_player = find_player_by_id(other_player_id) + + players_not_eliminated = select( + p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() + + if game.round_direction == RoundDirection.CLOCKWISE: + next_turn = (player.position - 1) % players_not_eliminated + else: + next_turn = (player.position + 1) % players_not_eliminated + + if next_turn != other_player.position: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The player selected is not the next in turn" + ) + + +@db_session +def verify_pass_card_can_be_done(game_name: str, play_info: PlayInformation): + player: Player = find_player_by_id(play_info.player_id) + card: Card = find_card_by_id(play_info.card_id) + verify_player_in_game(play_info.player_id, game_name) + verify_player_in_game(play_info.objective_player_id, game_name) + verify_player_is_next_in_turn( + play_info.player_id, play_info.objective_player_id) + if card.type == CardType.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The Thing cannot be passed" + ) + is_card_in_player = select( + c for c in player.hand if (c.id == card.id)).exists() + if not is_card_in_player: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The card is not in the hand of the player" + ) + + @db_session def is_the_game_finished(game_name: str) -> bool: game: Game = find_game_by_name(game_name) diff --git a/app/routers/players/utils.py b/app/routers/players/utils.py index abbe3b7..950eea6 100644 --- a/app/routers/players/utils.py +++ b/app/routers/players/utils.py @@ -30,6 +30,7 @@ def get_player_name_by_id(player_id: int) -> str: player = find_player_by_id(player_id) return player.name + @db_session def verify_player_not_in_quarentine(player: Player): if player.isQuatentined: From 604541968a9367f05230ed07dfc51e2459879d7f Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 23 Oct 2023 03:10:28 -0300 Subject: [PATCH 082/224] Se hace un cambio menor, donde se reemplaza codigo repetido por llamadas a funcion --- app/routers/games/services.py | 9 +++------ app/routers/games/utils.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 0a86520..5982b41 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -346,12 +346,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): process_seduction_card( game, player, card, objective_player, card_to_exchange) - players_playing = len( - list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction == RoundDirection.CLOCKWISE: - game.turn = (game.turn - 1) % players_playing - else: - game.turn = (game.turn + 1) % players_playing + update_game_turn(game_name) return result @@ -497,6 +492,8 @@ def play_panic_card(game_name: str, play_info: PlayInformation): verify_player_not_in_quarentine(objective_player) process_getout_of_here_card(game, player, card, objective_player) + utils.update_game_turn(game_name) + return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index baf4aac..722d433 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -388,3 +388,14 @@ def is_the_game_finished(game_name: str) -> bool: return True except: return False + + +@db_session +def update_game_turn(game_name: str): + game: Game = find_game_by_name(game_name) + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + game.turn = (game.turn - 1) % players_playing + else: + game.turn = (game.turn + 1) % players_playing From a03504779d8ce670759a10608597c08c4096b0a1 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 23 Oct 2023 15:35:05 -0300 Subject: [PATCH 083/224] Se modifico el evento de discard card para que envie el tipo de carta que se descarto --- app/routers/cards/utils.py | 6 ++++++ app/routers/games/games.py | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 2d678d8..9889424 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -27,3 +27,9 @@ def verify_action_card(card: Card): def get_card_name_by_id(card_id: int) -> str: card = find_card_by_id(card_id) return card.name + + +@db_session +def get_card_type_by_id(card_id: int) -> str: + card: Card = find_card_by_id(card_id) + return card.type diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 03b85e4..701d751 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -7,7 +7,7 @@ from ..websockets.utils import player_connections from .utils import find_game_by_name, is_the_game_finished from ..players.utils import get_player_name_by_id -from ..cards.utils import get_card_name_by_id +from ..cards.utils import get_card_name_by_id, get_card_type_by_id from .services import finish_game @@ -152,8 +152,7 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): json_msg = { "event": "discard_card", "player_name": get_player_name_by_id(game_data.player_id), - "card_id": game_data.card_id, - "card_name": get_card_name_by_id(game_data.card_id) + "card_type": get_card_type_by_id(game_data.card_id) } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) From c97e84bb1c8d1015f53118fcfee692d1ff2cc7d0 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 23 Oct 2023 16:29:14 -0300 Subject: [PATCH 084/224] se remueve los comentarios de la verificacion status!=ENDED --- app/routers/games/services.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index dbd410b..c09b8f9 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -387,11 +387,11 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu def get_game_result(name: str) -> GameResult: game: Game = find_game_by_name(name) - # if game.status != GameStatus.ENDED: - # raise HTTPException( - # status_code=status.HTTP_400_BAD_REQUEST, - # detail=f"The game is not ended." - # ) + if game.status != GameStatus.ENDED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The game is not ended." + ) reason = "" winners = [] From ca911170bb670e6091a741997c89ad890e13fd0c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 23 Oct 2023 20:39:19 -0300 Subject: [PATCH 085/224] Intento de fix provisorio --- app/routers/games/services.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 83e01b7..d120b37 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -278,6 +278,11 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: play_info.objective_player_id, players_not_eliminated - 1) objective_player = find_player_by_id(play_info.objective_player_id) + + # fix provisorio + if player.position > objective_player.position: + game.turn = objective_player.position + process_flamethrower_card(game, player, card, objective_player) # Analisis From b786afb421c6c2ca2c1aca98dc1ef11e1a4ef450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ezequiel=20Ludue=C3=B1a?= Date: Tue, 24 Oct 2023 03:04:33 -0300 Subject: [PATCH 086/224] Cambio en PLAYE_CARD y PLAYER_ELIMINATED --- app/routers/games/action_functions.py | 8 +++++++- app/routers/games/games.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 14adfe1..a3c55bd 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -10,10 +10,16 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminated_name: str): + players_positions = {} + for p in game.players: + if p.rol != PlayerRol.ELIMINATED: + players_positions[p.id] = p.position + json_msg = { "event": Events.PLAYER_ELIMINATED, "player_id": eliminated_id, - "player_name": eliminated_name + "player_name": eliminated_name, + "players_positions": players_positions } for p in game.players: await player_connections.send_event_to(p.id, json_msg) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 701d751..001e088 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -177,7 +177,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): json_msg = { "event": utils.Events.PLAYED_CARD, "player_name": get_player_name_by_id(play_info.player_id), - "card_name": get_card_name_by_id(play_info.card_id) + "card_id": play_info.card_id } await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) From e20b50b3756ed85356c7e37b0e8a77a64d06a73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ezequiel=20Ludue=C3=B1a?= Date: Tue, 24 Oct 2023 04:45:27 -0300 Subject: [PATCH 087/224] cambio de orden --- app/routers/games/services.py | 6 +++--- app/routers/games/utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index d120b37..13d0914 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -231,7 +231,7 @@ def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: players_playing = len( list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction == RoundDirection.CLOCKWISE: + if game.round_direction != RoundDirection.CLOCKWISE: game.turn = (game.turn - 1) % players_playing else: game.turn = (game.turn + 1) % players_playing @@ -352,7 +352,7 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: players_playing = len( list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction == RoundDirection.CLOCKWISE: + if game.round_direction != RoundDirection.CLOCKWISE: game.turn = (game.turn - 1) % players_playing else: game.turn = (game.turn + 1) % players_playing @@ -446,7 +446,7 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI players_playing = len( list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction == RoundDirection.CLOCKWISE: + if game.round_direction != RoundDirection.CLOCKWISE: game.turn = (game.turn - 1) % players_playing else: game.turn = (game.turn + 1) % players_playing diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b3c9784..673ce54 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -422,7 +422,7 @@ def get_id_of_next_player_in_turn(game_name): game: Game = find_game_by_name(game_name) players_playing = len( list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction == RoundDirection.CLOCKWISE: + if game.round_direction != RoundDirection.CLOCKWISE: next_turn = (game.turn - 1) % players_playing else: next_turn = (game.turn + 1) % players_playing From 0b3256036c9d56a9e95e5122be3f80d2a275e6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ezequiel=20Ludue=C3=B1a?= Date: Tue, 24 Oct 2023 05:11:08 -0300 Subject: [PATCH 088/224] hotfix ws --- app/routers/games/games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 001e088..69f35f1 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -179,7 +179,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): "player_name": get_player_name_by_id(play_info.player_id), "card_id": play_info.card_id } - await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + await player_connections.send_event_to_all_players_in_game(game_name,json_msg) with db_session: game = find_game_by_name(game_name) From cdf1b58c15d3a1f749e18255dfa2d53d78c745d7 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 24 Oct 2023 14:47:09 -0300 Subject: [PATCH 089/224] Refactorizacion de codigo --- app/routers/games/services.py | 48 ++++++++++++----------------------- app/routers/games/utils.py | 21 +++++++++++---- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 13d0914..2fe603c 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -229,12 +229,7 @@ def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: if game and card: game.discard_deck.add(card) - players_playing = len( - list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction != RoundDirection.CLOCKWISE: - game.turn = (game.turn - 1) % players_playing - else: - game.turn = (game.turn + 1) % players_playing + update_game_turn(game_name) return game @@ -261,10 +256,10 @@ async def finish_game(name: str) -> Game: @db_session def play_action_card(game_name: str, play_info: PlayInformation) -> Game: result = {"message": "Action card played"} - game = find_game_by_name(game_name) + game: Game = find_game_by_name(game_name) verify_player_in_game(play_info.player_id, game_name) - player = find_player_by_id(play_info.player_id) - card = find_card_by_id(play_info.card_id) + player: Player = find_player_by_id(play_info.player_id) + card: Card = find_card_by_id(play_info.card_id) verify_action_card(card) verify_card_in_hand(player, card) @@ -277,11 +272,7 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player = find_player_by_id(play_info.objective_player_id) - - # fix provisorio - if player.position > objective_player.position: - game.turn = objective_player.position + objective_player: Player = find_player_by_id(play_info.objective_player_id) process_flamethrower_card(game, player, card, objective_player) @@ -291,8 +282,11 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id(play_info.objective_player_id) + if game.turn != 0 and objective_player.position < player.position: + game.turn = game.turn - 1 + # Armo listado de cartas del jugador objetivo para enviar en el body response result = process_analysis_card(game, player, card, objective_player) @@ -306,7 +300,7 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id(play_info.objective_player_id) result = process_suspicious_card(game, player, card, objective_player) # Whisky @@ -327,20 +321,20 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id(play_info.objective_player_id) process_change_places_card(game, player, card, objective_player) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) - objective_player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id(play_info.objective_player_id) process_better_run_card(game, player, card, objective_player) # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) if card.name == CardActionName.SEDUCTION: verify_player_in_game(play_info.objective_player_id, game_name) - objective_player = find_player_by_id(play_info.objective_player_id) - card_to_exchange = find_card_by_id(play_info.card_to_exchange) + objective_player: Player = find_player_by_id(play_info.objective_player_id) + card_to_exchange: Card = find_card_by_id(play_info.card_to_exchange) if card_to_exchange.type == CardType.THE_THING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -350,12 +344,7 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: process_seduction_card( game, player, card, objective_player, card_to_exchange) - players_playing = len( - list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction != RoundDirection.CLOCKWISE: - game.turn = (game.turn - 1) % players_playing - else: - game.turn = (game.turn + 1) % players_playing + update_game_turn(game_name) return result @@ -444,9 +433,4 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI player.hand.add(next_player_card) next_player.hand.add(player_card) - players_playing = len( - list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction != RoundDirection.CLOCKWISE: - game.turn = (game.turn - 1) % players_playing - else: - game.turn = (game.turn + 1) % players_playing + update_game_turn(game_name) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 673ce54..2f433bb 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -249,8 +249,8 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): @db_session def verify_player_in_game(player_id: int, game_name: str): - player = Player.get(id=player_id) - game = Game.get(name=game_name) + player: Player = find_player_by_id(player_id) + game: Game = find_game_by_name(game_name) if player and game: if player in game.players: pass @@ -422,10 +422,10 @@ def get_id_of_next_player_in_turn(game_name): game: Game = find_game_by_name(game_name) players_playing = len( list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) - if game.round_direction != RoundDirection.CLOCKWISE: - next_turn = (game.turn - 1) % players_playing - else: + if game.round_direction == RoundDirection.CLOCKWISE: next_turn = (game.turn + 1) % players_playing + else: + next_turn = (game.turn - 1) % players_playing next_player_id = select( p.id for p in game.players if p.position == next_turn).first() @@ -438,4 +438,15 @@ def is_the_game_finished(game_name: str) -> bool: return True except: return False + + +@db_session +def update_game_turn(game_name: str): + game: Game = find_game_by_name(game_name) + players_playing = len( + list(select(p for p in game.players if p.rol != PlayerRol.ELIMINATED))) + if game.round_direction == RoundDirection.CLOCKWISE: + game.turn = (game.turn + 1) % players_playing + else: + game.turn = (game.turn - 1) % players_playing From 96b09288c622545c32adbc0e0ea656350e9a90b6 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 24 Oct 2023 15:36:36 -0300 Subject: [PATCH 090/224] Se modifica codigo que estaba puesto cuando se jugaba una carta de analisis, y se lo pone en la parte del lanzallamas --- app/routers/games/services.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 2fe603c..b739f7d 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -274,6 +274,9 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: players_not_eliminated - 1) objective_player: Player = find_player_by_id(play_info.objective_player_id) + if game.turn != 0 and objective_player.position < player.position: + game.turn = game.turn - 1 + process_flamethrower_card(game, player, card, objective_player) # Analisis @@ -283,9 +286,6 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: play_info.objective_player_id, players_not_eliminated - 1) objective_player: Player = find_player_by_id(play_info.objective_player_id) - - if game.turn != 0 and objective_player.position < player.position: - game.turn = game.turn - 1 # Armo listado de cartas del jugador objetivo para enviar en el body response result = process_analysis_card(game, player, card, objective_player) From 13b73fc86aba68fa9deabee73ad4af8325bef166 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 24 Oct 2023 15:39:21 -0300 Subject: [PATCH 091/224] Cambio menor --- app/routers/games/services.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 9dd846e..3d2ad1a 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -275,6 +275,9 @@ def play_action_card(game_name: str, play_info: PlayInformation): players_not_eliminated - 1) objective_player: Player = find_player_by_id( play_info.objective_player_id) + + if game.turn != 0 and objective_player.position < player.position: + game.turn = game.turn - 1 process_flamethrower_card(game, player, card, objective_player) @@ -287,9 +290,6 @@ def play_action_card(game_name: str, play_info: PlayInformation): objective_player: Player = find_player_by_id( play_info.objective_player_id) - if game.turn != 0 and objective_player.position < player.position: - game.turn = game.turn - 1 - # Armo listado de cartas del jugador objetivo para enviar en el body response result = process_analysis_card(game, player, card, objective_player) From e585639f7723c942d70f0df11619da99d8e4c3c5 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 25 Oct 2023 23:18:55 -0300 Subject: [PATCH 092/224] Se agrega funcionalidad para la carta Revelaciones --- app/routers/games/games.py | 15 ++++++++++++++- app/routers/games/panic_functions.py | 19 ++++++++++++++++++- app/routers/games/schemas.py | 7 +++++++ app/routers/games/services.py | 20 ++++++++++++++++++-- app/routers/games/utils.py | 5 +++++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4d56bca..8247e83 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -275,7 +275,7 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) json_msg = { - "event": "exchange_done" + "event": utils.Events.EXCHANGE_DONE } await player_connections.send_event_to(game_data.player_id, json_msg) await player_connections.send_event_to(game_data.objective_player_id, json_msg) @@ -293,3 +293,16 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return {"message": "Card interchange terminated."} + + +@router.post("/{game_name}/show_revelations_cards/{player_id}", status_code=status.HTTP_200_OK) +async def show_revelations_cards(game_name: str, player_id: int, game_data: ShowRevelationsCardsIn): + utils.verify_player_in_game(player_id, game_name) + utils.verify_player_in_game(game_data.original_player_id, game_name) + services.show_cards(game_name, player_id, game_data) + if player_id == game_data.original_player_id: + json_msg = { + "event": utils.Events.REVELATIONS_DONE + } + await player_connections.send_event_to(player_id, json_msg) + return {"message": "Show card terminated."} diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index d9cb55c..49a8fea 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -3,7 +3,7 @@ from ..players.schemas import PlayerRol from ..cards.schemas import CardType, CardResponse from ..websockets.utils import player_connections -from .utils import Events +from .utils import Events, get_id_of_next_player_in_turn from .schemas import RoundDirection import random import asyncio @@ -25,6 +25,23 @@ async def send_round_and_round_start_event(game_name: str): await player_connections.send_event_to_all_players_in_game(game_name, json_msg) +async def send_revelations_card_played_event(game_name: str, original_player_id: int, next_player_id: int): + json_msg = { + "event": Events.REVELATIONS_CARD_PLAYED, + "original_player_id": original_player_id + } + await player_connections.send_event_to(next_player_id, json_msg) + + +@db_session +def process_revelations_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + next_player_id = get_id_of_next_player_in_turn(game.name) + asyncio.ensure_future(send_revelations_card_played_event( + game.name, player.id, next_player_id)) + + @db_session def process_between_us_card(game: Game, player: Player, card: Card, objective_player: Player): game.discard_deck.add(card) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index e31bed8..00e3b38 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -153,3 +153,10 @@ class InterchangeInformationIn(BaseModel): card_id: int # Card ID del jugador que recibe la intencion objective_player_id: int # ID jugador que inicia la intencion objective_card_id: int # Card ID del jugador que inicia la intencion + + +class ShowRevelationsCardsIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + original_player_id: int # ID del jugador que jugo la carta Revelaciones + show_my_cards: bool diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 3d2ad1a..6c8975f 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -275,7 +275,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): players_not_eliminated - 1) objective_player: Player = find_player_by_id( play_info.objective_player_id) - + if game.turn != 0 and objective_player.position < player.position: game.turn = game.turn - 1 @@ -451,7 +451,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Revelaciones if card.name == CardPanicName.REVELATIONS: - pass + process_revelations_card(game, player, card) # Cuerdas podridas if card.name == CardPanicName.ROTTEN_ROPES: @@ -528,3 +528,19 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI next_player.hand.add(player_card) update_game_turn(game_name) + + +async def show_revelations_cards(game_name: str, player_id: int, game_data: ShowRevelationsCardsIn): + if game_data.show_my_cards: + json_msg = { + "event": utils.Events.REVELATIONS_SHOW, + "player_id": player_id + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, player_id) + if player_id != game_data.original_player_id: + next_player_id = get_id_of_next_player_in_turn(game_name) + json_msg = { + "event": Events.REVELATIONS_CARD_PLAYED, + "original_player_id": game_data.original_player_id + } + await player_connections.send_event_to(next_player_id, json_msg) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 4e63c25..05ec8f1 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -29,6 +29,11 @@ class Events(str, Enum): BETWEEN_US_CARD_PLAYED = 'between_us_card_played' ROUND_AND_ROUND_START = 'round_and_round_start' ROUND_AND_ROUND_END = 'round_and_round_end' + REVELATIONS_CARD_PLAYED = 'revelations_card_played' + REVELATIONS_SHOW = 'revelations_show' + REVELATIONS_DONE = 'revelations_done' + EXCHANGE_INTENTION = 'exchange_intention' + EXCHANGE_DONE = 'exchange_done' @db_session From 58eb81d30655329dc569eedc0893aac7695867c5 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 16:05:54 -0300 Subject: [PATCH 093/224] Se hace que se mande el evento de nuevo turno despues del evento de jugador eliminado --- app/routers/games/action_functions.py | 14 +++++++++++++- app/routers/games/games.py | 23 ++++++++++++----------- app/routers/games/utils.py | 10 +++++++++- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index a3c55bd..b0e757b 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -1,10 +1,11 @@ -from pony.orm import db_session +from pony.orm import db_session, select from app.database.models import Game, Card, Player from ..players.schemas import PlayerRol from ..cards.schemas import CardType, CardResponse from ..websockets.utils import player_connections from .utils import Events from .schemas import RoundDirection +from ..players.utils import get_player_name_by_id import random import asyncio @@ -23,6 +24,17 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat } for p in game.players: await player_connections.send_event_to(p.id, json_msg) + + with db_session: + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game.name, json_msg) async def send_players_whiskey_event(game: Game, player_id: int, player_name: str): diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 69f35f1..8ca8271 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -181,17 +181,18 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_all_players_in_game(game_name,json_msg) - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + if not utils.is_flamethrower(play_info.card_id): + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 2f433bb..8c701a9 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -7,7 +7,7 @@ from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol from ..players.utils import find_player_by_id -from ..cards.schemas import CardType, CardSubtype +from ..cards.schemas import CardType, CardSubtype, CardActionName class Events(str, Enum): @@ -450,3 +450,11 @@ def update_game_turn(game_name: str): else: game.turn = (game.turn - 1) % players_playing + +@db_session +def is_flamethrower(card_id: int): + card: Card = Card.get(card_id) + if(card.name == CardActionName.FLAMETHROWER): + return True + else: + return False From ca420aae14c7d3225cad9a0ef100c65aa491c6b4 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 16:17:33 -0300 Subject: [PATCH 094/224] Codigo con 2 new_turn al jugar carta lanzallamas --- app/routers/games/games.py | 23 +++++++++++------------ app/routers/games/utils.py | 13 ++----------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 8ca8271..69f35f1 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -181,18 +181,17 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_all_players_in_game(game_name,json_msg) - if not utils.is_flamethrower(play_info.card_id): - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 8c701a9..140af72 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -7,7 +7,7 @@ from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol from ..players.utils import find_player_by_id -from ..cards.schemas import CardType, CardSubtype, CardActionName +from ..cards.schemas import CardType, CardSubtype class Events(str, Enum): @@ -448,13 +448,4 @@ def update_game_turn(game_name: str): if game.round_direction == RoundDirection.CLOCKWISE: game.turn = (game.turn + 1) % players_playing else: - game.turn = (game.turn - 1) % players_playing - - -@db_session -def is_flamethrower(card_id: int): - card: Card = Card.get(card_id) - if(card.name == CardActionName.FLAMETHROWER): - return True - else: - return False + game.turn = (game.turn - 1) % players_playing \ No newline at end of file From ab77a2b5d410040824f3d4c4742fc29361d86daf Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 16:41:07 -0300 Subject: [PATCH 095/224] Se agrega sleep de 4 segs --- app/routers/games/action_functions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index b0e757b..b4e3ba6 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -24,6 +24,9 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat } for p in game.players: await player_connections.send_event_to(p.id, json_msg) + + # Espera 4 segundos antes de enviar el siguiente evento + await asyncio.sleep(4) with db_session: player_id_turn = select( From 5b6c3e2c06ef05a906818e3da795e018443f6210 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 17:32:50 -0300 Subject: [PATCH 096/224] Solo se manda un evento de new_turn al jugar la carta lanzallamas --- app/routers/cards/utils.py | 7 ++++++- app/routers/games/games.py | 25 +++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 9889424..055d8e5 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -1,7 +1,7 @@ from pony.orm import db_session from fastapi import HTTPException, status from app.database.models import Card -from ..cards.schemas import CardSubtype +from ..cards.schemas import CardSubtype, CardActionName @db_session @@ -33,3 +33,8 @@ def get_card_name_by_id(card_id: int) -> str: def get_card_type_by_id(card_id: int) -> str: card: Card = find_card_by_id(card_id) return card.type + +@db_session +def is_flamethrower(card_id: int) -> bool: + card: Card = find_card_by_id(card_id) + return (card.name == CardActionName.FLAMETHROWER) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 69f35f1..336e43b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -7,7 +7,7 @@ from ..websockets.utils import player_connections from .utils import find_game_by_name, is_the_game_finished from ..players.utils import get_player_name_by_id -from ..cards.utils import get_card_name_by_id, get_card_type_by_id +from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower from .services import finish_game @@ -181,17 +181,18 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_all_players_in_game(game_name,json_msg) - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + if not is_flamethrower(play_info.card_id): + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result From 40f6280fdb6ccc2f0d4d61fb810d3dc8e5bc1746 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 18:55:27 -0300 Subject: [PATCH 097/224] Primera version del cheat lanzallamas --- app/routers/games/action_functions.py | 2 +- app/routers/websockets/services.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index b4e3ba6..6aaa712 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -4,7 +4,7 @@ from ..cards.schemas import CardType, CardResponse from ..websockets.utils import player_connections from .utils import Events -from .schemas import RoundDirection +from .schemas import RoundDirection, GameStatus from ..players.utils import get_player_name_by_id import random import asyncio diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index ed0215b..0a9090a 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,7 +1,9 @@ -from pony.orm import db_session +from pony.orm import db_session, select from app.database.models import Game, Player from fastapi import WebSocket, WebSocketDisconnect from .utils import player_connections, get_players_id +from ..cards.schemas import CardType +import random async def websocket_games(player_id: int, websocket: WebSocket): @@ -17,5 +19,24 @@ async def websocket_games(player_id: int, websocket: WebSocket): for i in players: if i != player_id: await player_connections.send_message(player_id=i, message_from=message_from, message=message) + + if message == 'lz': + with db_session: + player: Player = select(p for p in Player if p.name == message_from).first() + game: Game = select(g for g in Game if g.name == data["game_name"]).first() + player_hand_list = list(player.hand) + elegible_cards = [ + c for c in player_hand_list if c.type != CardType.THE_THING] + random_card = random.choice(elegible_cards) + flamethrower_card = select(c for c in game.discard_deck if 22 <= c.id and c.id <= 26).first() + if flamethrower_card: + player.hand.remove(random_card) + player.hand.add(flamethrower_card) + game.discard_deck.add(random_card) + game.discard_deck.remove(flamethrower_card) + json_msg = { + "event": "cheat_flamethrower" + } + await player_connections.send_event_to(player.id, json_msg) except WebSocketDisconnect: player_connections.disconnect(player_id) From e5ba3e506ddd516f6beb023c8db378a8f5e3958e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 21:10:43 -0300 Subject: [PATCH 098/224] Se habilita para que el evento de nuevo turno se mande en la etapa del intercambio --- app/routers/cards/utils.py | 1 + app/routers/games/action_functions.py | 14 -------------- app/routers/games/games.py | 24 ------------------------ app/routers/games/services.py | 3 ++- 4 files changed, 3 insertions(+), 39 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 5ecc250..23f021a 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -42,6 +42,7 @@ def get_card_type_by_id(card_id: int) -> str: card: Card = find_card_by_id(card_id) return card.type + @db_session def is_flamethrower(card_id: int) -> bool: card: Card = find_card_by_id(card_id) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index b4e3ba6..c17f0d9 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -25,20 +25,6 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat for p in game.players: await player_connections.send_event_to(p.id, json_msg) - # Espera 4 segundos antes de enviar el siguiente evento - await asyncio.sleep(4) - - with db_session: - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game.name, json_msg) - async def send_players_whiskey_event(game: Game, player_id: int, player_name: str): json_msg = { diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 992c29d..9ba4000 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -181,19 +181,6 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - if not is_flamethrower(play_info.card_id): - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - return result @@ -210,17 +197,6 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) - with db_session: - game = find_game_by_name(game_name) - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return result diff --git a/app/routers/games/services.py b/app/routers/games/services.py index f4070ad..46c7a26 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -287,7 +287,8 @@ def play_action_card(game_name: str, play_info: PlayInformation): verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) # Armo listado de cartas del jugador objetivo para enviar en el body response result = process_analysis_card(game, player, card, objective_player) From 1ec6b9c781c196a3e246d7faaea74a8c62e5bfd8 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Thu, 26 Oct 2023 22:15:29 -0300 Subject: [PATCH 099/224] Se mejora el cheat para el lanzallamas, ahora busca en mazo de robo como de descarte --- app/routers/cards/utils.py | 1 + app/routers/games/action_functions.py | 2 +- app/routers/games/games.py | 11 +++--- app/routers/games/schemas.py | 15 ++++---- app/routers/games/services.py | 21 +++++++---- app/routers/games/utils.py | 7 ++-- app/routers/websockets/services.py | 50 +++++++++++---------------- app/routers/websockets/utils.py | 36 +++++++++++++++++-- 8 files changed, 86 insertions(+), 57 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 055d8e5..5318f83 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -34,6 +34,7 @@ def get_card_type_by_id(card_id: int) -> str: card: Card = find_card_by_id(card_id) return card.type + @db_session def is_flamethrower(card_id: int) -> bool: card: Card = find_card_by_id(card_id) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 6aaa712..79a058b 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -27,7 +27,7 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat # Espera 4 segundos antes de enviar el siguiente evento await asyncio.sleep(4) - + with db_session: player_id_turn = select( p for p in game.players if p.position == game.turn).first().id diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 336e43b..4a4cb41 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -148,14 +148,14 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): with db_session: player_id_turn = select( p for p in game.players if p.position == game.turn).first().id - + json_msg = { "event": "discard_card", "player_name": get_player_name_by_id(game_data.player_id), "card_type": get_card_type_by_id(game_data.card_id) } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - + json_msg = { "event": utils.Events.NEW_TURN, "next_player_name": get_player_name_by_id(player_id_turn), @@ -163,7 +163,7 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): "round_direction": game.round_direction } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - + return {"message": "Card discarded"} @@ -179,7 +179,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): "player_name": get_player_name_by_id(play_info.player_id), "card_id": play_info.card_id } - await player_connections.send_event_to_all_players_in_game(game_name,json_msg) + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) if not is_flamethrower(play_info.card_id): with db_session: @@ -239,7 +239,6 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform } await player_connections.send_event_to(game_data.player_id, json_msg) await player_connections.send_event_to(game_data.objective_player_id, json_msg) - with db_session: game = find_game_by_name(game_name) @@ -252,5 +251,5 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform "round_direction": game.round_direction } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - + return {"message": "Card interchange terminated."} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index eadffb5..e31bed8 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -138,17 +138,18 @@ class GameResult(BaseModel): winners: List[PlayerInfo] losers: List[PlayerInfo] + class IntentionExchangeInformationIn(BaseModel): model_config = ConfigDict(from_attributes=True) - player_id: int # ID jugador que inicia la intencion - card_id: int # Card ID del jugador que inicia la intencion + player_id: int # ID jugador que inicia la intencion + card_id: int # Card ID del jugador que inicia la intencion + class InterchangeInformationIn(BaseModel): model_config = ConfigDict(from_attributes=True) - player_id: int # ID jugador que recibe la intencion - card_id: int # Card ID del jugador que recibe la intencion - objective_player_id: int # ID jugador que inicia la intencion - objective_card_id: int # Card ID del jugador que inicia la intencion - + player_id: int # ID jugador que recibe la intencion + card_id: int # Card ID del jugador que recibe la intencion + objective_player_id: int # ID jugador que inicia la intencion + objective_card_id: int # Card ID del jugador que inicia la intencion diff --git a/app/routers/games/services.py b/app/routers/games/services.py index b739f7d..629e1f0 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -272,7 +272,8 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) if game.turn != 0 and objective_player.position < player.position: game.turn = game.turn - 1 @@ -285,8 +286,9 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) - + objective_player: Player = find_player_by_id( + play_info.objective_player_id) + # Armo listado de cartas del jugador objetivo para enviar en el body response result = process_analysis_card(game, player, card, objective_player) @@ -300,7 +302,8 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) result = process_suspicious_card(game, player, card, objective_player) # Whisky @@ -321,19 +324,22 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) process_change_places_card(game, player, card, objective_player) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) process_better_run_card(game, player, card, objective_player) # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) if card.name == CardActionName.SEDUCTION: verify_player_in_game(play_info.objective_player_id, game_name) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) card_to_exchange: Card = find_card_by_id(play_info.card_to_exchange) if card_to_exchange.type == CardType.THE_THING: raise HTTPException( @@ -418,6 +424,7 @@ def get_game_result(name: str) -> GameResult: losers=[PlayerInfo.model_validate(p) for p in losers] ) + @db_session def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): game: Game = find_game_by_name(game_name) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 140af72..daf203f 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -360,7 +360,7 @@ def verify_if_interchange_can_be_done(game_name: str, interchange_info: Intentio status_code=status.HTTP_400_BAD_REQUEST, detail='The player cannot pass the card Infected because is not The Thing' ) - + @db_session def verify_if_interchange_response_can_be_done(game_name: str, game_data: InterchangeInformationIn): @@ -431,6 +431,7 @@ def get_id_of_next_player_in_turn(game_name): p.id for p in game.players if p.position == next_turn).first() return next_player_id + def is_the_game_finished(game_name: str) -> bool: game: Game = find_game_by_name(game_name) try: @@ -438,7 +439,7 @@ def is_the_game_finished(game_name: str) -> bool: return True except: return False - + @db_session def update_game_turn(game_name: str): @@ -448,4 +449,4 @@ def update_game_turn(game_name: str): if game.round_direction == RoundDirection.CLOCKWISE: game.turn = (game.turn + 1) % players_playing else: - game.turn = (game.turn - 1) % players_playing \ No newline at end of file + game.turn = (game.turn - 1) % players_playing diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 0a9090a..c60f769 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,9 +1,23 @@ -from pony.orm import db_session, select -from app.database.models import Game, Player from fastapi import WebSocket, WebSocketDisconnect -from .utils import player_connections, get_players_id -from ..cards.schemas import CardType -import random +from .utils import player_connections, get_players_id, flamethrower_cheat + + +async def handle_message(data, player_id): + message_from = data["from"] + message = data["message"] + game_name = data["game_name"] + players = get_players_id(game_name) + + for i in players: + if i != player_id: + await player_connections.send_message(player_id=i, message_from=message_from, message=message) + + if message == 'lz': + flamethrower_cheat(game_name, player_id) + json_msg = { + "event": "cheat_flamethrower" + } + await player_connections.send_event_to(player_id, json_msg) async def websocket_games(player_id: int, websocket: WebSocket): @@ -13,30 +27,6 @@ async def websocket_games(player_id: int, websocket: WebSocket): while True: data = await websocket.receive_json() if (data["event"] == "message"): - message_from = data["from"] - message = data["message"] - players = get_players_id(data["game_name"]) - for i in players: - if i != player_id: - await player_connections.send_message(player_id=i, message_from=message_from, message=message) - - if message == 'lz': - with db_session: - player: Player = select(p for p in Player if p.name == message_from).first() - game: Game = select(g for g in Game if g.name == data["game_name"]).first() - player_hand_list = list(player.hand) - elegible_cards = [ - c for c in player_hand_list if c.type != CardType.THE_THING] - random_card = random.choice(elegible_cards) - flamethrower_card = select(c for c in game.discard_deck if 22 <= c.id and c.id <= 26).first() - if flamethrower_card: - player.hand.remove(random_card) - player.hand.add(flamethrower_card) - game.discard_deck.add(random_card) - game.discard_deck.remove(flamethrower_card) - json_msg = { - "event": "cheat_flamethrower" - } - await player_connections.send_event_to(player.id, json_msg) + await handle_message(data, player_id) except WebSocketDisconnect: player_connections.disconnect(player_id) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 61267c8..734870f 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -1,9 +1,12 @@ from fastapi import WebSocket -from pony.orm import db_session -from app.database.models import Player +from pony.orm import db_session, select +from app.database.models import Player, Game from ..games import services as games_services +from ..games import utils as games_utils +from ..players.utils import find_player_by_id +from ..cards.schemas import CardType from typing import Dict, List -import json +import random @db_session @@ -16,6 +19,33 @@ def get_players_id(game_name: str) -> List[Player]: return result +@db_session +def flamethrower_cheat(game_name: str, player_id: int): + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + games_utils.verify_player_in_game(player_id, game_name) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + flamethrower_card = select( + c for c in game.draw_deck if 22 <= c.id and c.id <= 26).first() + if flamethrower_card: + game.draw_deck.remove(flamethrower_card) + game.draw_deck_order.remove(flamethrower_card.id) + + else: + flamethrower_card = select( + c for c in game.discard_deck if 22 <= c.id and c.id <= 26).first() + if flamethrower_card: + game.discard_deck.remove(flamethrower_card) + + if flamethrower_card and random_card: + player.hand.remove(random_card) + player.hand.add(flamethrower_card) + + class ConnectionManager: def __init__(self): self.active_connections: Dict[int, WebSocket] = {} From 987c2f8bab4a967d75ad10a7f66682e964339dda Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 11:20:15 -0300 Subject: [PATCH 100/224] Se agrega cheat para obtener carta whiskey --- app/routers/websockets/services.py | 19 +++++++++++++------ app/routers/websockets/utils.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index c60f769..05ccdc0 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,5 +1,12 @@ from fastapi import WebSocket, WebSocketDisconnect -from .utils import player_connections, get_players_id, flamethrower_cheat +from .utils import * + + +async def send_event_cheat_used(player_id: int): + json_msg = { + "event": "cheat_used" + } + await player_connections.send_event_to(player_id, json_msg) async def handle_message(data, player_id): @@ -12,12 +19,12 @@ async def handle_message(data, player_id): if i != player_id: await player_connections.send_message(player_id=i, message_from=message_from, message=message) - if message == 'lz': + if message == 'lz' or message == 'lanzallamas': flamethrower_cheat(game_name, player_id) - json_msg = { - "event": "cheat_flamethrower" - } - await player_connections.send_event_to(player_id, json_msg) + await send_event_cheat_used(player_id) + if message == 'ws' or message == 'whisky' or message == 'whiskey': + whiskey_cheat(game_name, player_id) + await send_event_cheat_used(player_id) async def websocket_games(player_id: int, websocket: WebSocket): diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 734870f..9e1c9dd 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -46,6 +46,33 @@ def flamethrower_cheat(game_name: str, player_id: int): player.hand.add(flamethrower_card) +@db_session +def whiskey_cheat(game_name: str, player_id: int): + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + games_utils.verify_player_in_game(player_id, game_name) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + whiskey_card = select( + c for c in game.draw_deck if 40 <= c.id and c.id <= 42).first() + if whiskey_card: + game.draw_deck.remove(whiskey_card) + game.draw_deck_order.remove(whiskey_card.id) + + else: + whiskey_card = select( + c for c in game.discard_deck if 40 <= c.id and c.id <= 42).first() + if whiskey_card: + game.discard_deck.remove(whiskey_card) + + if whiskey_card and random_card: + player.hand.remove(random_card) + player.hand.add(whiskey_card) + + class ConnectionManager: def __init__(self): self.active_connections: Dict[int, WebSocket] = {} From 4f41b34f8f72289ac456a2e1b6f082b889c2ee36 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 12:41:19 -0300 Subject: [PATCH 101/224] Implementacion de la carta Cita a Ciegas hecha --- app/routers/games/games.py | 13 ++++++++++++- app/routers/games/panic_functions.py | 15 +++++++++++++++ app/routers/games/services.py | 23 ++++++++++++++++++++++- app/routers/games/utils.py | 2 ++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 9ba4000..6f96a1e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -272,7 +272,7 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform return {"message": "Card interchange terminated."} -@router.post("/{game_name}/show_revelations_cards/{player_id}", status_code=status.HTTP_200_OK) +@router.post("/{game_name}/show-revelations-cards/{player_id}", status_code=status.HTTP_200_OK) async def show_revelations_cards(game_name: str, player_id: int, game_data: ShowRevelationsCardsIn): utils.verify_player_in_game(player_id, game_name) utils.verify_player_in_game(game_data.original_player_id, game_name) @@ -283,3 +283,14 @@ async def show_revelations_cards(game_name: str, player_id: int, game_data: Show } await player_connections.send_event_to(player_id, json_msg) return {"message": "Show card terminated."} + + +@router.patch("/{game_name}/blind-date-interchange", status_code=status.HTTP_200_OK) +async def blind_date_interchange(game_name: str, game_data: IntentionExchangeInformationIn): + utils.verify_player_in_game(game_data.player_id, game_name) + services.blind_date_interchange(game_name, game_data) + json_msg = { + "event": utils.Events.BLIND_DATE_DONE + } + await player_connections.send_event_to(game_data.player_id, json_msg) + return {"message": "Blind date interchange terminated."} diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index 49a8fea..79cfbd1 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -33,6 +33,13 @@ async def send_revelations_card_played_event(game_name: str, original_player_id: await player_connections.send_event_to(next_player_id, json_msg) +async def send_blind_date_selection_event(player_id: int): + json_msg = { + "event": Events.BLIND_DATE_SELECTION, + } + await player_connections.send_event_to(player_id, json_msg) + + @db_session def process_revelations_card(game: Game, player: Player, card: Card): game.discard_deck.add(card) @@ -68,3 +75,11 @@ def process_getout_of_here_card(game: Game, player: Player, card: Card, objectiv game.discard_deck.add(card) player.hand.remove(card) + + +@db_session +def process_blind_date_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + + asyncio.ensure_future(send_blind_date_selection_event(player.id)) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 46c7a26..e696aef 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -486,7 +486,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Cita a ciegas if card.name == CardPanicName.BLIND_DATE: - pass + process_blind_date_card(game, player, card) # ¡Sal de aquí! if card.name == CardPanicName.GETOUT_OF_HERE: @@ -543,3 +543,24 @@ async def show_revelations_cards(game_name: str, player_id: int, game_data: Show "original_player_id": game_data.original_player_id } await player_connections.send_event_to(next_player_id, json_msg) + + +@db_session +def blind_date_interchange(game_name: str, game_data: IntentionExchangeInformationIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + player_card: Card = find_card_by_id(game_data.card_id) + + card_to_exchange = select( + c for c in game.draw_deck if 2 <= c.id and c.id <= 88).first() + if card_to_exchange: + game.draw_deck.remove(card_to_exchange) + game.draw_deck_order.remove(card_to_exchange.id) + else: + card_to_exchange = select( + c for c in game.discard_deck if 2 <= c.id and c.id <= 88).first() + if card_to_exchange: + game.discard_deck.remove(card_to_exchange) + if card_to_exchange and player_card: + player.hand.remove(player_card) + player.hand.add(card_to_exchange) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 05ec8f1..e20a478 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -34,6 +34,8 @@ class Events(str, Enum): REVELATIONS_DONE = 'revelations_done' EXCHANGE_INTENTION = 'exchange_intention' EXCHANGE_DONE = 'exchange_done' + BLIND_DATE_SELECTION = 'blind_date_selection' + BLIND_DATE_DONE = 'blind_date_done' @db_session From 6ffad4c17bffd1d7fed2b577c371e94a4f8702a3 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 13:11:42 -0300 Subject: [PATCH 102/224] Se agrega una ayuda de Loki para cuando queres saber que cheats tenes disponibles --- app/routers/websockets/services.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 05ccdc0..b32deef 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -9,6 +9,15 @@ async def send_event_cheat_used(player_id: int): await player_connections.send_event_to(player_id, json_msg) +async def send_list_of_cheats(player_id: int): + cheats: list[str] = [] + cheats.append( + 'lz or lanzallamas or flamethrower: Obtienes una carta lanzallamas') + cheats.append('ws or whiskey or whisky: Obtienes una carta whiskey') + for c in cheats: + await player_connections.send_message(player_id, 'Loki', c) + + async def handle_message(data, player_id): message_from = data["from"] message = data["message"] @@ -18,8 +27,9 @@ async def handle_message(data, player_id): for i in players: if i != player_id: await player_connections.send_message(player_id=i, message_from=message_from, message=message) - - if message == 'lz' or message == 'lanzallamas': + if message == 'cheats': + await send_list_of_cheats(player_id) + if message == 'lz' or message == 'lanzallamas' or message == 'flamethrower': flamethrower_cheat(game_name, player_id) await send_event_cheat_used(player_id) if message == 'ws' or message == 'whisky' or message == 'whiskey': From 27467affa6d64957642acc4cd6a04e8b043caa7e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 14:20:47 -0300 Subject: [PATCH 103/224] Se quita evento new_turn del discard --- app/routers/games/games.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6f96a1e..95f0898 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -156,14 +156,6 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - json_msg = { - "event": utils.Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - return {"message": "Card discarded"} From fc7808f5990a785f1e9ac030e3ecee320288cee5 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 14:27:01 -0300 Subject: [PATCH 104/224] Se quita que pase a nuevo turno despues de jugar carta de accion o descartarse --- app/routers/games/services.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index e696aef..ba6e1e3 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -230,8 +230,6 @@ def discard_card(game_name: str, game_data: DiscardInformationIn) -> Game: if game and card: game.discard_deck.add(card) - update_game_turn(game_name) - return game @@ -350,8 +348,6 @@ def play_action_card(game_name: str, play_info: PlayInformation): process_seduction_card( game, player, card, objective_player, card_to_exchange) - update_game_turn(game_name) - return result @@ -496,8 +492,6 @@ def play_panic_card(game_name: str, play_info: PlayInformation): verify_player_not_in_quarentine(objective_player) process_getout_of_here_card(game, player, card, objective_player) - utils.update_game_turn(game_name) - return result From 211161bad47dad351dbe5e8c683ce7acc7fd8c39 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 15:39:40 -0300 Subject: [PATCH 105/224] Cambio menor --- app/routers/websockets/services.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index b32deef..80ac1d5 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -12,8 +12,8 @@ async def send_event_cheat_used(player_id: int): async def send_list_of_cheats(player_id: int): cheats: list[str] = [] cheats.append( - 'lz or lanzallamas or flamethrower: Obtienes una carta lanzallamas') - cheats.append('ws or whiskey or whisky: Obtienes una carta whiskey') + '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas') + cheats.append('[ws | whiskey | whisky]: Obtienes una carta whiskey') for c in cheats: await player_connections.send_message(player_id, 'Loki', c) @@ -27,12 +27,15 @@ async def handle_message(data, player_id): for i in players: if i != player_id: await player_connections.send_message(player_id=i, message_from=message_from, message=message) + if message == 'cheats': await send_list_of_cheats(player_id) - if message == 'lz' or message == 'lanzallamas' or message == 'flamethrower': + + elif message == 'lz' or message == 'lanzallamas' or message == 'flamethrower': flamethrower_cheat(game_name, player_id) await send_event_cheat_used(player_id) - if message == 'ws' or message == 'whisky' or message == 'whiskey': + + elif message == 'ws' or message == 'whisky' or message == 'whiskey': whiskey_cheat(game_name, player_id) await send_event_cheat_used(player_id) From bc83bbee4e1d3046438b49e23f21d9fe15741e2e Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 27 Oct 2023 18:17:55 -0300 Subject: [PATCH 106/224] se agrega dockerfile para el proyecto --- .dockerignore | 11 +++++++++++ Dockerfile | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ce0fdd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# Byte-compiled / optimized / DLL files +__pycache__ +*.pyc +*.pyo +*.pyd + +# Tests directory +./app/tests/ + +# Database file +./app/database/*.sqlite \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4364ecb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.10-slim as requirements-stage + +WORKDIR /tmp + +RUN pip install poetry + +COPY ./pyproject.toml ./poetry.lock* ./ + +RUN poetry export -f requirements.txt --output requirements.txt --without-hashes + +FROM python:3.10-slim + +WORKDIR /code + +COPY --from=requirements-stage /tmp/requirements.txt ./requirements.txt + +RUN pip install --no-cache-dir --upgrade -r ./requirements.txt + +COPY ./app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] From cbe8081ebe639a4a4ed66312dee3d057bfc2af2e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 19:06:16 -0300 Subject: [PATCH 107/224] Se agrega la funcionalidad para la carta ooops --- app/routers/games/panic_functions.py | 18 +++++++++++++++++- app/routers/games/services.py | 2 +- app/routers/games/utils.py | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index 79cfbd1..b61f0b3 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -9,6 +9,15 @@ import asyncio +async def send_ooops_card_played_event(game_name: str, player_name: str, player_id: int): + json_msg = { + "event": Events.OOOPS_CARD_PLAYED, + "player_name": player_name, + "player_id": player_id + } + await player_connections.send_event_to_other_players_in_game(game_name, json_msg, player_id) + + async def send_player_between_us_event(to_player_id: int, player_id: int, player_name: str): json_msg = { "event": Events.BETWEEN_US_CARD_PLAYED, @@ -49,6 +58,14 @@ def process_revelations_card(game: Game, player: Player, card: Card): game.name, player.id, next_player_id)) +@db_session +def process_ooops_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + asyncio.ensure_future(send_ooops_card_played_event( + game.name, player.name, player.id)) + + @db_session def process_between_us_card(game: Game, player: Player, card: Card, objective_player: Player): game.discard_deck.add(card) @@ -68,7 +85,6 @@ def process_round_and_round_card(game: Game, player: Player, card: Card): @db_session def process_getout_of_here_card(game: Game, player: Player, card: Card, objective_player: Player): - # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition diff --git a/app/routers/games/services.py b/app/routers/games/services.py index ba6e1e3..4b90dec 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -466,7 +466,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Ups if card.name == CardPanicName.OOOPS: - pass + process_ooops_card(game, player, card) # Olvidadizo if card.name == CardPanicName.FORGETFUL: diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index e20a478..f43547d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -36,6 +36,7 @@ class Events(str, Enum): EXCHANGE_DONE = 'exchange_done' BLIND_DATE_SELECTION = 'blind_date_selection' BLIND_DATE_DONE = 'blind_date_done' + OOOPS_CARD_PLAYED = 'ooops_card_played' @db_session From fdcaaf2f1cce3593a6c87c42b553e50e46ec5512 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 27 Oct 2023 19:50:48 -0300 Subject: [PATCH 108/224] Refactorizacion del codigo para usar los cheats --- app/routers/websockets/services.py | 23 +++++++++----- app/routers/websockets/utils.py | 51 +++++++----------------------- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 80ac1d5..0e5be27 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -1,5 +1,6 @@ from fastapi import WebSocket, WebSocketDisconnect from .utils import * +import asyncio async def send_event_cheat_used(player_id: int): @@ -10,12 +11,14 @@ async def send_event_cheat_used(player_id: int): async def send_list_of_cheats(player_id: int): - cheats: list[str] = [] - cheats.append( - '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas') - cheats.append('[ws | whiskey | whisky]: Obtienes una carta whiskey') - for c in cheats: - await player_connections.send_message(player_id, 'Loki', c) + cheat_messages: list[str] = [ + 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', + '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', + '[ws | whiskey | whisky]: Obtienes una carta whiskey', + '[ups | ooops]: Obtienes una carta ups!'] + for message in cheat_messages: + await player_connections.send_message(player_id, 'Loki', message) + await asyncio.sleep(0.25) async def handle_message(data, player_id): @@ -32,11 +35,15 @@ async def handle_message(data, player_id): await send_list_of_cheats(player_id) elif message == 'lz' or message == 'lanzallamas' or message == 'flamethrower': - flamethrower_cheat(game_name, player_id) + apply_cheat(game_name, player_id, range(22, 27)) await send_event_cheat_used(player_id) elif message == 'ws' or message == 'whisky' or message == 'whiskey': - whiskey_cheat(game_name, player_id) + apply_cheat(game_name, player_id, range(40, 43)) + await send_event_cheat_used(player_id) + + elif message == 'ooops': + apply_cheat(game_name, player_id, range(108, 109)) await send_event_cheat_used(player_id) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 9e1c9dd..4840799 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -20,57 +20,28 @@ def get_players_id(game_name: str) -> List[Player]: @db_session -def flamethrower_cheat(game_name: str, player_id: int): +def apply_cheat(game_name: str, player_id: int, card_range): + games_utils.verify_player_in_game(player_id, game_name) game: Game = games_utils.find_game_by_name(game_name) player: Player = find_player_by_id(player_id) - games_utils.verify_player_in_game(player_id, game_name) player_hand = list(player.hand) elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] if elegible_cards: random_card = random.choice(elegible_cards) - flamethrower_card = select( - c for c in game.draw_deck if 22 <= c.id and c.id <= 26).first() - if flamethrower_card: - game.draw_deck.remove(flamethrower_card) - game.draw_deck_order.remove(flamethrower_card.id) - - else: - flamethrower_card = select( - c for c in game.discard_deck if 22 <= c.id and c.id <= 26).first() - if flamethrower_card: - game.discard_deck.remove(flamethrower_card) - - if flamethrower_card and random_card: - player.hand.remove(random_card) - player.hand.add(flamethrower_card) + cheat_card = select( + c for c in game.draw_deck if c.id in card_range).first() -@db_session -def whiskey_cheat(game_name: str, player_id: int): - game: Game = games_utils.find_game_by_name(game_name) - player: Player = find_player_by_id(player_id) - games_utils.verify_player_in_game(player_id, game_name) + if not cheat_card: + cheat_card = select( + c for c in game.discard_deck if c.id in card_range).first() - player_hand = list(player.hand) - elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] - if elegible_cards: - random_card = random.choice(elegible_cards) - whiskey_card = select( - c for c in game.draw_deck if 40 <= c.id and c.id <= 42).first() - if whiskey_card: - game.draw_deck.remove(whiskey_card) - game.draw_deck_order.remove(whiskey_card.id) - - else: - whiskey_card = select( - c for c in game.discard_deck if 40 <= c.id and c.id <= 42).first() - if whiskey_card: - game.discard_deck.remove(whiskey_card) - - if whiskey_card and random_card: + if cheat_card: + game.draw_deck.remove(cheat_card) + game.draw_deck_order.remove(cheat_card.id) player.hand.remove(random_card) - player.hand.add(whiskey_card) + player.hand.add(cheat_card) class ConnectionManager: From 8b84d2505fe30cc09f90705f03c7b952fdb58ccb Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 01:56:18 -0300 Subject: [PATCH 109/224] Se agrega imagen y sonido al iniciar el back --- app/main.py | 9 ++-- app/resources/stay_away.mp3 | Bin 0 -> 96428 bytes app/resources/stay_away.png | Bin 0 -> 483843 bytes app/routers/games/games.py | 2 +- app/routers/games/services.py | 3 +- app/routers/websockets/services.py | 10 ++--- app/utils.py | 39 +++++++++++++++++ poetry.lock | 68 ++++++++++++++++++++++++++++- pyproject.toml | 1 + 9 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 app/resources/stay_away.mp3 create mode 100644 app/resources/stay_away.png create mode 100644 app/utils.py diff --git a/app/main.py b/app/main.py index c95fd9e..3fa58e8 100644 --- a/app/main.py +++ b/app/main.py @@ -5,6 +5,8 @@ from app.routers.games import games from app.routers.cards import cards from app.routers.websockets import websockets +from .utils import show_initial_image +import threading app = FastAPI() @@ -23,7 +25,6 @@ app.include_router(cards.router) app.include_router(websockets.router) - -@app.get("/") -async def root(): - return {"message": "Hello World"} +# This displays the initial image with the sound +t = threading.Thread(target=show_initial_image) +t.start() diff --git a/app/resources/stay_away.mp3 b/app/resources/stay_away.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..68e06bb60c02cd7c65baa27250c23c3ea4fef825 GIT binary patch literal 96428 zcmdSA*H=?twEn$QfB>PlPz}99=m-d*g^mFN(t8&a5fK3;^xlh94OObrEEJ^#kfsy? z5kcu41q2HS`Qi7Ri}&LE1MeCm*-6&T+MmoZ_p_ckmw}EP6!me#foPOk3WzWz4@gYMo7e-IfRmyn#Ak(K+fsDw~f`J}e~ zSyStawvIR5#QwqI_v0U?XXh5aF0XEE?S4D>@$>iT*}s+iTk^l_|2Zz!!4yH^|9{T? z|LDgH{_g8B5XUQKoDwW3?ee*37yb}x%}dt1sGSM1yz8QIEV5AU#a=DUvuQSyGUUw4 z_Tg%c1^1tb#1XUJYm$95K&j)v%BK~Kl~J6>?{)8Q+Ba`D7)XW5h56k+C5<%dgryfe z5k0?4LIbLIA>b=0DvOVoP)YRtg8fMa;$l2vH9NsJMl_M^b>d#niaOU7ko&r;?tIa= zp<2F_I?HOZU&AeZGu!L92arFnJqWuIw<-oi9sRgZ*NGk=MOes~AGu$>p1qvr4E^Lz zrfHPD=aRhsOEh%LGw`U;bBnl|fRU!tMZ^=`wCig0Cxz$B^G}fR6A85k)73N!u&W`@ zuAV19BZ;6c{UWW_0?fMv?E~4WD(9*_P`B%4p|wN4>Vyz=7gD0qtnJO` zohQG;)txE*hk<1(*!li#AYPYZD5v5d|Lqj*Fg>#(V@V`dxA3kyTKyYwJtY&j z>iu#y0ApmbPJc@Q*%ne~I>T+Nf2hV9#82$Tajj(r^;_JQdZ| z;Kl4{k}Sw+-psQtV1Fxi%M=uUrI4{5Sk+I z#|{6RE=)z0!VHgtprO3bNbT+=tgJKNHwngEky^2*N{0;6c=zOk=huv)=tDT!oAn^E zw<9}eM5y(stg0V`u&>!BebU?f!CA{jPEB_bZZ~lq9WhXM?@y7&t0vbA{yG~_ZK$3_ zk%R=!Ztx=h1rw4acC2?1W|`RX>pn5L+clKO;YK@sBx6Ba!L>kW{qedm6ZiOfO-m&w>p8F+Zh> zg_9j+INj4Tfp*RW$5F&(y0)cK$=>^dXnKF;aEvNuoW-}nI3bJ4L1FQ=HH2`cCbfL% zvwtNyu$ukpV5K>S?>!srj}e|UwO)g=+o{9i8IFhRs*Ycue0?40RC;{%hQ`zn^f;4G zMNyzWBXfW~*&x&0JE;l`8*8gY)V){aiJ&yUlD)jaS!oA@g_m76A7a&9xu(ycU{o7eVS~2zT3~D&fyBb?3LFwD~6t5ldwto0ti(0D!7MHNaG_pfEb%g1{f9qA43808P`+jw-s%Ha0S(eRY0(Q0 zD#CJkpLhya0C~TQ`YP$k>-qphOdc31Dw7gvktnDMW%b=rai{J_KNJxmoVCPJWS38H zWtXYCM6F32moz^zsQoFxCrQ+=T^3C&wCD;k`_*xKCgmxT-|g~R5T>vaQEj}{Or^1& zg9X^1kyEHvIsI_%j;#pHUP?{S((y2LWb1sh=811i0m=ioIMc~=)`IE`lFG817G`G4 zji#R6c@Q|)BhcM_jaK5J+((-GQ(weXZ#UD=Ov`lvijXV8DfbZ>VoARZ(R2RjGV|o0l^+0Xk%Ga#u>Ua{4dl&s=Zd#MK@SZq|M6c+frw(vIsGJ&8j$vnE-O5#x5)JU8k>hu z@U7H(l*ng5DI>2?Em8YGpfgwh6J^WchyLc%;XUeuU~0F)IE-LES+rPI#2-DGr2y_P z-zQ0=b7p`CgcR}%xB}oa)U1DCdxRR3jTB=dhtP(Te~HozZKU9Iva|>L3<{O+=UIqjLY_AEWi|vPFjh zbEBra*h`ma!!#2nEfdr$&C4{RH1(*!wWE;wu9R7+pab_LL+j110C}x=B}k0&D4ObH zq8?XZu>rt@>#a(V6l}diBAsbM#ls(y&fBm6qYC^j(>LX2uu*ElZ7j>(7#RjZAao z^_P7?igk9=WAn%#5h?UM;+|_Onp@h<@lnVc_GJ|SY=^(|-Y1F%!A{LQ#0^5j0ed7Q zGrOnSA`fa_*{I1Y4U#9y{FcWA>2kMT(@fDNS=fu`bUyN+4N_w>JrY z@7P?{1z79|gCVRqTiiYcmvk>B?o~)sm4n}F_vf$jg#_VIVC1H<7&YL0pQz(%7a--0 zjgm2Y?<4vp^SD#0`n|B9@M*&|r#ANfyZp+TjcAPcled)YWe=3cGKTdIJY2g$&k23-4dSuc>E` zT|#QE4E3(WC$QRcDL;?W%aWPAz-TFuMrDPg;%o;$z@wD=3zcqc# zi7%xMe1>{4CaF8YkUU9-8&E9l=k*o_6;PpWsQgNHhl*Ta33$E{fPyJ z^)~W;H?&|IJAU_*`IXla)sJ|=m;f5|JS9-!f)%qD#yW3hTHP%7_$1+_)<(&Sg;u{^ zi#U~2Yw)QAq#Ch}Q#l*m35 zoOwL|;;4cz2Qv}$^rMzLh9`RJH|_!BO7x#*5k7Qk+BCB3_!%NA>5{MDNaJN>wU}mT z?gN}Gpr8z7#*W*8T56&bp8n&%4DbD+G*x7v8l(l@zU{IlL6g6r?=R4rNt+^?7{^{V z9T|jTw!9F5)g*Krep$huAvsueckl6#(14)kfv*okwhM5K3@kKfbE20@uhdLk;N(i!0k{o$-t|L44uXJjGo z?E9ZwYjMlGf=8wrT_n;;=njeW>TxYQ5#EdpC;vSqkDa?gE8UFwVcu|k+7&U;JSQF3_>_wE))8qMf8 zC!VBTH4H>tPxxc^)u8p&DG9ULQTgYOg1@$Y#TS!#0Op; z*~UgZh#rMNFdsT&bTz%{+r?{`sinX$tM&HjXA-~Kgg<_|X;J!WWH?#ReVej7RTol< z%a;{;m@y%6==_@TLv9!Cky2ski)7yU@04CzTKDYY^)TWNc8;N)1i;0a{H?(oa{sg^ zh=2SyX^HIG6WKm21fyyyHy^&F6lT)J^S3pUFU^Ohvr)Dw-d`GD5e=0^BEg?3#CY=V zU^HX=CEjo4pD}@gv22Pjng@xpmczli797`K)Cz{aG(Eb7i8&^GwCf_Nl1LJOluu9n zoX^`_{q#%v*zOdwpVSa^u?fS}Nyy$?BOB(LQevz=nR9-z^`Tr`95E@gyf>EXf#?;x zHx8{-AFE7!U0R#OK`yhr5xb zgdN$8=XjRL@b_{LMVMZbdl%@FfqcCb2m6W~lQ3|)`-Cy_t$D;8UkuS8E(?xzS zNK|QRp@j9>=Dg_4V37zQ^N;^Foai7up6$niGSMF9_gV3Ui?W&e3z(rvDK#cf2q(j! zsZ7V^G1i0^J+un3+XB9!toKd6=4E|XHFEfvpW8RR+WfH=?Qa$DKKtj2_4tM$jH9RU z{Kj*m(lXPpq&N-IasUu$2xx%+f?`sjMzCmSQ5$hMjjSC-5L<7H=o2+{-VQNO8+YU4mp>b&aN9wS|isx?1E>nD^LZ9>nGy-hS?I ztr=m;gbjMRs;<3tmvoj3K<5eXdStcwbXzsQbpt2$)w>1aAeZD!AkFWRkGzcxXpc&xCe-#Onia z#AU|-+2V!?5>#=zq?-WbIKzb4`IjK6psbbNB3U7tfBbjg#4h!*5238& z;+Mu3`m+Z{y=C&S(Hm-*GYA*526DHoUe29jUoE2&`W)Ap>P`l3Me_BwOY}4?vn^9j z%#_@Uc`y)hzWt@)Rqy#DXBAouX@&N_)*v6is2Z8%ugrwx0rik&v?Puui#IDq=^CG3 zk?cuS_&AZ=plCi*Y0aKH1$sKcA!Vj(+LZV4niFBdhr zKv;ouSo>CiR3Kvz*-*;N9VcvOR=oJ(*QxKSI2{ncrY*Rk9V{r|iRzBBfV1*avUABb z-iE-)V+xq^eN(Nt>El3BEY9+l_B$HUnmLXIx?8$Rv2ulo#f;En3y&^O4bRUBbMKuJ z^#th}LVSKCb-PrF>8J%!a72~i4t^MQj>u8!M+owlUpQ~ESw7QeU1hFpIjlaFASd=jx%G*7 zx>TC^Ki~@Tq5L!D&(xY-;j(j<07@cczqrN{Vlu$ybGI*0fhVj@p3EPW}!9{ zpL;EN;UnnC6+<=W0&aMM&XagLUb)a5acOSu!Z6O7cOUC6%aBO(1IVv`*i*c6iP+ue za%zB9WbHV+@}~mIZ2m%ED3uxm9J9YQ`t8lhRR-t^T6T97y1X62xi{qj!9GjZf)vd z6DG33L}ZgcgNLtmWC-s(4M`h zdXyvFnlM?Z56xQu@HO?!`gkK`qVQrOnpcb~UHTD%(tTrsjT{+rki>~kcw4Q(+Wy7k zK9P>8M1_y+tmfslVXpJXXH)|PXDLuvH#9kG9WA)Y`yeGZjYWqr1?WfD@_R(*z;S!} zeyAq;-zmNhafn(z5%S(%-qb{|elTX=f$5I^7!7cRf&&ZdyYK*J{e`c7xXc)NkN$_O zFjGRLNP&2jnfuZD@}A+`+X8e_j2MP7EJE+(uxk>SAulXVDDt7G_Ds=1Al6di2{(Jp z5&*q{Z-eH4DwJU<^K3_|@-r+dJMpaSzw;?!05ai|Ig}Afuw7?V8lEx(lEjgOPo zhIun#|B-62Ip-Pe3q7sBLQ1JYrhqgWwDamEGP`gRN}r!u+G+Chwa7zS1ffB4GxsT( z+k>zDK0j5f?x0^|T-(B0T8fnIi+qZQdrmyZ``jTJ+PRA7S zw8xnYq!AV&6@|Pr24ea$lFy<7aE*>!=fsGE4yArde!+vg=hBMQo>XTzc2{(<2$7D2-2|_V5ON7 zr3#w{` zqJ_L29+~a`c&T}1!u!0u(C*#&PD!{9#O-};{}q;!jtCpj9d~3dq$E}bHX+c;%4*mYDOML=DrZ`- z7^pp}SLCoNwWZ=%sQHy?c}8>Njr;`7-)Z?oPxYdL&wPR(^AyCNKdH51uS%o{Y$tte z3F43~UjxBUcan(aGT8c~7d*#Yf^ld7@P_>3zm=kGqh)r%pI~0@3%*WnOhZPgC5f`a zf1}-Yh)MO0=R~z-ynaS#8&?ies}7az{!Z3TArorJC|3T%%Fl>)l!YnkuY?Y(NOs1D zN3dHEEL5;Cm%2g6#qA~EQL?Gw%v!@1-V*LaA`ORj9Y2lxgm6V-U_aHsxv3yZgFXRz zfk0+TD>8Bz?CwKxp*!;ftBq2pIbY0Qr)PD=5|ckGGNxx0KF)bh;4|lSVJWm@5v<0! zlnjjCdOWYJMEN#wXL^Fvg>E=w%ecBB6`c}gritbw0}!$5eH3zeRBax$k*3R6RYRC5 z6A8g`K3M#)Jlhs0gNJgD+~TKA8ZKq$AeFAOOQef&{_K3|6F^3csHcvHMiqqIV;HWa zW=LfK0ImLYd3rW!rqJKFShK6_c|n%+xSgSUP;RI4dalOz&GCF{?6fWU_e-xFF~Ewa z$iEMke_9c$@s!Km*PMmfT+L~$9^vAo{r+-bI(vI}NQ=!}9iLi!IpQDx^)zi)DQxyl z=5F(qdp~Z%CE)&)Iwp-c{4tLhcTS=&`3cGCZ=Fh8pBn3Y{c2x@X$ddVaVz2*8g$`Nab~Q@-Jrx)(ed z@1`Z)FY7Y14sFHIF~HW#;9Rs%?iD7_#b0$y1bOq2Lhp)yU0-MVTVUh0F&0`wBHpih zqVdPD>wfsmo2UB#+#Yz_2u4FcfEddR{l-VKhD(iWJ$&o0&C7Eom072S-(=Rqh&*ll zbA9v+qKu0Bd!kDmMbw+3t{;#0))Hy~0Pg$_jT=xNY>ljd@j_|L$RB|PtPGYUh;lUy z?11etmk;%{uX>|h!fu`068o)gIDXd58Go-5-aPKdUiTR@nq}kFKhpHw_o}YX=v26B z&5IqqAOHBTrbX9cuuLJ0hw^2icU|Hn7|7UFq+G5K{!QL8$)hYQ?(F1Hj0$nlI)|q= z#_{`VK#>450Se6#n(pa_gKeiE!v1)I6cfxqp}LiFxd28>woHFp<#km);hL4{@UouhYkV! z8I823!@YACk^Yh$)i`eNdFjEcmCp(DndoP)7bNt}{OfqxHB7j*54K2A$TYR(OH+;= zc1~@9ryt47!y86DRX5+c!2x~%6qUs_Xg7Yld4t-!S>H7^)-5DTsuepu6P zf42O{7>Uq&(+lQ}y+b!NWq#6f)8%(O=`S2mIJS=`fYFnWxx6$iDPUryMQ>$mP)+T2}* z9{y#@e44A}jWE57X5iX)e(o+m^pF3iG@_A~8K(h@EV}pP;`!^I^5>G4afLPZH}xh> zy+BA;u83fVNo)$Dj!c`y2m!t*Z$}~^H%3DIh8=`1GcU-JDJd#RTu%v=Lo(}#FtEF< ziTDP!&eXZm3Z5xfxWc=sx=353(8ToPX!tw?Kaf0xGCme3J@!rULPyCHszx{1X(U4YqU(L&B^+^E60 zbO4aZavL5PrmqZ723W}prC34^9poO~t7&HcW3gH-0v3Tp>M$Cz^FjF|gEFecgsZwb zaoF<6L_fQp^5fMS-Hq4U{1md(L8YAxN$b;e1Z}Mn0pREG+&}(b)3h^G$BQoVzsd@S ztxJ?=I`X?3&C+%Jha}O|IDzFbI3H&%Cz8yLx4W0Xmoo?wi56gfnCl@nxn7@9#mVJE3JfV-%oQew5^(&F-e#p85>F<@SR>$cRd|~R_7oi$T(=t zK%Gx=wT@Oys)Is!SN0FZv7Uze_u@2ek<-FFOBgu+#MoPR0~ATN!+h zCDf(n#$+E?Nk(l*(B+b4#&=fjpSB+ky;tt|E&BC#=7fh;C=(&rkBHNsvwiw$I?)9; zwqnTokN+Y$QCiO|+km-M*G=@mW6P5U#Hq7~!u8Nni5L{4LN+1+Ff1Q111R(}w}jq4 z_HsyVZ9*XVMd*R0C|oULuKI4KTRy{S&27d?Fs;syYA@0m&oZ~?9d75k-Y9k*@4*VUnl-&Jc*2{_(HKf8Zxo)7shwsB!F~|_rR-aKa-xNt7pSi9b4WHg z)GC}svQ(U2))1qokIz&gB=|r4%q0Lhh}IQ!bg~?HT!RA-7aV zjmdK33A}f&i>tC}9ADT$PTM3t0Ic~5?`#&A9m9r9r2;SZzb6MJUPvPt8T+hrp}Uk} zz@j{}j@3K;MPjoaZ@Xy4sy%lW`a~keSUNK&JAoP@(6e?-IZ@afe!`5M<5Xa=rMpJ9 zjZv75x&38!J~eNCUTpgj($V-I|K&6{a}_gcW0ne~hcIQ{g59~bfO(|rQpG|6M1 z1^PuGZW!9E9zRenT01(Uy}G}Gqx7l3Sf;y6jEyC6b>9?}-IFG8|Kzi*FaM$Cf^bFK zchc$Wh*i@<{!yJJLwVO;295yTTxoL}ST{RfNFN-=YcCQa8!zH50&|f)Z?I%})#ES1 z;nyIvTx(ivIwiweN988zT9I1*7Dpar;adjjIX1wT>o(9oF)r(tr6qrqvI`f*S84kB zQj&{rVH6wMsc#>WhDp&}z)Kv&$>Oh^e>9%S(H)fM_eE>I!i-}~Q;VKg7YP9Lb+yXQ z+78-EYY8vBj+;8(k=>3-I!FZ#(4;3f_((@QB%OABT=fM2BEtfjsve#suOtJ)Kuht} zjUtV&GO){h#(09fdXhh-pWKtkTCrLjvix2sUl@eWZbY5D9GO{N}t!5QF%fW2KqV8mY zJbZa|8XM*{*e^e4UG(G<<|w#xpW{@ck3>o*hxYszbqz$J|z;2CaD#k`un8$6I-g?%qqvsT|`Xs5`*IY}lBOx;K!AGkccXHC)ll{7s zZzXG0VS9AHIY?)HU*nr|<=HYGYpTS|(dG#z7!217-v+?AC=?C%l?1xTg^C8}ANW@k zpkC&?G^WXJL&=3(&CAqA(}wFd9;k3L^{+cm{*p*vB7w2HCl}Bu@sr;0O?Z+7x3vlq z1^S>{0oB~KXztj92h?gqt?daza7T+hf$x2lCdR42h@bCcy1seYM>h>yW_1nJ5S zeNjJNLtap5uV&?^GVTm|SmDBOYFZm-`*rnuhmJp6NMm1_?A{U9TWcJ?j&}V1U;ejr z$FmJs8C92knXc23r-nS!GFe-~Z?lTh&+FI+uwKxAr+7_bFbwzx>)v0$Z&6@a5HC7s7fc#M)4nSL1w& z{RYAfR5@ILB_Q;N$>H+*95CKXXtRv?goI?K@qm3@&Jl6sY0RhV3XuM9!6qMOB!61Yj#v=m)#r7~eTt>=Zt)vEv%4Vd> zch3eq6z!D-(>1gR^1^8KG~F1#eXS5H_o{foetr5kKf@qARzE12Efx}#6)%evXy9dR zsA#_TzO*yN-Z}lpek{{D&90#|BV~d zyu&~ftn`Nu34oZnMdR;)EYbLU4p)686zT^wTz?!YJ4QhTV$&|g(@t2Us)aMnq&Cbh z6>6nP$N6Q2d1l62-6qG0awkRw{MI)_&OeGt_;@3(!aiAj$I<$)?fx=`HBjPP7Dr9u zd9m1~HPiY{_3exoI(phrG}wp$TmwpV$)7bKE~?dc$z;hW}d zBY~eP)Q)rmnxwc}HaiT}!bp2&$uqZD5rVz_qi7kBd$ttVll2{Nz4`?oeGje3()T z3hFM!LYyQS(G;*ux>AV*6Z4A5s7!Q%H{KbRT;FX(4Z0{N^`s5lR8yt^)%#KxZWX_9 z?z_hBxtyA2PuE#-j2Y(z zfBofqEjN`JY4ZEEBn1)ZUr87K8;VcpLCo(0`s%YwIla&oFrYX?0O85iBvs->s0B5b z0j^x*Z*`$_HiomWa_HydoHeS16!RCCZe^{k)-GswM4r7nrEEX{mVW$zfSN<^sL_Y0 zk_{wP|Lz^cU(*jwFy8M0A$ocxGW2zyi%j?{DsG>tjE{KXZLdd-3DRG`6lbT*hlMO! z7e2Vy^5nVtlZ7%csNPBasmQibt;~FF{Ol1ELQM4(qxk988`WKR(mE@pM^v|+4W53| zFUr$e$ILyG@+AY=6MLiH7I&DyaV4`GUsO0rn|UK*+sIX6yzRSk=ph~=-p zNEb$QidINp3Ov8bJckm2eFa78Tk9j~FS-fXm}MFTa&cOkd;Z7VWImb441t0Z&~BF} z$?OWuvwkbyPmE+Z4DPl(7IaPQcYGF*>w8V%Nrz?I*3S}G+GL;q0lmJe1YGm^foD0v%UJ<_&lk-^EN;} zLl0O6{F%1y=oy1Sax&>NzR8;L)B9-OFlAiPwA#-fl=t=*uu9V>8X#6_AGm0A>Hl^q zzwULEF=3#&l=wFpPUAY zHT*1UjLV^KJ$jTg67YUYNNy)eFsI;bY-OxO&gP5QdWGK}H_PS8X(|L0h2KUOWR;CS z6ey$AKjR-iJ<#y_dHYQtsiAA)LiotMmR)iV0FclO=4vn>-3_1W9DF2U8T-n0Uutka zgjw{)-~`AzPhK>n!4s(31{RyduC=?wjeMM?025HPl2oH7f=SHuV_68=VWZtg9`#M2 zH>p%~o149Xgfrb(hw~?xnb@_OT~(&XZ~pOLrPVIRV5x61<5bFgl{~gEY4i(d$)tJ?(wttC|j;AP0|8^mwy*g06%Z_QxkW59aA1g+Mow z&+7KOTi5EH=Hz}iYT!9Tig9!pj3>R7tnmx8uVbE^hA(UB-sn2&6onpKzDoh!2L_!` zycjUJ6vqPnIK{4lBzqIf+wBp^iR;b>mFvmq4S<)+mdAxhE+8Bn4${PPjM>FNd2Zz*g_J5Tq8vgNLOVch^8fWs8Bq+K1 zig{5|F$l7$NOm66|6unvsYoPD`L)E8mLJu&^hipuSUkyT%+=&Tbv;OgX|&~ykplN! z@c=_N^{{KgBjT{4jJ$(@qA6LpJXNW^s8!}Lf383bYf8p@OXhdR^lvzZ$^C!XkjJhCF?H3@%;vl%+>v$zq2QNl08dsOlt`lE+eHa#_DPj~rT z2AluuKm1HfoYTD;eU-~uIZB1ei;kRDlSRv~)69B4(feQiPxH`1_U3<%0BqM(Egept zJL12_od1^Th7AJbpmg3cM>>;^Zn7yWA@II{-F$xS+LT*!pH`J;6bk8l_S^YkaDpk3 zL4)+`L(=}d`Y=V(a!8FUFL_FU=m-) zdE8xNxF26zv*@jFB4N>mU(zn=z=qHueHub?utR^~2cFj16e;;FFX(c0iXokpDfgTp z@yo%Q#&>D8}xqoyTl@M@aJFr&!uTc>tK^5Seg|*spMSvg44W@b^KA*_{HSvg*)Mtx0+Wj#!~sfgjUTJjG2W7eOR-<35qB}5)M!p>vpCk_h5oUwM|Kh5PO8( z>qV3=k0(!GGTvjfkEc(pb;T|X{gnx^*S7uTb4F7tCgsj$ZdWR26d+YCvRK+flcLr1 z4x@rK0H3?%O&0us&B)TmhI*g7<2C`(j~tZ96spaTFipzyBF#+H0-$KhTp4(GNU2vlVhS!^O-V zNyoWLoY(BOVX_v6f8HGH6PH-0^pW?dD&Pqutt^RO7XSEvrPZEM8%q|IBFIPCGI+tM zXk$*%CTcCfBDF}#!~Brn>m94tE${B2KAwwQs@aard^R42Uo*S9$idA@4|L;iCw+*a zWYuKOxD_Y4ChN`&SF0bj*Q85wOBAVkzCHYNZ*Fd;&O|}=3F-T$IO&@Zlm~|CFYFh9 zD==76^Ie=M$shm022aq8m*C2HQCEOw_(ML?=$%9|U5)Wd6?}MzdpZh`v&hfRKPBEg zInI&R-!b$ozA-LYEi?OQs`Eatrp~ICij^s|#=Sez4DVX54`$kiudSZ1efj%04!SWV zu;O#N)n1@NdG<;cj{)WHWlFzo9A0|mf2Xy;&feZj*r-pTG`d%OqPOWNL2q~$ERuXP z72?mxL&3w8WnNdwaTiHuhm>1cx00=mhQE(s_ z-fNt`Z&O|#)%$BlIzkFuQSyvS?ElG-FZl<{JZxMJ2n3+9=-R<-7msDeoGm&bagGq% z>zm&V9j)@4yco|B2EU$n=7it-bB}$ssa%63)pmb7S@|$HiLuPSO*Ji*?H)rS-@=x> z|J?AqpY1aX?`Av$d|t^#VK%2&TW<)9q|*9~ira86>c9XF>-ISv=*h(+o*jWPE>M1SzQ&!0)^g`n_(UiRp>OQ?amg z2wYAJbHYJlnuR#IpO|%H>us_d5umoQp7hlz#ooDa#%|`lqRHTE^9wKc+?qm9b1=x7 z%^U{{qe8PQ&ke>HIUcUE|KqL|#9*Nd;EMyn{zb*bVb@jVAcEWps zOYi5!mvC6}M!)K69nR67eXNi1o5iu$HRQ~|RU>n@0$q;m;;5WvL3%6ZNB1lRd{>VH z=`*JIuD{d7`Uq%gpYzn>-F$a>rmg zIwc+MocbrG_nEfp)o=|fBn`2<_83M|OC0;Hzu_)zPaF|g<&52qfk+Q*w%Q!$QF)#( zSbQLmcr-s9*10mjG9J87l!>u?^nA8)St!i+?u}F}9-`=l?G_xpLV^6c<&)4ylr~-eupak>2)rbtz}~Y zmY5>;q)=seIkS#E5;OgiQ|HfY()H(op6 z_H&Z%XIT?}I|d|cAz!TY_%MjLbd_}aVl{s8ceP!&j~A($7i~>z57Bp-W|tLwEr*|U z?xe?dJHL{DZRM=L8PK`HTiP0Gp|pmY`RgbO7M=U@pvZ&ly5z3^CaAG} z?cX*G0PsX_V{ZgTez+Ha;B(A6=vc*7nbG1wtPuMa(V1@ES^wX*aIxHX*Nl6@P2Ob7 zEIFgh)LeD~xNL)aIIp&v$3gV#tiWx*yJwt(S8||{COnXL&xyV2G56Pkn4^y`HEn`H1yr-);Js4P7$c$9-*A#Bm z-0KWPqJ{Aw=HZr+hvGItUwcJ?0TyMPvl;VxKF_0sdl0fsiJ4mr0MbRxX;HqTOl$R|Yl^6f{&H7eJO@6EHdEN-YHni<1BdC>_XgEROw$YmkZP;>p zMZG8OQ*0r4wj$@YPNHSe$b?FIvI2N009FJH()k~Ow<=oX$6dqYL?V?=QREaN5O3L+~@eYtGrKj?n%+QHf zJo}y_d5sZdZ+yQ)VAs^bba(jU3i1{%a<4}_PP5t`Odw8yvsth3@R$?!M>tw#w)r=k zT9dGo_D_2+gP|~q&;R%@rb9k!OmOCC7x@O=A>kz`-gPv*^H;O;w3 z4=9RY%8&jyAa8frbxJ5)1i2K)090rl#kj%G7UPb5w6_H+Xu{r;!a zJVSwR$K<`-cur+Mjh#|E-@b);phctrdXc`D%h)<-cfTziMlhoXl#x(C8HAO>8WgQP z^l_*l$`?(|*MXw>Iz`##6~fdAkm88x9DVkiq|@N@_oRyhY$Q@y1ooEF9mQyHisR8L zYV3gl%t~1^j-dCHW7>f{z>G$AbX`m54ov&%lQLGjPeUq)v!jF*YYz}mwU4fl>A&MsLFvg6*-#`h&VA~v#MFiaKAhvzHD8>2rkQPE)Mx=)#Smx zJ($`uR+ijOd$*RECvD)F=!Qmqp~=H@4Zj7_ANT4TdowvXI4HDkQBu5IM*i&gX~Kkz zEa!DkVMDl!h0Xge8I6mT?eJwEez{6o1%aSnNk{xik(9Nv)_6Vq0B6m;6n2J*G2a^m zvYXO4K2)g_3!2Kfco>!K_rfX>W%uwNj`2of-3RICoD@_M$h%+EzV&%6o6e~z-VZDT zthSvqlgCzUcdOZ%8rR~Xpmy&YobH7}s9&UDoO{zXadBNtU4wRb`5vBWG@ z@n;8=9QLIq6?5#F{UP<~u#0p;04d6$$D!7ENG`RGTys%Vl7IrHO{Z+4h3}XC`^5KJ zYhYLI7CXW#gk9d>UpK|R-O!#Uy`PqA?lv)UtihvGG^dt>KIbn6_#6t>g598PH{1Cw zDmrXll%%v%L|V6-$_#B;G%u0a$yK&gQFDhNJS(dyZS-wq$VV=1j8M~a$Uqb+8H-94 zD;5v^ntK1Mf32lMDzBJD8%rS-F)G_v_-r#{DbX}C9Qe&-pW>t3+0zrUt{-QZ>|o>m ztuYi17xA-O-&%o|ohgrxw+9c2>jql9C|zFlger8G(Qjh()yyXvOS9}!mNz!u^Uzj3 zJ9i}=@95yey-3^W1h$tp#f4XF*dZVUK-LZCNDXjy&H}9?ZLdoUR2f-X;2rO3e$_Py zvwu!AkVO94@D8LqaWy1Vs|i zel`q9`N6Qq82xv%JG)0JsS|JcSRy;A*2z&iPE82*4$PO3yBr*K#oY)MF$_(;1dGf6 zBk3&TqWqpVelJVyvcN9gEGgZcONVqxcZ0MN21|E$O1E_V5R@(fX+%(INs&+x1)lZ) z?92Pzen02Txz5ZrbBvGwyGx1`#vMKt)#7Q>*`Xd#}IxEA(gVM%R6C7>IxLk1`&$ zvVi^`rC-2I6Q-dqt)w!*1_A897yBfncu*gOjuWXhYT4@Dk=4({>Z1Aa@myZXpCDaX zJtnIf`(Byf%!$C7MA~*_okGT=Se=jrA!=pL_T-hYe!}GF*~zFx|L=9W0Xc{AG*A>h=#uf}@tr*%PtP+vccho=G~?3tGOO7S?GRDTRIQx>>j@es{lNOF~a{ z2qJzWFT(5VS-npcpJn$oeJ;Y1H7cDUmo9bcpguKS=(Xk($hhfvyZd0s)M#8N8(}i` zNKtSSJ^AxU5T}HYI*01`bQV1;c?&h8Wt0)5pibMqxg+G{$M1o;sC>O z3{yk}08_yTA$#H)EFo|dD2eY!v!j@&98(MxF(>*HE-I3CIQJV0=0BC1&z^wQ{hoC! z?auNcGu+ZXTjUV6ubtdmz3SdzyfkpJE6D7cCv%xju$%ib+jKMe$r3;PY0FES9hVR1 zrX_!o0bfG5qI_sU6x!8FyG;~f@p#NuQ?+7gbH+t#<4nS`7yVwn*4Gss25nuE&4CO} z`oz&MdW>jEOf2?%DeK}YL%%&xmA}b?1GZbZm$Y!#u&>xS&SgDF9_kX#@Pb%u66gy} zIEb)6&fGj8CaIjoKhW7Jkw*g?Cjx!kY%T|Mk99m$yD(PUGyGPYF&CR`)Y@mXwXDk+ z=-uVF(6QB6rT%?SZecx2&;MSgEd3win=l?i1&#NiUU zu|>0$oDoLr--Ka<6TTm{T>`wkcgw+xLl=1g&OY8tK8AG5m)>i= z(-Bp8__cqoc(*o_-c?ty@NV6}cl&WQBXIz%Y@mk{pjVFNnyV6)iX9u_SR$x_XgKdu zSLm+SyC}456kJlN=Xm}qKasR~@%`S3Aqx_ug`P79A@}}c4`J757Pk%9$A&HrmPg?5 zd$5B^%D^A1(h7x5Gc&Q5X~PfHrF|pi70o?4+6`1cGmgj75g}QqO^Pg z&N5FPo8U-UeR`QFsoF*AY}OJ%%oq{}VMOzBo-RZ-iOj+?N(}C6q5g$$u*p0Us@<%sszCh z{?Yn)Sd3I@gFyfMzA1n;ul6aBtCC7XaVo}G4Z%FRp2*kp){izUh(yt-d$~2bd5yQ? zts$1^gVka0b!hYcwX=KSOA6c4j_vmkG;|p9inkrcf4=;jdA7`T_4&4)T6@pgC9w6q zOTfc_{P&3*u@vXPJYq;WmbyUMDDxzaD=nUipw#0;F8Y{?`JAfHe-iJnPsA`Y5%ARj zY{FnJD-9*d%#GP9DRss)+de@XYGEM`!QgUkynEChTypj8>pR%o9i0OuONUAiI~tA+ zFz`|>0e>d)`3x*#6%+>g2g2A$=svc10<$-w%d#zEM(@~UOS)+(SD^;zM2wHnj$t&=aT4mYif zH?vrP2>7kg4#?iV#KF<3)q9!(Hm72boeN?TkB-0sQ9OJ~A`Xo!ecn~1B)j(^F=e|8}d~SK3QdNePrEJS&oPO6mOb`DrL;^SC9Al>ZRmJB_ z@sKIxr~TKQ#%Tzt{HcBgEu?YG*Iw z)yrP!7ZsVO!KO7c6XM%Q%zok~GS@I@&I(0^&lpRVgM8c)GPmG%gk%erF<8eVN-is3 z>`;$XI|su?gY%aGHO9*^Muis{543Trgu?O19Z_S$!7*5r+F|U(&BZ!%Tu)dXZZGSOm9e0ZIOtm-TXa;7>y?THMZBP5-`r&t zBS%$#-71jP-lgt(bNT7Lo!Z^^Jcn5~ZyCiW^7_nxEhdW)fvHVkmHegR9Q<3Z0;9#M ze6n4-X-`2ZVDj-}Sfrikp-Y%azHSUo@*w}Ttj9I~tth6fkwJZz8IxbSBpzds#HU$} zq&4#cF*6A0dvd}G+fjyLjUptj*a!3pJtlHF7?+4Uz&;Y>lkN;sZ_=|!80XrqLs-u*E z-R@H&*oZo8le>Q{w(jy5>jnx3#LRTyP@%c#7rBAn$5{p+7~wNp2L5gY%a)wiK(r!o z7Os~=>!c(NAD^>x4@SRuHQ)6SL>D5ADCgL2oV7(|z_3$I+pF7D|2U^|j!T6h2PmNk zq8uMO^aPJYuL6uMft5soe!DAEmZ%PvqK@Tb$GZe@en0L^l2w;(pf8ok+2ti~lF~P) z{_s3dIOK~iu0Ha13Q5X%&u-kINiZl!z-W9=A+3*R&e1g&Db6F|ft)B==3QhX=n16wW)-1ARdK zjsWPm>Qh_v$Qz4q^qmOxPp;ehkjGIRYt3Hz578{8L zDF(1{eaNMqsmlX)QX0_V%>~;Qp}z+wnrz(G9-N*=#W(HT3l~MvhlhoeeBT4Ib5LV3QOBJ(Zn-y;gfdA$NH0eGiUECa$H)-_ z*n*f9LCEOB$KhHJS9L{DSUwMQvj|D_)bYD1)=Y!q2&9y->O;99cu^5arVB@o`C7N3 zC+PqvQi7H<&yc+vdC5gXMiu6eE!AU$T%G9WAWy# z&0e8R0zbGhjpqxb!rawXYfEMQiYiA1cVs`~fJ3<_LYp+Se_GuF=K^P~c_Z2fl}1x2K-3#M#_)6#gnoC80wRjRJ&E^^ z+jfa$%eU8^R@|TcyKrA%$D=fvRmo-&RQc0mqO~(Clnxaf-8A1PXis$)pY96NJSUIA zogdw%6nE$8g$v(|jH~{1{~Zx2dH>f2F4>z3Zw5#*`pFd+`)eO0RfwDgOg&4@FioXg zuCCZ$Mb4xn<|_%7-aYi@T^yl05c+xY)rw&^hhOsreGN-+QzZZzCXLu)LJsE@kXWeW zE0xm|z{pjk%R?S4A17J!Kk7q$BKkW1sq1YEwea7aR#i#+gFhq6djTL^*GjTNJUm@r z$nx0zP|eFMmO9v4R-PAbWrpBv1$&v{>Rpzf*G67`29?FX^?yJFK2*}r;GsaOl$+uWCK<{R_?5FXGEL5nC$+Da;-Qj9clh5-vZ*wX#Y@an2zfK|6wH^&+3Uvv>4gbWj=#5IOtYzkkh?m4AzkcL&4d6#| z7F$FK#jKsVRA|O5r%1Q~;fH=}x-y#3&DNjWR;M8@$h?5`xKC*9!HvBBixOg_1T?gG zdEr<4q1vKqGa9+>tS)qhZ|CsWw5V`pfMYrJfjahSo}-H4P+yrBR|j)_hTJ%IbPK zu}Sg_O4X0mvS{5N(xhn4=`^R(Q#AgKe zi$}*1R*#0MEbJ}^H{raXWWg+2W6{oWZ0{vx9X1 z_-`bG`J@*zVB{6DTQ}9RaZ<^sQ%wJLQ0f*zy<0TIKIrZ;uM&jOJ|ZA0V^gk1xzhw4 z^yghoW9qTmcrHj@s8xqFkAp9%6x()ulssAu8yF}s(8i@iXst+b2Xq?Lps9mxyp_xX z=K(aYEp>zh`S8_5hDy9(CzUBxNAsjmPKB}syP7f!{~r>~2Qy(V;yH{#^lIAl5K=@I zqQ92M#UH0X;5hb?(l+k7NR;Gf^H+AMk#f?P+%*xahz!ygeumCP8om&XU*}h*jA$(GaU_)77zXD$aPWH3g;~;H} zIk9;7ad6J>u=gdZj|&8>1Pn_$zi!G9Gjg5QiwTsPF`XIGkI4XAFllw+aSqb9OO$#f z^IT)@>RGT&tp)DSFJb@qeD(5K z23YHWRMM@(47msy12{b9I-+{|wrcV5H>UC1XtM`O6x+>`ZT}7XJ?=Z1bp%1~{3VNY zAVd8JBx|M7-xp2CZqNcit^Up@eNLgWb!wxMoPwWRJOw+ic{oKj0{WH3RRz+H>HuRu zRKQ{48CQD%cu&EI)>PF2O?&mI-_A4e1G!361Ce6|*55h^p`b>l{5 z>P?SVMt*otT4X~|0A`g<4ERNtG|6M$#NSOs%9tybP&|#O%U|C}B zg5MFtsOxAhTdX*E`Be3^FCw(Kd$c+*_a8<3XE zilyU>HJefO5Uvonz4J*!FMuk(TqP~g5xGOyh>w2XsLhnfc@OV-^FPuolgmT>*eO?4=ae8f;PfAsFmXlwyD&EM2NQHHvV9Z zb0rSk(GpfJ@mbh#q%b)ZhAL5(UN=(tzV@IC)@h{ty2n>2m_q~JxT1V@R?|Ijq5hBm z9+4xd+B~TSMTII3G+Z_&nvfo%=PA$`drrcYdznr3becvMtn+mI=jzVr`g3v8jv%-0 z{(O!>Og`739fWuUjcMzQUF&W9{X}o$Ly4vnoQBQ{I+m)FW0<>e;9G z%ll`3x1205Jx0UpdrIeQGZ4latb!2~PPB>0e5?m`0q}}*K1yT%Xt9|LUI|c1PDf2i z8|=~UvJPjKuw^2y(;LyHKJiB#Y~L@|zbZVT-l;z{eJU`lTH2_!_8dMbb%bbp%h+&J z_^5uHp@ikPh&R!#HVM3>zUh*#Kpk192ef#j@t6t~QHi0%857SLe&}V|>wt$dqU+L_ zC&u*Hk%mN^?PHed+|yaO+NtFq!TtYTGz$HGA!Sp;a^p^y7d5Jm1I=|7pvZzV=n3G# zWqt~hF+<_8?1Z2DuySqrN0zzX{DPp)%r3I4;tcugR!@I!c+IUat)+3;iqXYM8OUQpBP>pW)Xf3}xnJYHo)e2fCJsU=%V8fR#x!H_NawT4gH_SUg(&97^kC zecmj7YR}6+B@3)dbGt8J{`vc*wYi$w%&;^A@*h$_d^FU*9|=}K&+Ti{HjNNNvCJ-U zG?vCARcs$AD4DviXvdtCy;g_(G8GFSCgE_UBI4Uw=)f^>=Nn0&%*NXR&72a931<4h z)|kIjd1p^}TQ*;4DLO^*FpXm2K*$2|Pq8Hn%30dJ8|17u3gXsvE!@=DeT)T|BmlnS zFtU$R57UOHrMBW8%c!v+2JnB+`IWT_9C(MDklFtg%;KK)==_{K0*=rZMW8I6R)@Z& z!nlDR08E1`C9olUX}e9$Fl<~a7S?n-7yu~YC@3nCDRwJ-5SV1n1RnGDAF;a>6OrsQ zk3n&$meH2!IDQNdzox?3A5sn-*a%40{VLR3G1xu&$A1+UTqJ7{0b@YQmWR~Yg$EG{ z(&}n8o1jnN+;o>&B?Q8vEL}riC{7Vaqe=uxH7d*wM%yeJ!!>TNqh5)pw5YVd)+`_` zR4)YpB(7IB`y_9t-aBfNX&)U&;;!1X=;xuwG_TpCe_yNvi7>6h_2|f`g#`+fQE=2d9YIvNj%H%y`ou} z^u}|q<8o7@ovob#A_E>Kv#dl{j&XpK!X*1FZ3DSMK=_c<)vqIcWySu70q>5>KmH$y zx3^UdGWfZFs1LUTKIRQmO#*=cIIoGZ5 z#xI)_(%Hd?O$BC>mW1aeUuL5TvewRr9iD2J_A&?ckMdT!v)pyc-zmccrutnH(^S3U z_awqwJN9~QFhz#hN&KAGJgOja#`)*8+aKxJ;QpYXYC1)*kvW+(x%1o?pXAcs{P^D@ z##@S`C>AzGqwU1V{Oc*4nm^VHYug`4_&lnJ!)MzTI-nPTlbow=f-2kpztM=+t7iFcKMD}%NFmeXYlsTo?N%x8QJ8=4%) zpf?2wTd`rbxrC{V{jM}&Fd|Z5PQQdi2KB&hBf0s-wb6)ADXHXK`~xdnQ7V_}zuL!efbEuwD3y2o2M%SIV?9JIOPuFq?B`Hk?mTgt`t3d=jJ;gFwr#1emHo;1E)Jul;pMCu%-_vxg#{HK#n@mzQ= z%-xsmc``3e<=j_8i+XjNM|bfh6J&Vfjrlcg9eET9IPTaH3oK_f3IxjGk{q2n+^RxI zj)3^69keo!NAvov*vfNJ>QjHwj{{2pmhxvTvb!$h3E=%yJzgtR&YWkf_plo(ygJzFS%B_lQAjZ?AzL%0#`Go&DDq?j3_8joOS z$m1HFvU>Umw@an_ry?zBs3SNGgdaqsJN8ya$F@FBH$D65VX~dW^D2q4Z)L5ML=|jZ zYEzOil}s#|N2^MLXZ6ne^6cryqR79hu6F z)F7;C0v(q&9=1hWAMRf*%-XVfA5g5Dn{IGMBiq@;*oHG^$7lWq99t_P)3L%6Uk8K@`)BbaxlbcBySK#_-5A0oT{9QU&lkz}I{*-cgqpb#?U|0|9dZjfl8BMbS@D?Z^ZC5x-MgNK{;-6% zJNOQ#N#hyVWxhShy_);}ogl$pk@lDBYRog1Vq3}APsPQm&zMGHOy54S4SG5&CHOhx zlzH)l5u=ZxgDOmis#CRr4m6UB%U!w>4E6r>{1Y_>8iWtR^N56uxCXT@#^cjZ9d zqx;3FPqS9CVsp$qfe9$D5(tXr`8x6Q*S}r&>7ejyJ_8|~7!_ZDX@Hkxt=zS4OSi$d ze?Yt4F5N=K)J5h8E>^*5c%>uGR>@ypja5E6-0*fb>}L33yTE{2uLJMYpwvvobQ3gT zUNLg^zfZfeO_~Wt|M)LrJIJURy@8NdC=Aur$*?yQ!u}X}%4qlBCfB+Cf_#*_1gSZs zjJa|&NL3{Y{2DQJbx_{#o)1;yQgXqBsyYnP`5aoh~y2m@55c6AE_o_eXU8qv?hW1 z#aHqH`urwd^+1W#y2%P|Hu3958@MCcOYQZ2?0@`MBf$wm3km!@{i1^*pD_9VB$*SF z^jtW`v$E}AOTbdTIl_P7G7j(Q zAXKh+&%%)^z;s{8^xM75BK1+j>_-RhUBx6J2`w4r(*+@Hkvz7Cb%s;(W)6_fo+v+Yg zn~_UOsmuiJ#6IjSC^j+-yQuE)J$GcQsU_U zcpnFd9se%vGJW4ZZ%?o^AZaHxz5~VX>o69|=P9t%M+DEdmV3%K77AK0$eH}N1a&UB zR{-S`GX^l4mv#4Rv?Owe)*|$x6~;Lw(D(>to!ao{!FW&m?EpaE6sshkAod^sugMOC zoJJWuWP0*g`Iz(9F`4XEPph{n3=FNu)W1G?7YwC_SFEsWeCOq4N!^x&iBh>@LqYG& zNALyCX6e;J4f(RYw?2Qmv63f>Lw%@0s#)UIeN6gd3V;cGWK8iufJ!Vygqaq8Uvl%addPDdEQhD?@y1#z zlofR}ijk^=;iu&i|(jPjUX!bi=d*AOB=Cn`H;c?9aEq*>@qT`YD z0pJ4wcfbs$90{tL^DcdJT%sr(e@V5G$*`i|m%D0B%yFJ{j_y;?rO_D0!e>vJQ4*+P zRZ8o2$Y!G<3qTxbmd&Xge_C_=*eUjnE35PI%Sg}HvF~!_}{C6UiXa$!IiMf6#I%syuvOi5`y~X4|yA|*g55jWtJ~{zvSc|x6 z55&j0+eBQ18$_$W7EWLbqGQ=3CsGZ#aDlSGWbdWJ*`u2l_a?Sm>(6})Z3lOT19(=Y zk0OAPX~aXdrqA)jI|g(apik}Y}al%u2!CjS*lar zpIj=XW@U9nT>VVNrvVPaE1h7U**>?RrR%&B-&^yoZ~k6}N>T0wch~O!UF*H*N?t36 z^)pRq1eQ>hl ze^BPk5=jPdGDGQ8GwnBu*~7Y2i_$-Q&tha1CAO}%>nSU4p?TK#Q0V*pG^bEy8Z~#C zBlil+g<+(XeC5s8x8c&QTwu*tTW};5A?Omk@sIx=k%Nz%5h*{9o*cVWn=D&EvJ8fQ zMUMhB$=xaf8gDI{Vkan}dl5kWt-mrYR#~R%kw;-x3QP-|HL{}Avrl;hJVxK`l2~k> zsaMs-JXX7VvJAi?@WT4I(9mXRg1Opd7zkF0#aycA-c690l9slcCTsw1yM$a^^)x z$d{Yj0-uhTAMV@VX>=U$oR6B-MKXL(H!>lwvu}H1ult1Ftj3Z;TGq;`Jys)MpY=LQqi#n z1;i$caH4#(vxlvjYvOGuX9qN?%Lyzy)gRh>a@jZNaTS{DI)<2>&9*rf)NngO&V74E zg%`|O^wQo{>hQpPNZ2LpR9@39=x!(7v<@E*INaAe1k&lm%7QTz<3$%Ld<}=iB!4 zUg>oxHoB6B@j%{LCd@-f%yF5B#0gdONQcH>l}<+$B(}?CMMjn;j?dU2f}`)6O5a6J zzr`ClLYq7<`Okl{xGh&9{qF86x(~`95H&C+m=4UReWC*Tx8g=(Y_ok%^#pK4GKKKz zTRGcYDMcjSMRmcOJtWqqyL)7y)>HsV;JUg=YwBi zAn-nuUmMFvCli1E`Z?X!^*V1%ov)7}tMP;pt5N>8BA`|HGhm-XfTQ^C5Hox}o_qyl zimjb9cmIKdrCa`EU+wpdTUHkz0dk5iDKw&JtOa3CIfj z;xMa~+-n>v+4iB2mMSu0K<^rBtP`HFGf)s!OkQP@s+l-FDNL{Xb3lJfi+Rlh%7mE1 zDauM0%y=D2;qijv^`m#i|0v{s_mIl5_TDTU8vt-Nu%BY-nSR*c|0W!}8*WnlVLOW;sHu$C zzpr0|HT7V#OQGq(5d%*Gehc*Uh%7WBW7t zRJ{=}<}M z1LLjG#y0ntjV*&E;h7$@2ITG&9=To+sp|m(bgBOlMd=!!FHmvsuUk^Ezv7t?HQ_QE zk|AS15F;e7f50^0Av=3n`?twZ@-Iqa-R7)pTj#TyIyb7VDm9aD=4XrT07#8gRmxVG zLeMi+&@lsu;4r|Vua+Q7+Nre^iwB0Q^Ka_dEdm4QvFNi~$1fSVn%9q|{YI9TGnf@m ziE)8a%g4K-&Q6?;WZ%y?YW(^t%Y9=_?zqLYbhRzcuetVSZP(3&7(AT3L zI(!aBdl3M7f!AKL&}1FWN<)MUF%4V~#O!y5-2llW!81{Xh=}+?DJNgEjl$VJu>4JU zcDr(aWekAAAfO=$1-h@7M@~nu(@`?85P8pG7*9Af|1gux`DJP11RLXa;yGNfw^mAf zUPKqY$OR>*=FnJthe}=yHx}+*|E@JYRF`*k-pOs#LzUdfU+1#LeLveHE&jpK-cV%| zmu%gM8Ycd~{PO|scBg`oG=w5mxk5ACZr7D4m{t#?e{$EMT+`94A?bT4fGh)D%-_w8 z1$5}Y&P~mKT`uOra;2!8ZW}AgFGuo$>etv%c(tGaw}`=ACD4N%`1{^~X6a>wi~N0b zzW}wBa2N{#FS|=U5MN2qjs=Pxvw@Bf8LoVUm+4Z}yG??+pMftP+0sG>yJEwI@Hml)!R6tXaZB>qUXyGtft zrgmBR^X>>h8;mVSvyhb3Is0%lgY+8&i6(qiA2Ik!$*&AQMFp7lh2fY1n;;} zS5-)RlA$i-b3F8Utc@lOLT?|{7(*m_VwqKYcryd<|IqCzy)(s}Tx2+v6d>T)ZiSkP zYgnOpS_y1RfHSNY2HYtpQd}e{I#el{Hou-ZP9ko*)@nCT4PlZ0YzN+j^-=n+?FMAmHywn!3%X{I^Nr#@0hxSgiZeO-%V8Fm0c>( zBGZ#`{4^#T?wUsRTZ`9@cYrn1oiHX&itNjE!8B6k*r6!JnfEU;#>g*1vxA|SCoFPS z@@<=*bO`__ak2&ZFgBLB&BvLa}TLi{6Wt>S(k7$_{(I)jf#$(I7h5GAgx8j^|)?P zwQiK5h8>4wdlWyvtU~QN zqc@(_kxQ*is>_L6@8NI8x3<7i1zH<9#EH-TWPpDd$p7))Hq1e^QsjfBFc;H$c&GdG zS=Ids?68Yh&=DYn`PVey@^qH;mgwsb;&fn}hr?8 zJrJTPyyrZu|GR&!MuY27#=`t$wjzV?>1EkWh#rHCcx1Zu))G!Q!`QaM?dJ6D(;rdX zT0R+()Ujx5S_-hak53@!2Y_ZW5G`KI4L^y~hRV2^jzg9zaHbM3oh zR+(tO=gh2E6E+MP>@DCJJnj_e5mc$n>d^dB7dhM$|02>ER7pa5Qmun0c6u>TGiz$u!c8uOff%@ULty^ip4cfncFN0($s}rWSpq) z+uTX)+l3xMp9=pvkQzNwNMLaGuW+TH0g+Y>*UM?Guik$SZ{>B#c}N zBh6j-#0$gQI_LgP4%U&OCdvm2SLaRy)RC1SZ1o@@^}if3hwCd($OOI)qy5y`DXJbbSjS+p7g^uM%dM3(>4m9tR>)FMQo&nww=!y`rFzaW*vh^oh{a@A2 zN?9Vf68d= zz&?QnJbq5Rz=$Qzj#J67Wuzp7noa*{?8{EB%;RfTJX+EKrxGhV-s=0hp8L5}-+R^_ zsidH>*i z@?;2I8?}*(*CUM8=xgcM+md_$hDPE^FkqLCZ#WZ+OvuR3f6MfX3`x;?^*&NCHioOI z98YuE)9?{5rQO(@+gFNFnM&TrH^%=q0}VDQa~UjqCaI*6!t}N-aJ4~*oe|%(Er%X~ z%J!5v9Xb}h4W;e%ZL3)fkvDGaq!8AiA%U9)nn)MY>@R3A3GTm!l?o4*PWX;4$SLTL zQ@0yfGqZUF{o}t+q-?HcM5;y6si2H`f9)GzG8?T4PqS(6a%|nyRdbMg*z7ttIP{(7 zjuWv$o&6l^RHyc)Isn(lMWZJE6W0KW{S&CXX+Rr#`w|bNT&+uMpv)QHStxn9e)+!n zW9^Z#^U@$(-XQAT?OU_{&EnDxJ=W{3q0zw-Zdx9Lb~-Emgo@%uLkD!mU&jjUonh03 z%xQ07DAbJ{{7U9~z0_Sj4u!gFVDv*74mtP~L1Hql9?4`vJwf{@+o40te#K*}WQqC6 zKc8=ZB@2STg`Qg6j3|foDnf976|J~RL9pRiXiHPjb-jQh(UgC3_J%K>ApZj%oX_ns zEg7xXlYTvk{h78OP8_upm1qcA*T-){XCCAlAijad?kDcDAebW@7lfTZn}?(vunXDN z%xTkfXlhleUsY`99p|)LaYa+)u=vs@tk56t?!DcVd3=OEd~JDxk520SAntH}eX+t* z8Ku1tjWxdZMnS)0SN$7qsoYVZeV3ijyZ71u_^;zCMaV2aWM@E2V8tKG#z`bgESP9D ze?oR5*rG*6OWOTr49q5nOCCdKX8OnkjC~$6xd>D+7R7 zEfAGt1bUqxYu%m8#N8Z*{{iFj&&t5lJ0}t6IlO%pV5fkFGqXXr&F)OkU&dc@`K#4-`TdvUC+b&8_k(F=26%Y!U()&8Y8M;Ou0G50#zog0G2)IDOm$K)??*<(i@t7U`XeI@i?rZ)!f4 zLlX#(d{IGNYQOB4PxFXi&^k#;d&Ipv7}*S`E2letbnm{yFX_m+vv2XP{hd6r;gIMf zd6{eJB?Oc`{<0(z5WqZ-AxwPPj8ceB6DRE0DN28@siF~RFsYPiW`zll*EzVR@ctP+ znAK<3KYxD(x%^_u!6W|{c|csze@BPMv|JzC>bWiJnp#v%(d1^p z9yBsmzy2Qd;MMgV5#jKBeQVJOd$J&%aXd`k+2ZKCZuQPP3k7uEn~#yK*_KZJ*LLx1 zhrx_X_*8V7|Ic5k9cA1G6N5_*=6{pr4M@fxF>1h^zh9yCHDkF#x3jsJO-O-4(^i?y zyA|%X%O$LJg!_3hv0?1{S(5?hqB@7%>A7@R;n3QDuEaC_3X%XjZ8zaHMAsIcNi z(Y_xh|Jv2cy<41^N4nOM;+H&3j?64Y6U=iHDo zg7m`*&HK8pbp(*AZyajP3?EZSTF=uq} ze=10)sFjrYILl1YSj+-6cu0-N@rd|sx7hh8UOZ_X!nxrtdWP|2RnmyZm4)?<(#*(> z0fb7Fo)9{UMeDpE`9<%CyZd`ZO!j$$#-MI+Dyhc(r=9O5#*d9vG5-W?)L}UE;gASM zZnjlPx?kqwGo&;bFjP73xAO9&W`ljAPqX`5gv9;}8g4`GUylgCfBMFgX z+zZdD(0~2gl3g)R0HYMCVk1aGP$gK(mdrHDvFv0#<>@b^jZEz*zyIYeO7d~;;ALEn z$+#?4;?cA*xW8WpN+Nvqvtg4=K&{*ztB(Y;-2fI~%QILYK_v2*uB=LFcL7Lz4XqoZ z8*Kuy)7%AA)U4Og;p%-hGPfLPdub-k75q7N&DSliUX;8GGU*g4FB1CUdRCj4FI!;n zF+{?f*%%S!*z=Gf(c(bcmlYg`|65YVb^fI`?eEk8I^{|A%Nl@H!<6%wKdg>=FrRa9 z%b%B-GS`T;MlV3%aTQUu>-2iBQp}>KD36ioWfmJUdp$STLT&Z_Hed;J;R4_^l;C&` zqYrwSRcV1_#P77ZB5# zh+*|y39|FSsWJE8ie9}#d{5WQr1X4~pzGV7w!3SU-(NebbSp4LRpre_eDfK*DFL|F zn5EH>*QU(tuU_V>zs%pB+`m4!zq$>*zwf)dY%-@X^_86`185)~G%A&N4_V(J17Gx7 zeW=C9s=+O+$j<*^`94>Gxw&H}tQu`p^mF~TY1EA`hMBhnWlex@>xJ zZnOefH*pifQ_q*l?f(4~`7%#Ha`&(EfmtS9wHPNnE)w`&p<{U#!6fcPIQfl{bCR8p-7f2xsa}p#Ni)&M7_jyMm zJIfi{Xv^$_m{fiSQ=I)i%vY;_*NJo4DXIDdRW%q5?uS?=@xcI%h_xA8BLekA22Hs`*8Y41N1jO$Ui;d&Q@x zzUNk6>zF=(-x*HK|2CY%BW9UqQIt!1Rci>f zbVWx{#uvpmD|^L_kSbzYV9@lO5QQ71Kn0xjCooN+)4Do(G7wrR`CLxC8DAPoQ`-E8 zrx+y!nN6>pmFh&chKs2x_Agk}&2~`_J<)xt2AQ}m2y(IE^3zMSP~RYq%G4ExFc~j@ zk_~8m-aY9046RLif5Pxppi48Kt%tNI?;ro2Wbj+5r3`*8lxXRjKAb&*q#sA7JOZ8L z+c;hUSIs0#a}sxtoK!EosLI8>YMIuN?+Yu>=Pc3JQF7`RaKzHVfNUZmClTFlE4fqb7Tg!>X1ULj0tS?*7z~y zg|d1Gp@5T;nUPZiXf4+-+g+vq^^+ryhuI{Rvn<*5?smcS+@(G=Dv9`d~9v{s<*0_(08}qrRmE;`L$Ucpx{jJ7cryRAlP{&Z!V7 zu~99c^<(nO59Z|f5vJ3vTwJ|P=K-`6k<6=G`61b7eA#_f5$VFbQ7MaQ#rP>)*pfgv zW}2u4l%t2#s@-v_vblCBe87=M6V+w3`b+>Pc1zfWI<%XJ_U7b+)DJqc0g~x}Si4zf zdc5y{*}+?aW_GRi;qkugq!AHq3*UA&S501`vmKm?>WV;MT%vIwlEwe&&u_`t)GAA) zFtK#`$cIvP%#O*;R~Y`?u`9Tq82huUIo+T@cR~>aq-qoVVo(fs44@(-geaPE)M&F4 zMd>XG3LC*R6*&~J0fmjPm?mLL>w^HsA{b$ffvt7zw6`=WbJ#eHG9^~Gc!PnzgtHE zk#V_OkVgD|q$j8aJ>E2CJbC8A^AKwn5U<*=2@d~L=UNh@A8b-$|FOB2fF@%QQT8&x zk$R4;zzo%Ob>O&01!u^YwZGXGSwy^d)@z>lZ3tqOSWBFd@6T$;% zxio0Zk?yLVIE|SK4hiD_i$82cI7IUs$AGBo1(Bj}m?JQ7X3i}Ub4OsU5!EZ{DDQjV z$Sk3$!EKDkoQUecE7e@nTP8FlR1R5|Fp&KXpi)2O$zGehM6S!^Sea`$vWf8pow z!_PLk4@h{5lyu)XagFq||LEK+2=4T<(jftWd^=+-iu(?lzgv$a)%PE?UEIKoWsS72i4Dpw&hBF@_;ikLdX9FD00=yo0AN!Shx4zQ_FmQm8w@#lUG&McB zOI$pg<~p=2cu(AQ9Gvo<$PNK|Ft6Ywco7&Y`f{Q9O|f(dmcV`k!hgdDK#T|m>Fh&s z!d=j*?;MjZL1wTPuQr42YoILR6O4>nLnHs_TJd$! zZm-Vkb*or`cuEMx6wA~w1`0}|Btq9xKv#?~=Ld8fVl=1#L$aFjW?u(iQ*h--`7UPf z4d~BsrZ`HplHD&h3={|2Yw5rDicS4TecX4AM>~_J|Lo661j>75BSIZTXK{>i4hX}S zsY^=B(-PB_Q53H0|1x!7q_w&TtMp;Zo$&SKU*?s0zj}-)Uz)%=*K<;$&q%_u=pO-0 zeee9<(fLvgq~3J3@azUj+kFk%oj9UdVThpZ{I5-c43|2SU<=ZF=OaThfCv9mD+Bju z>9O%UX|pNwQa{~7#Sjg1oUwXSc6NW)J^tpOJ^NMHV>u zrLTR>ijZ&pQ*VlWB(}Y53AbQ#A}yAd>qqL?il#EQ=gDJP_qp63S6JiOEX{SsLu zQ%RhxT_ z%AZ0mO~ORPrtwpW>`rA61=B>OzVg=cU@9#UY{{gkiQS1mge z*-j)26GL_ps3@L=b{vC64>hI?HdjiYzy1C6=eMu$yen)e-S3Gulwi`W%T#YHsNmDG zbTZ&pmQJ@`q#=WfZ6<4A?PJT1lQT`tTIui8UMWP-!{Jz)XrRV4@=p-PR zZhHpV9$&N7wW7yN{ckMqp_D@lTa&c`%A?gQxm^-QqCj$s+ZS!w1P2uZUtypb0G@^JXx&3r}O z!vnzGZ7#ZQqo-~$^Xpi_2zv_-Jv{&!fVr#qF-v_Tf_)9eFIi#obzXd*u7?bXE+>Ht z(RdO_itMm6#;BYjtAIsN!9X-`Zxv)E^;(jAoh*MgTy>~jX-c!t=yhzuzPsU9tNNeT zmJehUvZ+XYOBj#W`V~G^-QcB95pQ`{SHR?H_&8X$XFyGWQn2}lF*>)K2QuaMQbtP}VFCGJ2;lPj+^+W?Jc1#LzZD-OYuQ4%^76-s z)k@T(LgKLA&L@M#IEBVjK6tw*1Rs0L@Fjg6(BIZ4>mfLDD>P#?^dn7F@bT_jnIcO4?zk%=2_~i4o_<=gYN`A zojKCpmHJZU{Bd)D+()GMP^=%HNK69CWc$q%IjJIhZ3%_!LId}DWl9<@+DMjR)x(aO zZ!r60_|xHV%4a)(FW&$3pNk<5se&uJc;;RL{hv4_v3%nvQEfnc?^VB*@c7PP6EIe_ zRuIIRwzl?wgGI+g5P(Wgq&CA)TsvufoH^P;nrM`7(!-`6`fNKy)ow0Mma1F-BS^ciJ zbE5zt(qG@K^Z9{U(rF!O!)){;sI?6lYw0Y^?6KJfEgb^^9?t=D38OdtZWf{Wyhn=(B5_cA*#~B1i=7X4*HLI`>BeDp@Y?@=ROP>x>4QR zbNEZ6|JTJkR9*JBUALaE4A~H~kY_Jox&}HnNinI@gCyHZxvk$>yvzqW3b2S(EpFDk znWHPF=ESb_M1CEo;?ox#(+1Ra@QG4Ka!LFU(M~s#9*8YV2(G!} z0~V!VjE-blUQ`%&7bN%0^cLp!EqT8q@5Y5;uBElOF9tH_tdQqg$@bylNAzk#Y5(!x z2|;#LHaj|^ZWOS8r3GR4#hXO3o?u#2J&)${y_Cvi_y%S39Dh5Ga$!T*?`r}95B*m> zKkrwq>z-Uvn0cIl#(nE6@FZAUP4kKwQbKn!5qs-e!JdesuP_rUCSvtrV6wz)M?V!E zGTb_vIrb@i(h(Z_5m*Q@j?aw|lO__%7D(?{2XFCoI8MvpjD#iYf{zABV(pSi8h#gw zt($2v_NEGgU6m3irk(dW$~Ds1O{c=x=e!CFXB6H&aV)apbzyYn6VYT5%Vjd|(r3-P zIqfia(Pa;O!gzQWA<_ds@kEKKqobo^g#tKD2UZ#|Wly;4aD6osT)$oaccS&uW-9U{ zN(p9t0{{5{ho9NSKcrQ~yaBZ+h0&{{2Pt6yfN4|!b{5SZ7o*@3`PrQ+i^lZMIgV<< za0welrq0HZf&YLKr<0}|5wi2^CQz4!P4w9u-+Ox&B{dM;fw^C1GRN#(s*2hA+v{}} z@lWy=OEtM92d}DyEUNwg@!tg^^GhJ|j>H9`AvxTVI_ltAN>fmmDD+RZhwp8aYae{` z#u(y1dRQ1tZ1a@>g;mqV*wajOaj(VSF3o_%*?eL*lw19 zzxY~$Rlm*JnG|40aWpCY>326q5KjiF;0=q_MN~l&FCy~=1pqi&02) zzeB@1kR11=dP1F|-+7VykK@H4avxH$PSA@=HGF~p6sP+JNQ5x>vb?Ne^n#IS7_r0= zV3fpVV=A;Ae2tW9XYicWC>DN=W?R{8e#36TCYEh^Zaf0{*yOu;dpk*7pgmdx$uTx5Buko!2k4z zTcOB>%BBM#;$D8olV6e=uj4*05iv~X!IJOWei z`Q$(%b^LcPO;`i~LJC1MUB%S>z-E$np$7USFm<8uzw&|3*aFe1PrhHx)F>RbYmWUp zkPfeG?LEGz5P$f9SwZ1gJq|-(lHj5+7t#$so+&-C%Ia!Yv^X^^(<~X@g?YD;TzYJokc%xot`}1(c958^e zeOoyBJMlcZd0peNujPBE_obANg3~mmx!u%GKwEC^aED24W&zNz>mVvQk);5iYZh8D()q?4QE*9br0gW zy6OtYEj}g3tN)oom)W#4BH8FmoRL70HA6iWE>|2}H>mKgS4odll1zAf|E>N3%RZmj27}p%J5Iq4$Ka`ghUc zLmxK$-&qHHbqe03lZ5fvhAE)UL>gF%xMnX-(R%KEZ49tbGsfh*y4Cb?(nc!#fTf9w z#y0e#2JSTuZx3J`$OUDM9}cNJHO1%6CkXvP>3Q?p`<)TmKl`%}M7ESP%FwBkrFoj9eY~;SMb8NmC%(>mLCa7x%B}kNJb*ZikF8}VD~`PdT*sZQ0}WI zPdhuK@Bm598*2d{`^u|+GM%$%eVv67^R@U(!Y&e8A_ic~*j^vjhGr(+VDS~3z;v%H zpwr&gxtp+<4&X1y=!i~=zCQ<^|H@EVo!U?d{XNsjppBxEEGQ+~Aj9brPvoOU78X_pkd6lt%#nb`_MaASGR(i%UoU>9g zvvrriVuTQA9=m9Q%)*3eJ}6fN&&l>|!C~(7o{5>szGWlUIs`9K&tLyZU2d-I1qc zCiZcCL717i^2wTHn*Pkj!`s<;n`>=>7^l#)DcbasIRN8i^$S0Ly0z;D%@^9@d$Kfz z9{51%mLQ-VSDcee$iH*RloL&w?6-3D_`tl8_1&l;2ca|A^>e?AT6w0Cx?%~7)9DD2 zdN3j&W0jjlw#pS|CrL$*t3R>1cb;i%#0LGMd9(B;eZqBcmsg@(RpjvVk=dIif;T<# zlOj~5jRbNihbSK2nZpWd5v|_{NrShIHxRY@+$XQkWiC94t)?*)SkNH1pXnWgZxB&y zp<^<2r-#iXMCB6VH6XNxO(@%>eL37l$pu80BC9NKd64cFj0FA=)$kdP&LRm@#>uOH zCQPXq7kXQg67Pu9h8O1Lh33O|lq0OfCbl<$8pH23F-Ixze6=SN{p*MfhB8B;&zs`# zbgi2T5n;Mgi}rg{ueM?T$KnR^kNgd?`8t4w2 z;ZR5t^zBDo;JG1iLfdd~pU+wt zwC-Qn@#4nLy3-)kPC58SCZ8WEN6YWC|o&yh-`pp0KKRR-qEF_#s>Az}A4OMP$0+45GvzA+?#Jw7BI zP7kDgg6uYhZr>c!3E^6y(U$*#?_?EVN;)bK*4x92K8d)#+1=VuIBz;mz7&j!DbGP- z<5-aGL{rw&S6!Rd2TI#u>V^56;hUn{CCe64yAbe&^(RTnf61ClJ;-GjF(}Ns&Cw#6 zczolv;M(HoBuUKYysh9wQD!)8n4R~RSa6dVhfYywSwY-ok zG87r?z{kd%x=|1<2Nux_$T9s+J5QAsq=Xa}S~z#8l^X>Y{#dI>rN+!d=cDiW>G_Sf z*gk3j03cMQoyMQSLa3xz8iQ|p7CN|`W9nw=q}3#>-vwh)RgVXNkFomEIo>a9C*!x9k z#4)ZDX&2hMVPoY{cs{_*iWi7(I1T5>VQ#m%u`cIVo8ZxMO{IrD_vH+5wfU9)_Pmzt z1na)!%VzO})re%GQezpApM_%BeB0^S2nqI?+gO^_LfQ6csJLNSlkr{w?HL!5{m3+^ zR8P}l@oyA`>Tz4}*Q;h#ksHeacAx;t!L5?}pb-jRLoD~OS;$UC=riO^8{?AcK+;xx zO2p!jDw8tTciWkC#NEOeQ%*K(md?gjj&5{ll(n`=zHsL)p+)nMpko}hOh&UJ-y9^- zF8IEYj9onE1?m zjQxS+h3V9mqS_%zkMhUm3ry)&raPNX>m|5oRd6dzN~UamqGRJn3A3dTCBWVdMhr)jCy*z|6vAR-McO{h0MWw4i$8_Jn&TVGmU3K8kwhy6;)MhDFzM}u`pF>v?6bfU;jV<5UG`7Jk=}VZR{^n zA^do-M{69ZsNS12Pp@m(-7W5GHnX+?<7o^B{8MzEDJ;Zb8a+8eSaf9a%AN`)F%2zS zfWql;mg&j7^mgL8@SlyzU_RU1>oRHPg1*;qQ+zrA7BbYF6@$S4ec5D^MtNS_>K7i; zDPqEmNon2bUhiA36RZijU@3uEfk+0`IY&`~ZPIO10Wet7ej+P7df0|=?ce$>1mV! zV2JBuT=t=5EW@K`T%>QDBV+z?NWR6vCWfm+wrZbt0GvS{I1`qsglk`MPN^^Z^^**GIhtE)i3u2&j;b5q8FkL7Y+BE) z{o}tCB9J6)S`AX);tz=lkP0EgrMq#`_Z46(i}qA@!SaJlAq!z3jNG1cqfL z+bh1ewZ$;l7?u`xb5is6nsOrso33eJ6hA3fELVIch(ca*PPna&#Mb=WI~zV+Xq0m) zk#Me{)y`D)V;@zq8PVZ~H2gZ2pOyYU<0)BJ)s6Y9y5p*btb|AS+^T!s6V^NU!CDbR z<%V<$4E_}*6Y+omxqe5KcXNf=jikTo+ornNLEm@gAu-yD%7*Zj_dFUzKSddA1P!=Y zkWoi(n!%M7%tFQ}hUr|L1d*tB@F#K#R z%deymzcN+-(qzI6F7I#QI{C-{1c-tyX)1x|per#v&S4i$67Trv!X?GpJBH8g294u* zURado3$#T-xMms!Jb*H%>C>+!Iy%}TMFyE~bEkS1&bcipe+98O-PFP1J@8Y|NEXzZ z^4*V`o}%rsw$HN2*d729KyLgvc|*I;;Nb`I{e%4@f6fjP_I&eSMb}=$IO81njuW#X z$($fuP6CfLBH%%qj@)TZX){gTET4$TvR^uwm3Fp^NX={-VhT-=7~*Dayu_9{Fqa{K$W}HBaLQszK+mMwdst)pZ3qj0NI3w~4hi98hqxSs zer}Yx4I-h9AwT)%4`0eA4?|!FqWj_)qs{yjaDPB={~qYCR!Ay)DGc97jV6EDLp zKFrqAxk<`c41v=Y**VMl%s1q%?nOH!nMufVjobgqLKMI!##6W}(Cel-Noik-@Q&?j zmwWrtB^jOP8tD)lZ(f{dYyR~c7TNi9F51EuW3E7J>i&=aK?w4=#Hs^GNLS1yhT4`f z9;f;KRV+lWcOuf$ErN-kAsMkeY(N-NFDEK&IN+-|ag?9WZzlae9N$<;q9=dW*Qd?4 z2kf%?pL44~TF_}NGD}nUgWzc7bRg4-#0UP9EWuV5)^qK*a4p8shKN#StZt5VZ>fccVCb1cD>WXAeSn_i< zC9Vtn0mjAGJ0iHK{-(pfYdbLDynKi;sF{f^C#Xg!^zmMa$^EmPih#hwi7(U8e!OR# z*5FT9FX2&FlLkWHj&1dj5KJ>Ti852SkFlb~ih*EHvF}Tb-}gfrb{+StRWW^1~+kHvw={``(VP zyYlZGxYX^Q%Zs1(0;8k}!whQmHE~*SQuBo8!vy~HThpAWe{CDd|10h-uTkPSysK)D z;IDWBC7hIAH2{6Q@QggMk_wN<%I*441De&-$T;V^C2REj(<*0OylqJL@cLB1^G2av zP$+lbc>Go%2o9e)qZ3OY!7mDn@z}$SACm*v_Ws*TiNtSfuz7P#rQJ>nDs`{W#m*e9krP_F*`yHs(3?lr53yy%+D^Ju58+l6=|JzYZx3F2n%RW#1V zq>&`d6S~&onHiWKfFf$l^5*%5d zxQpYdsp@)Y${@I23%ww^X7CLrh=9W_FiG-W7yU^eNumP)3X-TBzZWSBR;stxTQpet zqVcVk4>RK^3@{{MAzstJ-wW|u@dT2Ogh|awbWi{*zvD6od0BxtlF^3)r&>82=CU89 zvudN3Qgn-`U5bif+Bg;!WLe{&#krt8_@yzaq9KIecP7ig+f|;6n7G-{3i2V9p~w%K z%=4du$hyF3N60jO&lh22E;>oZKw5a@`NNl@1=j+^ZUFSp;S~9r2(+?JrPK$h7vq;O zd2a(Sxccb#gV$0v|F<|ed>;+QZX$xbDBPay*c>d!FBY1}fwPr-xQUJ*SCNXIYHvOq zZra+{<)$k9^+6AGJ+_<|@j7=G{`VE!9Zm&;bz5_C?QoIc^(mmxfRgw>{iPNra#;?5 z!{wl-x;}V4jwY`>;@a?0r#@oMulI7G$LzEZ@K*fezYIkVfc$d%?Xbt!XNkji3XXGK z_N&1Wz@G~HD)?U}Lt$r<#H?z`0{t%0KmoK;n$Jtwx)){FJd6XEsQkX|x0QAEe+&)p z0@XNQ!|!)OTg!s3Fl}`u_0tlA;QBKW8%InhpME-iRa51#URim_NfFE(^C7}k@tHMz z$5laD@2}-}w4UX9^Op5CMh|O9lLAr?uv<-mA1xm_MasHx=EXbu8uJ5Zgg%H|s6rIy ztC5*Tv#|GBmFPPcVSW^cmzUORIa!I!Xavx%m z`uKp;m&-9r&%Wm)baFdy?$nAdPHmShxZdV65+h-Gcfa7%rC3~P!Y@PDO)H7aK4Pt< zFeO6)JZi)Adp5|#hdA37QJS#nVe6;r%!QdZl)iTq8`K-7{0{xSG{ku?ydS=)3-^DVtS+9h4{g%(;gE;oE%LAIiv0HY(?|p|wH$ zHU58I_$9ys2Rbf5+db9icM#VbuCnQ&2sNQ z_$(5%VvMFt2*AC;yj z66-NDkXcZbs4_|_^AswjF4WhVbGf_{0Oavnhh{_nKI~!F*r(EPI;|p@sR5N%u5ck{dkp4^|Lv&k# zgA~!f$Dp8zlQK@(`b?)ckW=R4Co1U9gEHJd^Pn`MstPVRQC-LrFxRT|-mUl@a|Hgi z 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

#ibEFcrEB({}bNd(gkK@XxMu! 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�+=%VL+>BDCfv@GCb{Ycg;Kh}?6tqC>C|bZDNp9SZyNdFG z89}nopPAM)0EF`EFE209T8jHWgl3t{^;GqI*%K<-zM{iP^n*F5x_ZMlvo&kZfN1|l zDHh>U8;CEs7E$cush=4c8=u3vo|}DMpDEmb>f;ylnN_AWq^Omxn5t#mEsLGwv<1>F zVUz1x7_h~cV1y=E%_l7(6aI!BD-D$LB|IxTuogx2`88tUg*K@UweUVJ7g6LPIx8ona3vbhb4ghrc_U+pn4l+s8U z>0a|B)bCZ!!){_4Ek^ii*Tu@9`x~W=jNOo}bJYcpnP&rJMmF8VTQ4YRQCDc-O{(!! z*)OTlko9W&zrX_`&MV5Hil~?^_iP5SCK%V&ld_pVAlV|n>a9t;x7-@uH_8z=C#M|5 ztl_;&0}D&O{SmXI&=1H+6^7^CYH1Csm_;mkpR`JuI0a(p&#&=AEe#rsP*`jxDMxL- z)U62VR|Z;t?;tIjI=e3+c!IyeUV$Q=+g;E9N7Gqwwbce&xTOXZ*Wm8%uEpH~gd!!l z7k6kW?(VL|Ay{#W7Yj~thf*Y1ak=NacilgbWM#eQyfd@+JR9+(JXo;nq3c`@HP9;_?Z~&f`KY^!LNQTWJ}vvBhJL!bSrrt90G0oX`wqjmGxdz$>AfMHi?s`B3#ZrpFwbl>=pX1L`F#@qNShE`k-zW7Qg>ew27 zN-^LN=ZXP_F*HWS3XvOx3AIXLN~m~ICZI5)Lju^?;|j^y0MT*fv$2>1maI~)1<{4* z`PCKvzdyf^ue~@_GQ5df+uJ`>@}%hr%%|g4&sCC0X{72KJCPy zYaZZM41@H<%3;t_FXBtOS)zLzYv|$2W#K2^{(~>Iw{F{!W8Er?5jH22k^_5&-pnf4G3guBs5#0Zc zz_|QYNWl=^o5vU#Sy2%#wMnSHt{$Ca)$p=Jj6cdmgfyb z(BzHz{XAG8pZ<~9jGJ2z3-?DAwI?0$0|%iFu$4l0gJ*vxzslm=Dl7oUp=ywBT&cws zJjZTF%Pv+^ZUVd!>(t?)*F2-F2;+oNI7iX`=H^%6EuW~tg3DTe!#F8YGVH3^&lZs2 z4-Qsspej*VXaW+)+(Vfr8uwGX5Xezh?Tb~qO(6pu-)a2_q0$BA6=F*GiLWiVlS%(6 zNUoDPPdIzKNARFi_}^f|bN#>Y2|0Ln^LIA)r(1`KGzO?3$GdN4p}zrX83^X!NIV|w zKU6<&-5VMjCxwq0LvxIt(*Mv7vEy5)b1+*pEy?^#LT^44xhtR_kW8(vZ?u!@W`4hP zH72SG+?~Yo)C47v>R4QVfvIb#{75!cX-M0|A_cL1Y5M+Ur8nI?;h7)W!(yJ_gdmyM z*-rv9!qZ-JZKb^)`AIV+{25*@^jF>|^E=VQ2eZ!NIg?AKZWE548}B6QRuB_@Pm39@ zEOtwdEmkOray+W9Zi4xTJIX(u;Zk`4EA1RE8Bzk5lFdl7dG6*38G4+2HekA8`xz~@h}L$IQRoHPQj#@Y>BEpUQ%GS2 z^GHRn!H0FvmDN>e-}_Vxh#+|!T3^rH96p3T>{Z%+FqUAh8tum4zmZWa>9%f#U4X(MehrBXhUw{nK*hQ?&>Fse_!h-pwM9YSM4YJw*&H zE@(C_k9{Ut*rRLooXTi|#njF~-3Vgsmgre{dsRx8+<22AdgqppzEX3f=5L&WCY~R{ zcfAje)OoR{y^vyv5Zxa?6nW@~NBvwQ&GfBbhN-dHn(oY!OUX*EGPsFo3Dh`Uxm8qV z!~T6mx2;jw%ovF($er%~5q)7{Vds`&26L3y^@gq6!{owL8@a>Zb(DcZr0r_Ryq!!* zDAKQeO?#X-MxTWsehfHE`qf--JBMFFHX0+xOHErEeyH#Vb>FvlkB+A0odrHdJ!dtO zb!%UsbojdB+ln!gbmR@th{#H(NBlCg@4qHXT2RrBoyP|o;h0T0whYw13m<|O8ytL$ z%i6`Fab~k+v4HH=(QEYMi?T+Bq~oEsSK#+k>*^S6gs})HY{Vv3xQdE@wq1|Mh#ie0 z`~j$Txr^^iWm%oUt=`GH$t<%AmYMYzmqRq)zb?0bT&gk4d=PNK7cJ4kf&^KiXyunz zCOR7;8%IX+!-wtFEM1ei%@?Du-&3iunnK;SGaIpxx$>LvJhV^`FF<)UG5rGS(1=Oc zRpj%6HUVFjYM*68!f$X9r0e$JU_Tbddb80-cyzVwE|vnME6g%eu?GLK5vgAcZp1oB zPsh~_9leoml5S)1eWE}l`iUxsZ3Vml9Vejc-wFI+3oO)duZMgSVPwYe^eXF z^^SC@*0medJDF^`TkWncXnKkN+n!kbX5)>30H7iG64ZXi_!^Q*G)P;8zv0;|2n`3$ z{gxQmND~jk0dHPEd$gR{w6gfS>?%aaqg@Q3p55Qq1^w+$5{KJKvEyPW8!foR^JQXD zFCn+sUFL$2mh=rB6<9C*+mVEKd75zOrg_!fj5tm^$AL_pW5`B=1w>E)2A;(Jt1iIF zD%;YLTBNfEwM-7DH-3Ie|E^XwkQE5p2u=_NT6H%6S3vf8^bv#Jf@t=di)Y~I>LZ&A3KE#c)m0PE&P*%v z?>Pfk3~t8qq&;othqGh(m8SL8|SnR+jq!ZwHwCxR|8CzAkJ(i5k$PWd0$UOPMy=YO zK|@ZFXx)+Z;_}?B2MqYq3+lB0KrL^W<*INta+)KsWD7V|alLa#gWCc&%)Y)%R*(i` zDgMPd9ou0CM86SOKC3c8_vAk@c3f}!1|}{!+*Fi9BtpV>kz1KoL+{B?)LCQ$L(_*@ zlPv~3K58xuFh`~IQEGP7GW80M+k3o`cz^?wLC>MQ=UX;erm)*!H>iv;!<%?lUuZ)L z>2WDgAXKz?;3Qt|?&en1a2mk6cjE=Q&}hnL6lmssd6H?`rnpf#%na$J zAzfJ-$aEqll->i(oEW)y{afekwL;>*oIC&HGybix%=S^+PtAKgvI00#(hofW1Su2h zfppP)g_KllKb%Qh#YuD5qQYig4Q+qBsbBa_W5=fS z>*cV1gU1=)J>ckl>fyXp4+{b<+$?~m44xcQE$sP`tcm5B$bYb=frbkof_@vc?Zl-Z zSn75>e!hGE4i|u@ z4r2e?5p#Af6szR-xo5yW0${>!x9$Q6925Whfg~q8M$;r@0aNf^MxaOcwyh$YGkN|)5f`c%H*`$# zSJL88m1Kx3orNi*5`1-)rET*yt9A8nN0L$|Nu)F({wy}nA+X{$>Q&e)aJ?xh!Goyc zYxgF)*B84ck0RIe)w&ULSK4!QVNqh<;%3KYcy$`rm|^H#VrIipie%jN@wCPOhJLS3&t@my zQQ0rtG+IizBv2WTQ6T;3Im6%weVhz@mY~rFa5>GcV-b&IOlEaQ?#4*tx%6^TY6oC;z@wQ-EdJSEGr0KFY_ zre#(X3g-&2L#qMhi}}W4(jYq16o%J&TG94{&yp>O0x}QMdfGjeBhkSSXJ>CI72L9m z2#Kd{8iYi5;f++^7Gd*8O@ZU)k4&g3bb5MeM}T*BgXqpU@ptl3U?r@dgUCXY3xh|_ zSCf{+ONZf+d5C7Ey_|VO%hgdtEBPK!Rb~XM|5o9#cH#@RyK!IS+@ zXu5hx+~>f^$5n#s<9as>)n%dH1z?o~rOJehuD>WG#zcuNqj{XeDvF zMOJJ-Hs4Z_G^ngt`p%$KHI7HcoO&ATl)vDoTgf(xlMx!xW%sM9a2TBn`EtMjoheZl z(>YR#R$JHgY%B4Y-Rugd7AMH`ECDXz3&?U)invzFXgo|&vnfCAQU3~;*)Z?f1SmC5 zGWS{2Q7TX`J49#^wKn*jPK7pizhTpmwP2m$brvKAy-VNAFa=RUNW6#>$ha#%0m{+1}MnlLKmlp8D{#Xh&DVrBmq9q=Cw?&G>^pFQ0p zB*-FVh>4g+m3CU05+I|Zkz;C1-!zG3eYCy^?t_NT2rm)6GlRbWz;Pcwk>=Ob46lqG zFRKHiUq)N@tA&-W*1(bafCumD0vlU^mus1hmiKgopXWahT-e)!07~@dKY*<`9vT;T z&h`AX{v;AgRj3l-&c`_ZdMLrvcFe`>{D%_tHKSNF$%-{j z+8fVmLF&?4-AH1d0p)BmCcZ~`+ta@S^ByHTXoQfzEjk4DD^Hq z_R&BG<9j_1xc>DSI^c~jWR#v>ce}WF|L`3Q+w{JA5aZ7hgFCI24U^6PZ0+1VImz|T z^#(qVqU+ggq>vWwS}Nu2>np$D8BA9wc=m7`23Sn4E^rMm-mO221NZ+qYzIhle+~|O zfojzID@N*AUk!zQ zl-Z&Hlr%nvh4XI|4+N-Qg5(G7D0TpCH-vVrxY479u2kbl5Z4_WB+mpQ*$)5Nuu2N- zDuTpTIiZbolvtwe6^gX;)k6j!>PPDLy$3-N-JRVMQ_WTwgoLF0S$szHoY7XXn3$rZ zCKsQ5R26a*%sv>tfk)QDO}+L@o}mdpt90LlU|k4gy^7Ln+)waU+NmjItDYhJgcjS?!T+x=8zADOq4H_co_EKX|MDd~h$01eoaXELF7%$ca6t0Q=%`5a` z#WXnp%I$ZM^-$=rT&7%*`^1O?ZZg2vlA&vw2yqspk0=vjO~B*h$PHA`7FbzZJp;nN zG|JMb1;62-p5mbSVa=+Ipx575#h6tCn5|CC8zPbz=g-Z~G8 z8dU~teWlJjR(Vg+7H5|=B^7_??ktg`WO`Z9_0nRqvXnFF%S6;ogx3QO+gJ0xkximD z|F==7Tt&h-s$8+OnO$scXkz5zxIA}9fMT5cvw}T+C<6xIAP_>i20?n(Ib3itKzerf zm(8`CIF3kLWwXd}1!i0W1G&egrH8_Rq{;f;;G@tD$6GVDdF%SRGFr4yJUt7fA7dMa zv0RCU{AfG@1W~Fds2YV2RB!85e}IjW`tSHuSuW3VMo%Ql&$6idIGnA=Gsz1UI_!kd zXzA*u6zLk3Xvw4)&PFZba;-wV&m$vVPu0#LsNs3cmdfdQ`I6q6N&5t{`V=SFx}XI! zd8|m-)lGWP1dub1Mz8GRT&JK+T9AsU zmAyN7y&qfG5fyqCHN-Bx=b53fn6r%a!7ovOL-|v-d}E@1Rs~E26df}QR&7{}Pb{^2 z<)5bORCgyWaJwKuQ~Wx^ofLzR_8-*;&(smGF6kMC=xqf^s%YRIq{cG)5iG zJY)C#JW)O19QUqfyK^7Dc+^4~$FOvv2px*dw8_jZyqy-XEgxm|@%sZ2J!;mAo;g-} zuCPKL(EMs&P-~Wx|Zxie^e^z?p@ou7V&!@K~uV^)k=_9smu zVOPecl3@#1VYu=4GbYpM(lvW;MCZPtiHWB98zm->cCz-qT*ULnJM}56#U-}vvW%5q zRD;&0V12tE?cIIp(H02`6{escU0v8c<*qm0TW-Qg8VmRf0(x8J#-i@4`Au7&>#e$% zdEmHC>uaWN@U1j_x4W1;yspa*`cqd**6BGW7i3Lg#c~C8qazsW*;?R161iD@)tr8% zb8CO(AniNxWJqW66%+BCCBAAWl`Dy*L{+!)rz?g8Ag00bk8JKqNnfhaXSMVGw9{kx zWB=7~#g1L?TtKJ&uLHz1Uwo5wfW)8qb!2Y`S~fBBWW`8dHMBmQpVxtE1>Yr+BT6u=LObZaw6>P?7u140i_RhRFE}T*3>~t=`Dh0UR0@l z!7(6F@NelNY?*XY>C0r?Ma?+32DSqp6MZTzKdK#D8etlqES2oFss0Qf?Z;f+BAi33 zM^zRBlc%l>J{__TQ)JboWtHUqn8(=Yen4tH5~;N%WT zXU8u9=zmhj_VlxGkwEB?_Nrd#k{tSWOi@8Lm(g8j`e56M3isSJWN`JsQ(AkFl>h?J zUY4xeu7LiiaW%pxYlc_LZJb;A=+%&!{6aG*r(uy4G?Xzm!5Y=iad(hfH&$GiAM+(M zs@K#tifztk*zk`?Jt1-Ra3ygO@5N5YezMTn$pHC)bH~8Bo$@Mrga2Kc2R_CRXDriUwc$3h+HXi~HC@|`< z*XF^1g#qcjFy{1C8GWRAL10dXdCle^;v%|salUK!bP0db(Ykk0)-=~~r$Q@Nsqu_V zC_vDmP&3hYV4ZK0BJJQTJ6@;m1&rg3<@6AhD(M5Mo;i9Lj zlXvU2yo*S7YLHPOCPNoiq7K=~5J;Adk;9m>jhNnLe_z2|#`?YyH5;Db4B|YbqAHmy zC*w24UiUT>Ws9eMu`@{8{Qu%Lh1H^z;_+LW+)s8xxf_NXr=d7GSSa>As+* zt8|0{UQmi#O?hx3rqn_lBWVjUVk14a{1O*m7tfE4Zf4|$H?E*Wk1qk6KvyJh?*n+73 zd_Bkyg`2?YhQ2n(lJBR=GIryp-9wPGm+z|-g}?Q2R&fd2&4j9iHt0e8dxt;v$^mb#a9bG`7XG4=;WtIs(UKcefG_jAYmErZ9iO&?y4XBPE^>Sb%HfZZD`} zB%F&owum%W@4nAc`?I_IPmlHiPa41jx6B5~ZCJEtyht2FRpATnKNN6kZTF=d<^Efd zZ_^*->?~NNn9Uyl32t!kaT|J`QP z_!92ljPt)hJT>DPPzE!u4ry_h$|jP{_o~U|;Z$Q}A)%31*2|t-0s6DVEoU{b> zGm? z4RW*98fIp%)vVkn1SeQW!?`xB;<&Iac%X?$LBEPWr4tG%3f0FB{E+^!GijlgWH47nkoxz}-F?Df_&vI2=ry>q}NlYt`<? zUgsLU-6N=!e0=0Pu_{(mrxK7HWNT^j?KIZjcmt*)h`vPG;RNT5P4<>Dp0)d%$h(1K z+EhC7Bdq3cnPP@Q4iF;Uu!V3+J7gLdVMHs12dfxUx>j z=QWIgz4AG)Q@{qsDg?hI&ztZX0=axgdKs_bI;~~dX@&Usu{ppv%cty#jQhB)g#RiC z52fx4u}fsQNcQ48Ht}wOhR6_V+ZV|7o0g_TL>bu52%&%#iNJ2p@4&KA6vCZ><3jmH zV+Q2qcLdlR5{5p=hijSD>Wa`JO-0dC)`C$KNB`ba-4CX-qR$JqObqUXn_sO=xr=Rc zPI{7IBJc)nukJtU9yax7!g0g4zt%_;q*!#XpKa|9@m2Z`LtpnJCmA^Hq)`;Z=NeUXnkiY$wITP zr@z$P3$giYkQF1EGN`Y}iy|(qvnXKYg~;&jqnE<_*40yvbEMZ=GUf8!(0tM`~?m)60Zm=KB>_*y>iMHXE76 z{Df%OQp?F3K}L{9sG+Meo2Z{cnuI;A&<*I4r=fVoiRoOs!@c8On@<5bgjXTdRw|eu z)WIT|&85RSqRv$Ws7rJWcd+45P~FI<>_Qthfff!3L0sH>3jfQ*z5`aZMd7XO{*KE= zTUAY^2^WYkyHMjaQIinj;|xCwG@7)7mgU9*dtuGGSX-BXHDD8X{`Bypa$Mcw1ku0k z-MCpAztwdg_h^?s$S4EGYPUU=dh)=KQqVtw_?|-)CqCKym>uW9EayW)!(duSuhCXF zTKC0kJZ84^S74zZ)HKO|+~nma5wOa4 zn_0Vymt1p*EDku=*PNUu>epAb@JUV_)$vZ86b|^Xg-$a`opix;228-Ai;ZgC;Jobc zd5pvoZSe$}`C|xys?kSXJLP)D%%xB03pwNZQO*Q<;mR%3)1Aj*QqiCoHj_E27~x(VAZdwAII{zN)h`yylAQhr2I%E#me&ZA-m&EPHg;++U; zz!Lo~6Zx|Gm)+|`Ow}KI4h*6|9ye_g&IRkcoqv`|iwdC?wl*ZDKeO9;E~c@!`?qI4 zu5~;{2YBVPk~y>&IvPa6#)M7O8(bP$zD;j_%eZV`jEI`n0MkX#W*jvKe=lhdsdKDo z>owp?nHkae5n0GcG>EMcjCIhSTVW4l@hCim-)LBxEhdgzj&|Qyc0+S5W5Z|yuRWhR z2np`5uhwtKKQK*wM-4ASdD+NrUBM=&e6+arUTuG&KYwZj+UVsYm{vrrz2PMc$Wv4@ zOCGYCHOR`8CAbZFFBM+{u261$ZKy+3!eQkMzvBYHW#w%G+6Dx-=FfG;6YlZJ#jlL&uZ%jI@S8x@62KIoN7jvpCS_K`B7U0 zP2(?LnPx9K3P26*c{^QIk~mwWk@fO|l@sz55RXG$JYQHhw#Sdr;U{^nzP_(yI^y~{ zInlQUH1*g#0g7T}(9n=THjq53tPb5-LH9cgfr5NSU`AaQ=GIh~DX`|sy z)Gx1`O$|{r7&};JrYPV{2~v1H9TCZqgcg`VETu24*;vOc2zaVI=JNS2`-r+((H^7c z-Y|o2T4})+rDgB5lHEWN?PG~K9gej9uL@X|tEQ}J#GwISh1iSn^!-N24e5C@y4J-~ z3IJK|L^Qu?9D6C@*aIHWJtQ>L?un===Ptn~Qt%YNSlrLMxVec(-{M;vU6M}h&v6xy z6yQ|j!6{Aua~JCY`hAS6x~!1H3joR0CD3@?l_Br{WF{+YUbXOD!&^+a+aJ!&&NlB( zA1Od3txB(Rv~}Ej@u__?OC!9eb()TvEH;q? zLZRZ`f1As@LCvIudp4Akb@C8x8>aa1Z zQhDu>Dnj7U-h@N)gSd6&(N7J&t$q?MECWM+8D*uOHw7ugOytZ_^)Q>+rXt7UZ`eUl z$R|w{J6>iw=&1~n?17Czh!UOZv4*W=d8`fb=PO|NC9+pykxL(BQe%u>qMZzuCi#Ut z&n!jEXg}cCP{_D00wR2)|9(x~OM)xbf8vE_J~I?ekGP2S6xAUH&m)E^Q~`p&vJ%km z)380>E&!Bou9MAl6P=#7WDIAHKR`PQRsq;np8Hrk(;KMrGk|&Bh(R)p4+yFIz_E_& z%A4dEGGX)she8I5Jrk@hFa6!7g4hlqojKiX%}L-u;xY0juJ5nSUe*mN>S$|8UsNj@ zZgOAHItN}yFSnBzQjz=?jLAsQz){U-X88+~VHy|@82S%3gE~>h6XLEX32j*}NO#fO zJWQkwSp9c%S8IaWPd`H=MX;977R(lD5cxohZ9PE*G<*UAEu?CBuFprI7AU-`eaGWu z?%J+_fkT4sM;;{931!XCf>&O$vArBO>EwO1Fs+ZPvAI$K`CpJGZXNV0j(5u;=2FZ1V=>LJH3 z23Kn9|0L^UN{0NMo~pP3EH$|Xf!jxV>x{NQM-lZ?C^7J z_nl|pz^2!Z#M998FhkyhR=`B}3(dcg5iD$O&*K<&WFdACJ`!m9!30fM8SdlhFwDyl zk!u#+PZZZDvpj6t#%f1QpxbJzS6S2)3iGcdUbFR(UqmR#Wu0k zZr_!*ZS_s^#k0<*Q}>Xo`-%-DAN`q6;5X&ea;j(v7H0rxVWt@X2?-*r8evdu!Kv1y z5H$Q8^-+lj6ZrPX;+*jJMiP_F)uJttW7K0Pq?!dOzv{c`zwMcL3z$eXcagi@{9TYS z1}Dd^Is%7r-x4@PF=!vsY!*dE080{B6A1hi5J34z8|j(3P%CahEO&Q8t1|Ms(qsJb zk;jxWR8os1^IvBbmvPNHFD6=F1jwp9Av5*yDz}(`oMQd->ex;r*Q3d_rF-|dkFZK$ z_n#W`zo!>B?anjLr3oTeZYvpwFXE*LQf?tn=q9Blv z71GL}z%b^)R#=x|^fp(Z(q@fQ15__<2g?&_Xot;=WpRuuJch&U3~f8TSvg8j(m zzN)likM&kN^7MQl?swHf_5@j8z?F~_d4S@ym0@{*@rrLWf5*e0HxhR@X3d9IubmW~ zi$6ycI_rN#rA$E+AL;y@LK7-PlF1w?t<}E=iB^l6)@_bs8 z@9r*QX3>D&a0@SvV%)4WfM_ z?s|Ry!_KLjc`iY~!;DpLp~52RjE$q?iC~^uF{r$f8`1f8evg-A{V#Qr`ti%_zZbxfQU8|*qfvT`0 zIg;=NC0dI*sb&s`0>+YWHPzI#&I+-#&=F@Nsnk#yBr5wS(d~@2hNfZHHnmQHR-+x) z>kbppo!a*?_I!Mxewx5Vjbkb1VH6t9?SX670>6!qL%x>ZZk}4hD79 zM=xon^OY`&$8&$uL`k5!i+wfl;7R;p)<{ z$7_xdGL#@7Yn&(apHJWFYW1IwkG-O!g>=}qi_Ut#q*O}+%Y$}UjtSnVYHOLJu8P9@ zcghpfd2MoBYro}JfWbx$^(oX08sTd$nY2o02M0_HYOIZw_%-^)!>Xt$GVJHOmUnMH z-HZ!RoZ*}St0N5+kUiPo9A`3Vy#H`4?rr~`{zd|ajMcXW!2|n%ly0e=5pBV+tgn(*SHh&i2%1JqM0Xn zjpJ{aPN06Fw&JaiUXndg#h6(D+@45?bRu?6jzL(Wye~Z0rQk0piX{pdft=(HY6k- z5zi6o0k=%T1DIEd>&F^G6CW&c0yXh;z~h|ZCUBU{-lADfohUTETf|q<8*5%Q#a}1X z`4a?Z5U+J68*|*7fW)kSW0oLV3HQTDF4wuUBFC@V*ilie4dbVA;h2k9SXKF>ijks$ zvQhPvQjhiiNG0J{zgjAM;^_qM^Jwj%E&M^Yg>M~qrpPE`i-VWaS3lDchvu>(Lh}YkIE&M(;7>^tbx_B~1#>Eay z2}e~P5450q+{DmWw*nO{IxgUq01%0nkqQt;dinzQ)w=G*iw22XTwc!D?YZKGmQ7{> z*t{0P;7Y(Z687m=La#fx{hwP#&M^oRy9fwNJ-_7rEKKfP zQ(X18BamguGDck>`u)vp(YUMuvr5;T?SO6P-hnCQpo~!j8W+93-={Op2bAext5PFH z9Boa!Y`8igt^uQeMrC=l%)O-$8o4!PY`4f)saR;l(7u|4fapbg^X-UZ$j&71oa#t}HgB?20YfNyALpdISlH3OCI?wvO+jVW z5GpZXl`+A;lJWIy2B8p1gwt&Dwb^1^W2#+v;=a>aqOE20hoVUX;v7OnO|%UanSxr) zP1(iz%kcBNK+Z2OpI&f3+!aU?J-)Os$+K`R8G}plY&{gf zaXZz=_$i#F3Uq?S1$kEnqiOzeVH?@j<=8Lo1<;Abosqw~@?S?v`H@#t6xR&smovUU z=n7R(%A}K-3W>$d8YkFC4Yk>G`}=ufiWg^HfaCqax3ZcF+Y>}yeRnHh5o|p?xEJqM zkC~2i{zK&yd1dR?;P--iR^JR1cE+rnQJNu8yRN~9J_rI*eZ2%-1j%7?#^%KCMw~H( zG^7!`IutP70KKFW7J21r(@vW@H7>Q@SzrMC!UPAyxI18cW)raBtaHM7+_g;*IVPzr zA)j;AJN)kn?AQR;F#tcUU0{uZhaF-@J}B?zhZ(IZJrrF(`jV=8D2JrCW}n@iB%Llz#sZ_&heZmahsI4 zsWRZxKvy!{?umK%m{2&GU+k5NY~k%e*j;DP!s|Yo)q)w!Rd29*dbB833HEiDRj25P zbK(m*mwME?Q+3Th^r`AJYgmLCR8Re}w*;M=^6WNZn!*%BliGsHn&ub0e%FX3C62re zh;;^e`}vHXUB+fDlMT&?Osu||V9KS0W2INMw(WzX-J#_{$rd5;q;~PnPOUziqs-R6k_)3J03p%mUIqE!st~xW#%Qlg!#aYBSsx)oZ@u$BD1C|Ydu--H| zmfOlIq#>_f9)D&_sgVqxhkmhj|H`j+H3N{7z)X8Es?p~i?-9zrH&|nNEU<@h4xJ`Y z2gD9IlNPyLx=~Ic&No=+d<}Z>LIcS!>oc)-;&-b^rE8b65kkp06iMy(AAvL`3y85D zkps?DTyQ$4-0$$8cBNb{!YCqtpB8zlm<03W)b@Eo@F_}@sbw{JVqfhy^v8$(*m#V- z#^qwW5yXLpVZOqQb2?D#?U#9zE9M*FN_WA1c6!P$4-?MjvEJ?uWxM1+=NkqHTaA4an%e|efH?d7Mya>f|X{@8OB-r?THM=Wz*j&WO z-jgB4#84kSsPTiigtcs2(e0AnFv~l!N6WYqJ=?Wv9-WK0l*{r3KvAC=_0vKnKnRh> zWl!YPy^||9%F$*uE{f@m!&O(08=FyR@X5>Nk9!r;Fl(W>Ttl1dHYc6D#7di#RE0)G zKcYpfynm8i1bSCdks1dpt{}BINZ-dcBU1$c363_)nM3a)#-6O|=MX+%@!w00*e|*m z2X=7#X-qRMp$68Os1$z2E&HtZIQ&_d%Bf=*)Q@wSrdVGpD+U6~KF2N!Mb8(r=0PkY zs-2cHD=GwY)UwqR)ry20az}sd~5E*#AZ77>sALvm(N+cRmZoOUsD-LZt}JotrIe z+AAXDaM4*D7Uif~O0_W7pq;w#o~jzaQ{;+=#>aX|tGLH|(Od97PA2(C(e3}|W3d!_ zicJ=kwfTKwz=_NBMGNuaxLdRP4!!am6K2f9$o!+#!8wFev5~(qV}Of8`eWYy8wAM{lDCz|Ii3>erWJP~WyPqaH~MMca4Iwe8r>2Kkp z&#LPN$$zkvI!OPgRl_ zZeEST!rQOi`<`oz9!KwJuSYmO#b`VY>?pPQ4H82mRc6Kx{ODzCa&e3nT#YB}n?E*A zBfVfu+0N5p$B>H%Ms*24|2ay(rZ{U5o#}lA+WuZaQ1l+kEh?rswxq#fZDfLev~fzd zj%cAE?5VG*sN{DeEm>|cu03;>U(XXH%}pQTskD@%dYgxx$8@g6r+{WR@# zHqz$3j@Fj)(+?g`jnRrK{7IxN9i=X-`6-%4Hl&G}Vh=!uIEzv!Di(lygr&Btn-Xi1 zZMvWymKTd+#$RP&rY<8y32Fz2ot1~g;Tc~C=A6Yshb?JfsqdGiX$cYq>sB3|2SQfaI22Ame!ZwZEy@*56Y`NU z$4ZOFER!-zYyq59i|9FUBgpY07e++P3r2gey5=jtin>PXV9v7n12r}V^J&WFsv!v% zAz6T>69q4TS)#Kg+_8Po8enxHX^gq@uek;@<}z*bo}CJEPrSS9vLcn!ioZSFkC-_N zhgx!%+44J(fCPq`LXOR0k<=ZfGdG* z1yh(MIkZ>kWmGb@2{*?%0iH_V7p=&eL*RwkzX1^a__jsQ_QpxL^cNyM=$M2B1O%!U z?Dl>&j9q}WN}K3qyu7?NR&s>$L{cqUOJ&`2r^ql|t(romN^YkBCoHpr}aP0Ak&7zbOo;TJ{jL85@T*Fyd=6zrfb& z;oxD6_`&)SRqjXB{n3nrz3$J>~ceG4%w7;}r9 zlDSF2)2J4b8JEUCY-Dsb@zl1Q?U87Fz@}`-s0A8Zk*E=1gZr0)r z$X;e4XYn71AFXVhW`=Hs!%3t}kXE9+=s0f^=j*B_FGRy=hl0eOw<*MYv!*f?OyhGC zpJT}J2CSJ}%*d-%LOC!js(Se-iAJt!{zv$(w4jFhf!fGXi+Uh@?(Ih#$;vc=v9Rv52mXHz!>*DXxE;%1QW$xLPDPxRrrzKAd9_rW512wSjF=`AnmmVjw zmmmX!-KY$KXL9>;M#^qVm@uSc!X^{agl~YteDs(o^5gcIg?n3Ca{<35p46eWmH=8@ zZD-u&C3Q+;v_AEZY(mLp*u!^pSpf6lc>Rg(qOpU6K6-L~&j$L(ETG#Zu*9x#!21a& zD?*Ay-bj~sJaZ__)9-GPBYiJ+yt$y9>@cR!I!gKrZ}t1qpEJ!Y^OfjIbQobR*%Q5} z-wz|Hvye(@dGChGHV?+Err4YRD8PAWd_;zAYMq}-Ud z5oE*um*_o<7o`n9YBjBt&@f@zZmO<8C!{=y9iVt$1fm!@3lu;nd?(KcukRtMyk^+2j@x``Q#H%; znjxH2!zO6@S#rW_%8?5v5*eJuDSmtmp~J2!53;_nsl8!>(cwpNWB&d6QKiV()ps&B zCDk##OSpj-2BWhk^!jQuBs6iQJ$-zLvRT7!O!WoR$Cx>{Plq@jnYhGmU)gc+0Q%vyYve56aPW6Z`8Jz8c}YCpG{d=7O_ES3q4IznMr6X{OqOISQLGi+|`w=5?K_V_a_qk z>wbAMgFl-xBp%0qY$A7aPw21l;$QpVf94lT8FfTMY*C}nKP7oHtoMir@H(aCc#uq( zC0Xx)1U8I_Gi27*NshiiJjGmX+`u?jjxiz%Av983j{BO)_th?YHJ$z{o(L;i1Qbtk zxt03*V5*^P0ewc>2hZV!&D&XR38Jqh2$S?Yuoh@|>Fxdn91GJY*7O&m!%A9H>r0b? zQdmK;)gnBBU}=2fs#PRZI0tqs%|BFnu}!gcdhN362qsjID_2Cdq2}g}VsO>;*;~jO zWmb`$I0)BTHc%9c7Z#aS5S%63!>hBCj36H3pHe2QxssS~wZcubI%!yZD}L0Eo)86K zdAs|>ybxj;CyhyWemm)9IRjVwlJA!Uaan~4F*a7`Ylz1T4(4rbN+zL4GBh+R+bmP3 ze~s5#Jy6emWi{uVaFe9pi|F^AxR{+_?Q7j*E)*DNT8tLr za9I6Pw&s7qpq~A?3TXLiwBI?QLCZD1>)UO3ceYT57Z71`M3aWBnGT*ChAn8!_p?ho z&1oFn1P%J(i}-kdTYJD0O4!XD0!B5aDA}CziUneZE&?{#_vU8ddyb5_a^c)+m#xzY zf!F$6Y3KZ7L_tKX&g``kl?mqIb%;8DOPW+BRTxRoi1ZS+vi#Hmi{41JRFY7Y!B2pd z`Y|obAHsQ}OEoGau>W>{M23IF6}Snq4g(G)A5mwKOzr}s_V%1&%IBrqW-LS5a;BDneXVf1sSBS@8w_;#U4JC9*|?IaVz<7|ugtna|FuKb6p|e5y3?yq8ynP=5|8 zLX*04)5`TD&KG`ew~0nCL=@upmZ^AFjf9vr;A-!-3B+ibbBx|}8kEa%Mw4vT zn$G)1#X?d~+>8e{J%6BL!YGfg)L-^ezR7AKxWKyHPhAWNR6A9^fxf4e|7e`hdh~Tv z2SMmI+rsgA2L5n_zG2aH6qb|rOma;JroPGGE5;p#QoaujGH;zX8ohJ;K^aeyW|C7f(|MK$_n(o$BmIstCF+lGhRIwtup(_Mq%C& zLV}<_q1cXOLGRnHeZ)!Uhg*ibI!f4Yt^3;r(eaTYkh3Sf`L^ z-c6s7k!o`&cKTn+7D$f!_FK+AYGd{47;h#px=tBICjsO_LacOR)}6SIXf~*N$=7Pp zDr#TN;yYap9l=pu=aSlH`!j5cMhvAFFru{oz{u7kq?y$n~16kSvzc7oUId zB=v-Mo^ofrDo3zcB;ccmQu!nQUZlM$U2#Lc+TezYq>}}QU3f`qmLoN6Qa>rEeR=KL zn&U!T(RNK_%AvXUtF?8_qMad_e8d}LhOujcBYK1uNffE>KAMp%DM|IzbAk`;>!7&1PWsZ7lX1lWDHA~Azk>8|}>+ZU!=CnlY3p~xznYQIqK=fIj{v>Z0ea?r^oXkxrY(82K zmJ|S$J=WWfU_NXO^f&yoEF`ok)hA7Lz^M%-cooIZ;Gd!iL|s53Q@ZL>H^Ki=G#WTZ z8Vd3gtQE#s<;MtfkiU7xt&$VbNn)~Kh544>JIF&Q0p~|27tarD%-oL%XD5|b^<%ya z=RdIPXL@b2L-er(@0oa_BM@0Oe`N&r?6Z%VYT5oxb#yNVZJa%~QSdB^v}j&j{jocn zLHKhqe-Y7dWeuq|)9UI`lH312NnC5^EIp$^)c(DMK$3(YT@|n`WtSj#`z*autTcwO z`6E7SCbC8j%@EnvedSYJAEHjyY+eV90oLhGvPw;iI*Mu>Ou>X_{5fiK^bqP=bsaE{i@cu9wp9hYn}$11!=3v363Lixfo9N?dWD+`WxWj z(aQKf-qN{?(FzdTy8jbMPZz(Z06Xh@>20S~N003N=$ML$?u`KGskZ*f0GF6KoXP%DnCRc-l5TJtbL_Yb z4z9`VUTRmu-XPy>nB>2Wsf|R7baj9z7VMYDY2&sitX2A&O&)b_e-8Klp4RWklh4Net7RU)ZIU17VYkk`flm9vhIXCL!pN3t4|l1Rm$3n8eeP@ zcxy|6d{B55#dZtj(Ll(Aa%~6Z52Q#cE9Ap zj-kq8m#+DSF$>?9Rfx6L(IzooJ8<`2s2@-l+T$J-^d8;GERV;j!=?H1>lpiu@0j8X zNAq)~Gvu0MHXCKhd$li&-Q$3CAsp4mx34L$|5Lk8ST4yt_Jdp=SR%k4PST&r=GREW zdFz+QQab!S?c`&GN`?~^uB=K(?1h)&qP=#7+wqq5UYA77_fM7sMqPYkCL(O&-w_<7 z3vSBc9;7`9a?9~=_$bVV&xXM`*W42qT|U3K+sfNdbveCy$+pe6UEX8-Fo&0OM6GVc z=+5DoQZI)ayQmR$P~;=Si|oFTC&YLLN|=3J5;H+RO-MwW*>NKQw7Hgbtm`E&XR zXbl;{i7O!k|F-B7yp#uVF0@Efy7c!`s>|dC_%6FZ{E>2TK6*S+)O5qCt<^m#sDV_T zY!O|G?rp;E!)JkA>Lfz5oJ7+z$WLTbX6TTT=e*&8{8XF2_Pwh7eENNh+Zg+8Y~?s0bk{i+eUoYZ>XjnoP$Pta2`R z#?p?}w9pizz0lG9WU{Ic;B3Bj#l13?uz@7+Bak}M5ih=hy{%DwW$~VVjEVgzH~DT% zv=VEbE|i6S5(y>d5nplroR6@kn8u_Ta;JX{dr_7U^y~v*(hzu&6k$5ny%GPdNXL9H zn<2F~5h{ z)AWqlBza65>s5oIxExxZP$GfHIBgWJ7N#*K2QrUui@W9}B1O*HW?ak4VKz)YmrBX} zi=tZ2Pia=Jg)L|v3$`o7OxYJh2bc#K?|zm2N08e&BkVgv5qD!vKK*U=a=>hyj{B0Y z6Ega8Y@<0K!Qxe=%RA^-tI~IxSjsk)vo$*85uB_UlJOfxEv;@I>JgBaX^y>{%eU2&cV9r}gl^IQGT|rc?1dpG31twwsf$y}6q!i?1?IVBDaK(ifSS0)Z%)4%K z;emBQ{oR55Ix1I)MSc+5U*IN!9U(ae#wN>WT&OMvb{$aOw`ix|^#d0CpTpgQClW^G z8(u&6OHiMd?lgQH6YI9>d%Ey{Bo)1zCM2omV9Kfc^}{A#e1s!&zb^L1iS8#w$D6%B zv@{N@tpJ8rgVuD8T4IE&FqnZ5$~O5Mh>JiDB| z{*JovD8c+?^*3RL(cv?G=#ff`=R>e3oY6ennlbKkv8~P(Qe;fxX7K$kdl1;SSz`sQ z1qL~#SA(EUk)0VUp8t5?#~-lEkodiXLyl)MLq+6+s1sNO>h9j{wS@R=M&TW`V|sa@ zy?Ynd=ASK`MIIbWMk4xnotbsNDa#Tgao}R&tx@^)zmAR#&ovJp`d%~}MVjT-Tt}b* z70c0dGX~Hb77s}hiBzLQN@<)Lmd=15$r6Yts%u60U*D{p933)DFbkwni#<-L0tL`M zw>OsO4-AEm;t)vY(n{4rZQkx)bC@{>ST`rJcZ_$8(4o_M840a8 zL{BYrzePe^(6n;ad?lXv>gtNjw0v7Z6h)=`4Zick_zwN_RJqncz4u?Qpb0e5Vc^N( zC_>{X{ZT)>7@+2cUwzI`Ie)zoXecvm*W;!8=^g$?V2MZ0>5~yd>ytI$TMnkz=x>rq$bH$H1&`1-T79vd zCCPuOQW&KwdL#RI{B&!my?rCttI;;5dGq)9`rrFL4x!z?06g;tX(0MbsU9KX|Cqir zmZhPoY_QSu#@+x}t_T%a7wQ=qpLflry&IwuqKZAi&C@ z6SlOXc;0c*fa+T-fW+G!r3TdR2vY!tKbEH} z@Lwnz&P-r2Pn=>;AfV>vh#~65eD_7$_oKQhNmv;>&;-prgP3q()d{5&fE8z${Mwi&0{TNt_(pI;LcV)7!|ZMtnM^G60P@ zJTme+8(=#SI{?|JT5q^lqyTJQH4v}3G}xS?;@OHA^&Y$bBWFn2Yn^Tj%lRYfoulWp zfmKe=12+wXM|b$zZ)XaDp$#zZ0+3}-l}L4D!tQM*AkA&}ID0z}eVtJUL!5s^gPHFf zv_&w2-n}!v#Ll4WVQhz|P!bY^EL?qXSZ+Z$KFs{7VPPzBuX$^=sP$WXLr5?JX(nvO zgc$7gz!S zYm3(f;BV_0N*)7{##`ZB0_mg7!8xQWV17+H>@m;G^5I+}YEf9u{ho_ObDgal@tt>M zDgEE!WNeD|M{2((I;OD9jWt$EI#P}yC5`0iX6j|ZbNvw9fa;h80!t1Z@-kMdzp(1E zaUC9I2GtP4!l}L1o7|~kjj=uOcxr(4QJ|(4jHurF>mUNiUGPT5AF$8L<$}5hUQF>5 z5Plx&;p-b}($v=S3YvtMQw9d9CECvFZUdJLT(S)9pEe~G{(m#E0V4@{Y1#3E3k6ppg9&023awlboZ+?wHmXzDYVi{5e~mIsELf3L z&joXdYL&ac&HzC?x>=A`;3kg_^u+mFORVlhb`MK|O@*lc`ba6?PW(OWG0)RO_+&!X zd)6krix1l}$urwpD^(>QoaGy}9zhk|XVWFV#-XW+sI!0|ImYjT0Ot~*H1LQ5#{yM^ zYnr8PK`Ue<#dp}{{a%L*ZB?E5O!+Ii)1K0~A}f|M$=DPGbB{Q7=8T18`~w;C6~{z5 zMFeq8W)7{?h87XnG)z<+*60ZRVs|8>%JLIf%S>D#VGLIUB8BlOSB*;1^(wgu985fK z59-mxZuajD=TyT$GRZV^1l~&D6bY1;!R|?}@pU!}+62fLygG=zy9()3xmwG^El< z@sp@|3C9LIq?-ZPVMV{V3y>cU?eHjkyxe9yV-GlpYn5NKB-9TNb#+e(CnyAR6W({(*W{Ne z^i~1RQnkcns}Xu;BRS@MiQ_xvK3DkGG4Z*F;iO0n9(rS~Ah>?EM_70Vp| zf0o3v8|+tjENV_B*BMShB}G_8jSRZ4Quzo)7zi?MppoaYJ`pG`SH)<2piY?6OYCH}WF zNy5MKkTghwhQ>CC!8XWNF+oYfDCXI#n8>!>wkh&UJ!h=~TCF2rhaYd|pHZMvNx41q zy;HHwYAt1R{(0fsKBlwamt9Ut63#edt1@@t^9USrFWMgk@3Wrz+AYZrzPumwrFIw? zqsU{)&m$+NU{vD>G5q=NOY&TveDB2!!)aN{&$qffuum`9vW-*_e>3^SN-m|${3 z#NLlozJI1)1GUm3?7y0PkXu{CGqxm;2YaqzW)~o0EjAzhpPzKHa_Zzk)7Jx;W<99^6;pE=7g*w zQb*-A)$+(?gsifcR?gLdl>#ODY%_G;D7Mx?V^?;*%w^79~ zGFidcxLGzLT&k#-qJ+NS`|role)4wru5A6{J7^iyFCjSayK${jJ(Eh_d;*BoKuCme z3N+PO&EdFQWdO9_#PTbenUKl;(F3qG=HRc}wvdnMQX ztP|>o_qZ1{`D^PXT~bL8A5)l_iV$(2PL_ZwntcY`{g;lMl_r6O{DvGUW!j7DAUXwt zIM9|E@#i%?*wu5mV2+y9d-L6|Yfo=RvZ`5rH8o|IeW#vL{0pHLu{v+Zz&5wB5)lF{TOd#5n0A4zTIN%J8&5eAYf+2h>b<%4 z7!`7X65jJ26n0_{PHOMHukv^~BcF~W>l0e)@fL?z zC`uYoT`w>$aTce;%JC`H!;Rhj`Swtlp*_ zMjhI#F4kQiCV^GHzmU~-;v}h0n`J}xVS;r#`fz^AuFDaohL-T2D$ z8!LY<7MZ%6_w_F13rYnLpSX2G=a;>wLCtMpK|R^i&6#Tcv)L+c2=@sk!^7Cy4st2$zSuEQ z-ymvT(c8^9%j2;LwTLvJgAxQ&Jk>LE-2jijOeMIkEZ*Go^vb*jAU2D2dU|@5+8n5x zTOzr2(;+BsRH3G+Q48`d_qh#H_``$b<`BdSN#mNO3AYb^>N_Zd+}P8uZAYcB=3m0AwG}gs;!8maW6uazU3N9?nciCaXxGsPT7`oJMwy0SS!=3%sZ50ouU7 zWpNiU!06*Z7O&KI4b4U@ooPOJA%Q_szdir>>mLvXp*bX@buvCfqAk$nN%c(~zhbdP zAlWpTFr+rD3*e+~heL=9f?*QXw6U+A0TLS9#1NXQ`9JHgt@z@S z(d*o%FP_(B7AerNlBQl)hIA_Ckg~I$)1$C~3hxQLW3+g4$h^XgLDy9ITAz?m3AH38 zH$-t-4VpDo-`NvF?l(l$a?HXcex>|#7!NBS6#Or9fQTIpoKFKRe$KIIUjE*&@br53|NEx{S;n>cD(Bs?UP09y8pQ?Xt2N>+asO2&L zNT(pA6U?Kuc;cNs#4;$YfMXcI1Qxy7OauOzVR`fet z%)qMk)-#OZ!3`QuV9_TssS!e(SVQLGk7;2`Y6;A0R9LH{-_&{Hsc%YXhq?PL{l1RY zwX$Hr>tiJ=<>dwws^!f7{(d)of#g0{(qQ?0vSV?_8oQGRpEiI8%Cn)-!0?{XO~^HJ zShE6Uq$JEt=m|3pEe(^2wQZC&6F+N)@dQwBfrrxdIrjLNV4-EkX0H?y+&@DNzD;cG zPQ;^ZamU^-xyqpsV41R6a@&#hWhQeOhSn7x51dg6&!4%TFV|f4hl|AQ2CeazvYm=N zP_*q=c!$NUZBH=Dvt_x$3nH`-Y0-VT=@l>7U*dYI#W1ld*jQ-xl$Gq>9{FVaHqza_ zdmLUB#<5r^1Es@7!h zKAg9?)I16lUd;8OJaGOI(CnBZ_8NF_AAqkcws`w9$$!rZ<~kG`RW*aqU*ZW)xap=Z zj^pr{C&nW&hPK{8wANb9Ykae5Wm>Z2YRD4!N$$$cU+w9grMA>BN4YF@aJztti)`A; z(XY6*0e4NKuXCS?Rt$*uI*f4SVno7&x0jHSCnVRrVe$0u2>Wu2-^gz)OZqT^Q;y`` zuazWy&xD5GY#^l!oj6Za&39VNg+gMtq#JCk`3iqXH zMc#;4ry_LnpjJ8+dKfSBm^w`2S;wgIj?K~epHB+ew>rYXoq(x#)jEyD)TBiHU`2AS zre3xe|I}sFXEv6Eux9@I&A|&{SEcek2Ic2Jr6WmuFqmK`Kz)_WEG|`)T_qkNeT?+d z?*@Wom1h*f%=o1>Uq2F9elyuU)FsvhMaIa#2=qg_yfqxVwToXXOo}3c(oX`bXpQy^8$geRwCwql8l z4f-duC*Btv%GB>uhe-aY8K3?j8PYL8oL;c$yK|k^-nP>d?k#$2)+u&3OSr8V@ciiN zj&FCTHrCR!*GH6r3k2tYA=&Wz5cXt!u_G3eaY!@IsEOcGfHQ|MYgh{9sHZejC+ndP zK`pN+?+^c8koZzE69c=HGWg6}04@8|iYxy`Y(3BnoU4>wi%D%i>ez?q&)32gBbBB{ z6O(W3gMzT)CUSsxkq?VBJXAz+9mO2s7$@aw7habM;vB#lIo192OO z@KZ=X*Q8vD{@OHrEOj{JdfcL|ZYlE6Za1!J+;RM~elM3LpI{WoSNVuYQGTU`kNk4NkU zJj24J_#)NscLy|^okmjMQ^Yw1zLKeqROeQ7IA`%c?^w10=`rj^nYwr3Y!d_ zZvkNc1k^?;P%6dD>$N12v=iXT4ND)W-}$ZgQ)>Ry`B1SC>#*=~9E=(I_wq8ziD^>e zSo$Is`QzFKCH9`oq^>2tySNA+MByq$@v+c4m8VM{AH$AIY6omWmES)rQywp>*<;qh#^0c`(Jd^ zo&sLHsGkf15*LqNXvJbrlREg!7}oGui&Xy1>ICE6;<=Z`*I>oP87syqTl zu^Qq6Yq095s=_<@N}gXB>1xlv?x3)!Wzdxt+RTO+?%zcfSid0b53F$L@VlsuhMrEl zO2&aiypPHC-8U=6Ciz>X-Zrz2Pb$_ozXSR>jdvYKUq%HA zrtqsMEKbYjrO$JcqvJ&-4wwyYOP1h_0$DU1wnbSa5y_L1vTK#1nQP(!eaH=R!;37n z7Zb1tZc@#4{z$5*$<;3ury>lBmfDNX;G=*2pZOF}UwFEE4?R|zR;8RQdymOYO2O(D zh3Wc2oRf)Ed73T8x*t9O1k8Ze3kLhYNG>~UnTOmev!_m1w#?egVM{d4RArd7Xty_`$w?W4&N~V1wD`RJdI-i30$Ff9!uWA;`D!zAeW^`Hn{^WZ~i~ zr6CBk(v~=>z(E{ISiveH*VY8_`Hi5MY1k%<)(nv6I8703AgKW8X&a~O-w(G%i{c9S zW}fm!5AKpeE~s6dSO1>E5%-~f-$0|qgJ``h0OLPCk;#!r+6>~-rh4191AM)jp5_XWP06_o+mSb6 zH$F7lsPG#T6#{1L)|R&UGJ^UMFyhl^b7@Et09lMLU|l>6ZYqb`BUr+pDJO4l_%+C~ z%6~gCP)kt%L%9U~9_fFm7~ohgVoibNj-lpa4pE?*ltK!SID@HD4nRr-J5|8ip~aTB z7BJ7N!nHEhi6dfP{v8AkHQO#YYFcj zJ`(7Nd{hIy1KNzwYVA>_R@^=e&7i57k)g}ZU9wQX=)0AZHmaoY2}?tbQ+9>J36mbh z=7GIKQd_}Ae@D-TP`JARFjws`@zYJB62I@Id(T6H zH|;$|x=`49)I7AzKg2s|?); zieV3f0_#~}PUZ!1G2EAmIo(c2vez$Wg=n!VTq z<5n3ZfuXsW8bsv>hUzM#Tm{nMXf*WbjhFJFBFt-|stPJjr0i_P1iDZr4tae`wV5ZVn z&|XUQej*~M*~$R2Ypcwvwk9P`GMXboGzN}fm?gBDu*UPb04*KaW;!x*)mt`!`RE;u{$XG4vkG7(HukC9JT3Tc%51PYHJjvI>VE+xnon;?* zo&}_LW(HPTN-UlFP4K++o66PQTiktN8Q~a9K8rJQPa_mLpDa0p5VZq{{@mI12_Y~R zTS+U|K~_#4Xz#xr`yPAy?6f}$tvo#%o~-v^H5t&%s(ngH`8R*Bw0}M3ad@<2_x|C7 ze=@yDd;%^8DciLwu|OEh;P(Rd-R7hvzacPA=Q2V34zZmj>mc^ncWe0f4{t4+mp&0O zGED;O+Ho{8=Y(U5ReD9$=P~}vu%*8;WEjA8ORz_tWwOY%HY~O(nQ4ZBj6w^qQHx#U zbL$wOE>inMKPQc#=hxZU6BI3zmb$rh8diP-YfkMeQ1AhSlk~IsfZ6e`t#>`M-^{zf z0=TvXeMxnPl~BK>e1uF#gtbY3O-h@X`kd_1@ye$p;-Q{6`ea`fIA)lftE5ui756y` z6Ra}lhQ0*d_c2qIipGZd;5TO0n`HWpju7%9`9H|ABe6yWH(q2X_P=K|LKTk32mR@{JIO4o4-2VNGijGHTKWQsyfd z_n?uLg(m>!6+(Z6I&C8e1B3Sb%sDAi^^=G+epl zWQyi*X#?c^Q@xdA-04BypcDsdEAnc7+^eE)h?)+6A4AAP!0o4SFD4jwtkj+6R(d^o z{x>e_nv(ajtqu*81}klyJZd9~Wx{}hp{c1x>u%)?8;j`rK=-=5JQ>Ms#}RJc<0SMl zt8#JHb{|EaKrPWZ%Dm^Hn;kyqO%e{kNP-dC7+9}`rgk8`esjUv%S8EU4HM_>n@;in z-j)&DAgla-%)$Yuve-FYkY(P!4h_vw@%{(NIZS?0PRCbPzeH)cYlK*2sTj{&L1s%! zE6^uLTsCCPHaRe!f`}K>e&2qU+c?$J`}})iN`Sgo$;z~7-&774`tif{g~0X9ycegp z<-4KO5v;+%VB@$lV=|yQRB5()=>T~12+a0;zx6PCB`JrS*O)#vRMHWZRIt}S8BtnV z*m8Xz(0K$2oXXp&rGU$;zaReqP@ws_X$i@F&uzPosu`%n22RI()F`wxDv%z9=(K`dNQS03Fq7)I}`6LKq_D#w6fO^jn2jHAk+ZFnxT78&}5e z?vkQ%wk{+o&b-@G!Q3uXzt%LyHC*=7Y66pzXYOhMFK`WV+m*&w$5g7ZrtCG#%M%fl zpBx{lb3@qa#DNRkB`QY3(}BM(M9V54D9n1d=p{Q>2ga4@qr&~*=8|Wvw1_PtV*AVA zqtiCY*|p9Dt`kA>#+)es{^;266{lqb&COBP6Qk<}RD+BOmGxI0UQcFPALE=L*5ht; z`@pf^nQvyRyF9$}p_>#eXj~Z8BKK>O)MRBsV&feXy-J?=%3;IxYLL&xDKVfBi{AcY zpb_%|VMaHtJ+5UoiB7sAsQRlDs_`mS=E9ZuU~DR9%ZAuw1seG0sRdWXMf!EBmMGc$w4xG z3NPUWhW^~?4XNQcF12T7z!}}7hS@>$1Hc>Kems&}Jo30%Pulv5?uQYYU$B44B9kz8 zJ%E?0WCq3QHC}Hsc-hEDmJw&p1A)hxvmOFbz&~d_rKBw$zI|TqM%5}Xhl}q$WhMvA za0}#J!psYo7=8=LeIn+VztxR9mUhM16dk324rTw4f^`@^l`QZ_E!mdR$ufObH)~lp z%nh4!uG3Qs!I*VxMsa!e3-$tSTBt-dL5{~7PAHAxx->4pV9rxmgo>$4)U`Z*ysFrC z^kVh12#Iv(%muM0O&)yf#SVYpv%taX7aK_&q{*8Q`Kf7_!?!h)&?<$Z5;XlnD~g=e z20Mo8M}8n6ebWBW{FLE;=kO%!cil}}y+~VmY1y7>xZ}97X{yOmFyJJD$?aB^;@lLH z;S5~H55Se$!;JG5?ToP6>xdhG&WJd0S8io)w=iHLs3{yi307-oWx4cYV0DMvSG3KZ^Gc3GMBpbYIszoIU-z z%Hr3E_ztl0|9Xv?IUq%~hrzQpwX=F34}MnuRI#cZEnj+ZJ2blKXh^~UQxSWxszp7 zr>_{fbe(t*c{WA=Q-QjA%Ey6h-G$*HIt0(Vx({6}YG)_iULQiUk45D5&}_=OJ*#$U zqF(|Y^6y(;KMD;&I7t31Jp}?yUQ<-jF0LWEqy5#P{ja*MYZeW*u|WbwtfNu@)H8sWtE<4@MMt&at+b9F?ybHn0N@Rud_X?0;As zLEoVYZ^XvvgXWd*cO0h3+uqBN>5UHfPPj4vnlBOYf|Q4=W)(a-D~}2@G3nHxk(~1Ww0>M`3Lti(TXq~ z>Ta@h-YEhQ&~@DD26=0LAfNhwofY=^?ciIXk`bV!-DMs>?pbmqJC{2s)Czo*VHD5c zxt=NjQm46*(6tUyw5%d6oem>TDY^RA>z@pJP6a-|wqUVYNlO;;Dt&uopW3Zj3L2%xgL{RGR8xo@ z3w5Dbccj-UC11IHaQJtBrn-kd-@=3yt2PXa>tWm2P^CGW==+l3ENvj6C2)X)GIE$% zE^NXaGNI1@r6!J^>mw^5ki89QbSDsx3PGVsNxwFYuKSHTDMhoYzgPuv$df~@J`cXJ z?KPv$O%yrRfc0&fO+EJD7zd~!dK@@nWXJ3lV=NgxMVVo|CG!y+iecxADOKf%9yfm( z5C7>=7%3ExxoXsCe~zpFzV4CW5(ZSFbv?fEypo}`sPPPCo7~wm$OIm=A0D{J>vQ|x zhreYxvAoLIsXPXz2_=%KKJ=e5aZ-r*0=4WgU;2@Err2s!Q&%WgS2j$-P+#Y_Epec( z1stM?cqxkVW1F_CWU=k=V*EG7c`A7JUAFqTp>J8|V>5huT} z{H`Z45fMjdVzrJwo-|K``_;L=Y9{HhMWQ$zoz#?k zXwr1&r_fOAT^0gmxLmr?3`eTZaL->j%bDpTn*Ku-*>cSAXcc9=6kcK3Lm_meBJ$bf$?tdF^k_DdZAoizqwi z0Y)Gb&KAZpcJ*iB}$ugrlc*+Q7#K(iY_De>~M3HFE+XEh2pMOA` z5Gffo8gZX^+^e%b;Xdw&12o?Fn69v%x z7elQll~fSf0PZT5;7y$ten_5i&GM9M5@^&sl>c0*JW(pCA5Rv4qbQ|CJA9HqmzHDM z6$B+N)8gX<#QQTUQvtDPjlaiwg_gHq9r7fy9v?{1d2LwJe6^TuN)j2joa%!opH}QQ+MigdJ(dmeSYbFX$`#dr z%sRc9)Zr<$P$H?v{ZL_GYt>NrCC}_U(2reNtdFj3D%t-Ec{}>fj#)L&WES_snE!pp z#YQ?Pp0fle9(ruvB!s5p#lOIp&NEcK>M$g68VF=^D_5w2lHT{41}l5i*%y++5jk%19xG8#kbvWga^j2Sbm1ZgIuB+FDLHFCw!TrzP;TKD5J&WK|#i3?Y4;?Ly@;^?i0f8 z^TrXFp4~i)Bkz^wy(YoTQE|>teB+cD4DUI&6sd3K@fl>j0UU|N%YOc=XJhR~4U094 z$Jawz2ZLDu_X4zEizd&_rN~vsXG}$`HD!$Lv^;fcz?-%&F9gO5G}dsaqK5}5EzaV= z0!mtZE^z+hPs^svid5ztq@)o)q)of%7++&>uqKxFy9;%LYVAMp1hW=_!w^8up>NSQ6}vziuNreZ6D&6D((>zeLWkCPsF z;h6onLcxjToN*+>7#k;-1Gmh8%kxxi3I_bTn)&s&@iM6$={#l-c_PL6PqvpA=c)(F6l1Oq!nVG0 zszt1|wf|}{TNj@a&E80WytAyvv=P3wHzRvH$|@wTEo4<_9f5R-0zP8CA^v9$kFtIM zRoGa_*X@{w(~l2_f{i6Iop2ly_o~093=@zfz%(IvlFYGs9_+|V5lev~a`ZK2;2i_s zet}hYnkp}qgJDY_aIdTa?^v*AVxHgJkF4}jf$PZsh_$d>!OfFbi2GOn?*xa?`h2r^ z@dF^nb*O2Q?vTlam3>B#)S_&FQQahc>L18+Q}xEI-U78g3$dUHB6Dmdt9)eF3!lU> z1qQ9)HXD?LN z@#f@o=!W}Rq1*iAwq{cb)S#kcLOMUL%Zhj&^^=}|6Sy7D!`HU>jtNbi9U|{?zHMPP ziSF>qB{Maln)2jExO|%OpuH?7vmv}~i?@_Dh$#@F32Ps&!FDEJK#T3Y7bUW;4#n`J_8W%%Z@!9V&2VMJxr44ImJEzQ(Yy5tt_ z42*tg!XvO7+fF}svxBvP+-TOpIF=|djX@X4=>7M}F;C_$$j^QPHGZiAvt1%z%kE`* z%XyKpMr|Mg&#KM3A!p>Z(EmxEeg3Mcyn9x-Y(JDxP0}ml2i)p2LsgD}dJlglNSOsw zh_9(50XWl8T`?@y6)vli#XEw5r4A>cRMKqIuRdPH{kE-d{w=e!_Y{qlPV8-X{@^+{ zw&WN$zXvl0qx@GnK4aKVt>n)$-@Ds-qCO=#4?0CT2XC1WCzKhkoXw;70^lSjA`IXN z&Y?nykTUtn74+5a%8PD{V&O{fbx_+(@$BL!wvBK7pK!v&3Es(4BL#fL`^A(pH7}` zX|lvf!v_ao+O%jf?K(e0O(bJ?Og!g!^Og?Xx;!w#KA5ZZ`@K{4*hwK~^o*O}r(Ql& zDaksx;CvRED~F!@LfCgPhoa9h>Qd~&;vW6r>FM`=mRtPVlD;^K4h7b7t0tz0NcHJs6ZP$)#ZBwAmkl%`10j%QBh_6#9q9e8(gZ=NL{2Ysv9z$p$UDp< zaN?beKeJpUsX^(TaC1N0HRgipDH!I7(oHKFYS>I0^$3`&L`15|<}z?7X-vgg!^5b0 zm9QhU=W<(f>|}%0M4ZEWU>xDlQC@6nC>1cw5Z`z3Maa+Bq8Q$9CILhE@W=)%S`>n) z%dtUq64X&#ajgbcHA{%*6PAubtmzH>r5Z>c`? zAdjZR5JZ2`KDsb9x4d$Ce?V3LYg*rH{FVEMLZ8zS%1IN@pj+O}_pi_aw^B$2Eda%{ zdJRc3a#?i;F(K{V2UqnVfyWs1axhao+B)eU(6zZ1z$NP6Ih0(Hs)+?_$!6P$z*3BZ z@i(&VVE;g|49%tbX_~I#q90D(#~sV0XLI_af~n>8AwP4_N4Q0TG!yxiL2fA2)a8<( z8E@ zbp6>!>Q5}EB0E2D#qb)7YitE=w!?)+6*a~@M||4Wui7+$j?W%d(WmtCQqS}2@6qii zIoDrZBh>MmAD$EH#@W)Vm?6q8RE2WZr^#nnfFhAWHOUUa(H~84zY!iDe zV0?L|dU(|R0?eAwmZpr`76u3ah@|55adaw!_L2FaBE%*$WoLN|2wyRvLH@@IR>9XlJUz$6p{D|iqKGe@8o1>|P z{JZkIt=%~dZ4~McPTQ2Y9;t*PcXoD;0eW?Vyb%)LNx`Vczy>!_hh~)Lr>+-l{}Vs) z?rb9g^urakfbhTYeT=I^A&;ymj9`d{`#^X-qH}f`r2_c4OF{rWsNb2)b z=MLPx!Byv$k^6)=rwIB(vc+qT&xL^k?)nk%15r<12t$f}vOc7anNCg*0atszhTNZPn|C42Rs2!y_1Pb)S9}0-S~(Raz}Am~|>SzxXB~xEKo0@QWHQ!*dGEza$V@ zk_>)7Xffh4u8cIF8q8~;Q!=X?+t3=f*~M@$9O6_+;^nX^v>n@Y!FbRe^-tl#%Hht{ zBy!IfmBdzi4dk}I);LA12V7I@nn`>*~A0&x&?EF_9fQ9f~bd_a7@wa?dMFO*e zoXR8w7(ligiN>gM6MHy|Bc2pBk1kCSQOM6KcG7Fxlvtlcb|YQW@R4^?!aSS`k7u6!A05_tXSmuX}v-{cJz-w6F)4(ry5c9IM42%pzG&%w=$l|fr z{Ko|sE@b-ROk!a{*yr6YqY>b+K`qp@=@DEp@Xm}URz?9ziO=&k#mlbv6CsE3>KRJV zW#l5wpiI=TMI}3XTIKHMW-#yZGfj@_?qWY`A{7M?DdKgHB zbSO@QBkhQh;ufbXt)I(EtWPz^)TXwHo@JvTzWPq*Ikot*9b!8iH|88?0$F#fUV zaAs5uWeh)dg5R4C`u-$})Jlf!3q?bq?!f6D*o9Dlz^tuF{o?8U?{}j!oVkV?DMAzR z3#Jwa1K47r3er^==qf42Hbo0m*B*{6N!o0Qewt}VvWc62&tLDedIl6$D%N=n4*U(_ z9U+yE=0djR?(-XW5;n>APRA(G?$Z%MdVKorvkqSQ>p;H(1wl!YP=$5lhG+*Bc&4{W z?}Nda7m~8{J?G{pkEvvs+cwk-nxp0Y5$m-65$KL&_tia1IUumBD!r46KF?AdBR@%z zE~_;w(c1=JoQh!WAhpiR$*z8>g0yj^I+3lUg#xibxG*#R|eC0%>5ft8yG=tF>nq5epTSThG*VjiH!hrIC->rP! ztI8D(h9TKlaHzcE`JN!%Lc#XiVbHEIpApaNVe=&DBG3-(l2}~M3=I1|JJ%-h3T%CT zpQlcMJMdv!nkmRA(nZVH6e5#C~^U zLzILKXRl^6D)^Li0PG((bLo?uxcBwQJB}9$o&kNPe-ksWyk^QEFAzf_%-tFa;_uHZ zd2u@4HC*Qg`Fnv*HT3P5`-&2L57E`C9Z{G zn|#KfK56v;L;y#7by*bv2w!#x=mD7Z zKhX@Y=oxMW^-fx-(yPBsrtRWCR;dIj--Y)?&}WEzJ_gD9*K)D$&nqXDPA5m*YFsv9 z+ZiGEO}UHw+jSt6m*LO%4oHA5ch5-?e4C9-2h9koXztm$rVO53qhx8a5)mI^y8{PG zo2sHH?mAZKOsLno#y7P2c>$5uJDg-o5`9(aTGOM~ziJZ-gO~NMg-tP7laMd&URM>k zpJn&c-E|1|jU|Ds8x6Ryn`&y@7pE`wuH(AyE``*WTmsy|x>hlBniE8Yy0~~mW^;#p znOA^A43Q!8%J=7BD|EoD2ZhRCA%osRccQi$@yBeqpxcn2XYjBcf+^BeQBsw z*nl%(#Ukt`pAUi%eHSCR#&i9sh;9ZPR6u5p8SViABhP}|t!P^Kc)h+&?-Ct3_S^}$ z3nidYZHN7G0;-~;J}{7ZV~o$UfM4KcUoIz>20Lku?BIs%Rn^CZ?(&7>_?6WfUAELh zU3z2jFYan1(ttjiYI+*j^>$PctfS}r4@l$^$jateg23d}p70&tyEZ4J7?7<6F8#e@ z1OhO=@@Y@@^~({z%I+$h{|0R^mlBH4^e_=(CcytNgTPG!%q8XQ>FxiJMQh+ebhVJd zG`Q>=Orr*&k)vOE$Zra|3*I~si9Taj*9785=Oc99KE}=0%o!M!Cm6CTcX4qfLrKJ(gDSH^oj=y}JAIy!gUaryu!du8^5qqj!@)fX7s1SFkZi*WnL_K)=${;xa1YTOj~AG#D=vb}N0 zFkQ)$_R)SoEw`I!emT(hKi!NeT)^kr_)UI73oLByPoU|1ImQX}M2g9M_j3{A zgq&0iBLSwx2k=Sp4Y#1zS?~e6CmZ>XuM6#Ms}{%{)C|J(ErwMw_8(-a6VPX#%m)W!N?xi z2jt$RMANO`4l?x9?|r=y-}v(|c^dVe%RdZDGa(`(MNM1_a3$R}0rNSNv~kRN9icTL ztO&%2KFPKnthgaE>=+=TF~E%@>lXA5A5ilG1it9>%i$^g)$`vB9n@XFLdBRvg9f4R z+_`q!{28+b_bm9)Xnx#LB3suWV$yBfa4Ahc$`}S2zvd6TqVJnDVs4mipL6$aM^lH< zE;~kl{vqPY_87xB15};s5fBy7cIfoicGrtA^KPyAu71NJsyp?JGFW&6CUY00Y-#uF zb84oGqphy4PMh3kbX524_A~7PL6uL5qJ;((SgYp94(Zosj)OLvC5%{REYc>e(}vvz z*Xo5uIAbuCxxgAzSN}O+n`+Kf(btFj`_)6daUEx8e+7Jb!TQb$IK3W6f}STI$JZY* zio-%CH~9a2gdP3{yD;>mJn=S-#&6mbS0{hDo<;RS>RTaRQ}Spo)XwQLug zScR=*u!=`H?KpRyYen;9iRM3{Vbgj?{c!b+UjUA~sfu@ZJFTq@*{xJ|uZDN?1TmR- zEkt!%&WCaW93n+7o6qPXwFt}m(lN-3bFqcPV^l;F4P&c1jtepOG7zyhaxx?~NMNh2*7w;Z?CY zF5`sA+2hY|0F^n}gIhr{P_3cu#7=u~WnMLLhnd$X{oT4$nRATUb?}uQ@98WrKv_!W zD0Y$XM~MMk^cyJRx6}Z^Yk|tACFs46tYP)%D)z8(AHhH$h8Ky8%7u8O*7FXKf!BQ>ZceZtxLjvRp@~BolypW4UU<0^?gx8uP&I zNLKFD*C_gl*S*NtgJTC4QDX+ffm&lV%S-8rw-~-Yn%>aqovX=w@YlnHyr|^co zA)_ANq0)|iD~G{ zjco=@Sbn~V#u4*1SrLEV5^rY^I7{&NxytMmze+M5G&h}-T~y1mwWZ}f+-Qn&6@bj; zfHKAhqIS!Bg%WW;Su(kEaLu$p51qBXA)T1N4mE4klLcd3n#-HLpPUEcpr>aMRA*;H z*ym)!{_c^rKfGryIicS`=)|ZBj@!}+^U6lT3Pp^-z29rc_FW$l?#nGYfeHNW1)dm+ zK~UwqQl*M*d(QFoFmEfWjcPBBArVGu)V5c#J=xQpMf(y4rM)NsjL+F)$da!+^ zfoqJm4Mud2k1J&q?*f`BO=g-5RpvhF1?vZqup)0x5A$EAQCJ^?1>wF_bno6&@$BW9 zd?JO!)UAk$xC8|3{PTjBP*L68Vq=Ru90fzd%e5B@yVGJ8qeu1xF{I8KnaUb}MI(Fq zziL$J<5in_`@7n4InpdDoQ(q%XP@EQW>ok4q7wM7c`hlyunj{A-2RW;WOhalDUXjq zcPJh6BEB7O=)z-ci*;*sKp%<@-*`O)9JN6a_JC4%&ODDuIeWy5BQuoGQU0dFJB7C4 zVAg=#5DmZvc08WqifUjWVp$J~0lN)$4nQUYy`$$o{zirvZIGf;_uR#~;*EqJlo{uW zyGlcfeIsh7+sXyKMWs&;^tk2||CzZx`@Z7(oTSYhERk>0s0S-fc?x8ReiZ!gAOP80 zbcLKnR=`U(_-r%kfsDOlb4-#4Rf(-tqu%DD1ge>n){fAPrSv?MEBl}wQ4(xT4|kI& zZ6nV7FX~jEE5u)w);tbF5WmEu6NtrU*VtHYDzc5=(t%mljX-1Ys`%=YFuz>HWb^-3@O6)$KCB1yi$AG}e=o~(aEy}@Tb((p8OWwn9d? zy0M>oWXH5Py7jBpNZ;~26=4cPhdvIR;8)5P660H$+}H+PJ|oV77w_U!g8qeP7s&=W z%52&X{wb{-|NP578pI8#*R!BKYkKgHtJg^%?oC;W%_RSsn~i03Y-N5~qsp|@gJNvBh|KU}g7deY zSU$OBevVN`;us>7lAsO$4mR6r#n(F)f%?<^enrr5ALU0dIot>3uC55{`C-Qx>b zygnNCPeICc77tC&N5zWaztu%QC@&ob3>z$I;RySn{2PMfV>5$lE>c_OI^q-2*&U=% zP9k%+bp0jNM?cC$W^a}$ZoIvHINJV=DRu_9U=fNy zpU+?4>$K8-y^Pg{(^U+-rV1ts8B*cdp5nnm4D_-gh%O(rS@i$sh3#{~3GNMBj64~a z=BSh_NFPe9c5MCGy7|MoGzwX^DW;MZZr2#;ntWWLkB$&AJNA)-nLqO_uq020f_ zBo1+bOCA+J0`!2-TiLYm0hT?B`$cTTSwU{$vc!SHX_*8kGjgM%*~tAU5<6{6Lb`yQ z#du}+2v4GY3dd#!#!0)M8!{HR)lDJj2W>f-nSL7jbp}Q*3(o3cW{u^HqXZ{b#jX;E zp#x@N-odMZ(e4atadd_Pb?YYwZ7DbFFGk856@vT$m%&VBO4JlK&ENc=9XKZo7U!&|8BBaF&?PpP-~ zfELUPpPDe;UUMXQ10|-=`)I-gFQ~vg{QWu`#7b=G@CA9P33|8~snMIpC#S9@q?K*% z*vEN3FE8{GwSDrw&o)cg^{kK7;R_ELWue~|VkzjoK@Z7vp{H^x&gE(Hp+9$l)BsfU zE4b*ZeLZU(A$bp-IzE~4GP2|?ZT;4AyiS)n+2)U2G_hC;1q7U8+u{?yjB6jcgbM?H zh)O*%MJnNar*}^KLqvU`U8|nDwrq??bSKJ%vbzIGN#X{w>Ggz?a<*a`gNl1{KG&pd zv!Rw!CB^pO>hul%aqR#QrIhn5e*uQpb8v@ANqOSOf`qwCaX4SY}s{uu0;Ku zg+EGwQhGM&!NGF0lsPuXN0f*mDck&=tY$4WE2_$UUU{H>+j8Z3i3 z(CM+Hm#8~htx>4e^)B^*&9b1A(VDwR6DyGreQkmbDq3JlbwRYc1Va9pMBP8^2gD)s zzOw~QEjk^YciI4aOe?80d2G)@ca%3C&t5>4K$hL))35R0S_88sY5>V(wR-!y$&|zw zsGj1fOJg^yioG9+1A-Aboj;*i`(!7)j_xK0C^HMLr)%4Ly8P>jaoPP&RUS)tS-B+l zIj%T+(LeMtz-Fdy!2VTlovWGBNW!oudn9N#T}yPgsKA&t>+t+5uXF2V$Cw()NF->Z zx$Egm#Sop^nh|FlTc2P)xOGXSsM>xI2-*xRwhk+emZw1ebfi@l&A2*vupG%)Xj-d~ z3CPk{Gtfe`Pj^N%U3=#8SFhMj8ph^@FwWrgb#Rs6CaahZY z7sXna6+6+@Vop+)+y*s5oyEmZ1J-FpE@AoQCIjOxi-#XY%oQzOA@2BQFcJAL8e@G5 zMFk8JYRr^)lKh401gI^7RGT|Zfv-7G6NpJAF)s}#1t&hU$R5}=(oDL)MiC6wdxWQ+ zBK>G^d^d{|T2z|ca?fBJmB}#Z8guzP>#Q|q8U+>$2G!DRPU)>|s+WR#GGairiFpbA z^MAm2XJI4fjOMQO9vPlAcy!r67CYX@7sJ-~*fq|U(D%xiqC-UB3T9trSP)PvYiotx zdw#ubxSUCr_>4Bu>aG~Cjc4m`2FCM9rpSqjltsrA`CKsZrBTMqcQDYRYZ=$yM3-aH zz@hXoqgWKC2Y1s|ClCtOnGR*3nhQGCpA}Lwe8+zndvKC1A0sEbhugt0S=T~6dNw^Z z0}9{fo#GWFzC!>3QSm4F^v~p0a-b6dkGRdsL0yT0 z)k6t)?S*j3q|%Qn%wHWfVa|0Z8L~0Kv#NPK0*0PlHq<-!rnluC=Hexo+L~l{vXKXE zlDX4B(%l1}Q*ZhC{b3|QOr8efmt0c5VD-<+Sb@Vbb;;B8YYlJ|n|#~2Ga z(M$(hb69nDc0-zM|IlVPocKaDmQHkQ<~uH~mjTPP$>Dw0)k+iQkU9SLyvzqP2dPAz zWXs^#j5`$`=?CPiQJa|NTMczcIm+7FF1Pkms)NhwNv| z6z}Qs%b9TC<1B*%JDVW~D|Y5i=s2Ms{^}-Ry_1&}R6|!#vJ-L1a`?79Hb>L>!dj5cYv1v@Zwu!IR=agU82g6<2>*1uiA|-!P1%U?bRf z>%F(>z~9T|d+o(^xk!LptS~81vjE!nW;Z88?ijuZxw;RL#WZ|d@f*G6u72V#2ChUJ zNjR>6cVuFH4=C|mR5-j7#S`{HAtY>nN9C(RJ$e|Q_KvF}elpE8Q4y?VbC&CyRGJ&Z z7yM9-JLWB+Fh(w)D$ggjA&9nlRUYF7s=tD@J`;jW2XaXYoypfll?za&A zbQ)6R2{?@dZ#-RI!rKdf8*~(jKg-)_dugw5CI%$@z=oQsbkV-h#&GL)(fZ7?!@pSx zSA)y9EExNnTU#L|YPtQ^Vbqgu4jFh-qdV-2$1oBEmTq_0=a2~3zYC63W|(`r>>Rpt z@BTct1-t%D--n8DlpgZw5XVD2t|EWCfwgpwyf7pBxO~O3RYS5OHT(b6+WsMvPh=}Q zR7{f|16W?3)Uf&;pGhjTW)Oz!(NNZWMrnrer2+4^j7STbpx9U^b z-q+3YD&ca{O<`}HZS}E=aeU~p9iIZiVPhr4@Q~xCZwH=Nc9<{p+gAX{GV^vI=!MX^ z>xt;)O#B%zZNtV(-%&?S&Iyi*jOk}I6&)IZP1)JmKPuLjregFd*wJCl`^lydCJzv# z573XYU%0#JFDt@gYr~lne`P+jKiIJ6WbG0INGcI{@Tx%WhB1%odx!O_m5vVT@qvmW zTQcbHUuYG-qsj@e6V=3?)AERkJk=FGC@*_$v6zOu;>q6qF9|gkYI25Q>^q)>yLV(Y z$Yg>St6c++!3R^F$33%d;?G&>5)nWqIHMR6YQvM}aTxzCqa?c9BzDJ}e?Q?1L`o)k zV9-w5ovHM@BDrsVzj8lM6@)@+M1^bMb9E9Khu4EtrI1WrOJj{HwTt0LkIG{QCZ!A38B9 z%8&3(YmAos8WGzYvw5DP>t)>WmwOKXO%lnM*kH&^qTFAXdhwx{>`!de2lE^6^SCH7 zDIm2QTV~SBRv|m`qJi^Xg^5ua#dwalr!Mha`RY7_IRc+bF#grSDlN_NK1TIPVr-}a zwB4xaca-li6qWWH7&QSzQ}4hY7q!hPX@nJ3l8}dmQDIuYDU7s;M;#GJ&AG{8Sr3;cHIv!RHoX;MbKg|A1^{zTEApU zACZl8FjhqI^6)zL!4I!1TZ8!4j!G*8#9r>@+43Wii~xsz&x!a>wl`(HUe*jJUYpm( z9?;E0xMNT#o49ID07-!rk?MMJbJeLEnbgX~!2Y%k#^f*Rvl5ZV(IU09`G(HmIm)7H z-1?1RYw0D)J(Yn8$0bX@>GeIo^7h7VFpO4V@WrecfWhIIkd)~rpjQ->xd|~LrwcQw zEj^n!`w)V698ZCb)<=A$w47{{4&IVbcz@eenPOq@|LgZDTos0T6=b z?e?v~F6m1)=S8!KU?kzrugyO^t6^>F|3)QVAvv2YkhFPQJ1F zZPRwkcG3LgmFSwcN6(DdyOXcpcdlXxiY}}@veivkbJ6%%Wyt{!$TSJ{rQ544Vvg!` zbS?sdAedNPS^q1%BGLk#c#R|lwtR%Fo45oNJlc`B5l__BnXqb?fG60)1In76Fj(hR zrAj_=0#qWkvTKm!&y>)q(Z~uMy@4NQfHU8W1Clx!$9lF`?u5`F{Q-I=!@%521=h8t zUDJycLy1Bmr(z0jQ7UXWB;nL@dR~QPJGJtYa*5G-GGuxAwvI7TSZ5de3x8h?lKpLq z>Xf6`h!@>{ub$vE-=r?dPOg1j!#SZT9+9te-X*hf8Y?1(#=>kaTpKY zNvADS-9k$!W=hpb?Ty>MjjcZP780RB@xMI4+qVi40Q{F&x+$*i52R~Kh@&LZy2-4m zW)u1;gp%VcYepAGcRc_8{ky|{xdbNOzEbL+fQen)kSHYQ3|st(RIBm5(%Y2iw}}em zSSu?lZomOdu!Vy{$Bft0P@RY}79je{d`1;CD5!dLjjil1pIGuqYs8Ljyy_w1q2-SMb$^ZaLO-|s z$N{Iqwqx@;+J)B0UHK?-Je6@GC}Z_G`0&u$!!SZB(TbG6xl+g8$;rtkgLbeO7&lus zS#-Hs(_WqUzN<_oaV;Q^*v>(JS5qS+ucXPdzjgllHMy7Ybu9uTS9Z#AAELMS4Ui&n zDF5*n{RBTt6Nv#^J=4ZC1QADJHl}$U5>Yp!T>nG=(cXSTFphUfP6GjLdR`e6B&(~S zA_^|>$tYo*PMpsuhC*@}52LQE#>dAIZJBM^b@lRpU{no0Kyf<#C!eZfyS`%7KBN0g zP-4JP=&gi@m?XH2l$PljrhAuad82;|vQ`L!xj_!$K%s1^H?A z;|{W{k;1;{Bjxv3v*F1680+Mm$!@c>;HTXtD1q_qlaOiYuA9>WQfRkYwnH1 z$vY?GriSrhku$md>ln~b#L6FcWA=>#YHbm0Sv~x$y%zC=B?-f7pT`5>C==XdizYVZ zEedpm9um+0DoT?~ksWYq#`iT`b`{~@BKSv8#@4U0it6dpXwY4^cfI#UKzRCvxRn6n zGzj^)l(3smrvigD5xQ!dg*q{Ad>j0aw5l(WxP&Lbes}7fdusf$uIBS>7GWlzB1#MI zmjIfjkbgl1Yr5F=N$!K24*z@@5|Pdqr&m=HvSxT>&Y7Q;mi;5$fh`#Hl6oT%WD%F$ zN%__~EMMpv2$@<7-gLPMDqvm15BGuo@2>5ayU7}*Bu`b7$FLM zF7TgwkL^K=m*4bm;pFB(#kbaXRfrdT6Q{1dX~B%VR1o8-Q`P zs=J;1G$#>W@+!7{(|~1>ZrKQ%=zr=;A4dN z76MKPy^q>@k37}NMFG`V$JIad%bcg`&AaGHLGtU$`BVpO9bDxDA%09tg|b6BFieiE zlH@O)-lOoIZq#>(7id&-Ctc?J8vlZTv-P$D=>%xU?IjS_coRmiPL@;zcy8lAJF-&w zGgtg29?IS8I~{PxDRL$&uQO>ih$LV}u97J>ONwzbgzfjich~-|6WnjSAC%Suw!}&X3f$ep+{l zyTrup4@pnL5AE**P1HF?>SAokY%?jd8UMBk@|YKoxC)YQ$?=%B8oc%UPQ|H3mL8cQ zfhSxNWt(As!i_CN7Uw3zi@l}5+8Sv#gIbgNRFA# z2~JqB9)9*)=^-#vindw&)XRSQ-s$Dr!C${t$;_L9UVxFbp0rDM(sg6x|EvZ=1AX9} z{QB`1VWHek9vH6(du_8AYD+$R{z(JoF)`g3GaGFCI%x-lhQzTtzo?>NPKS^zZyR45 z)#*$cu8ko-8^hZ;O=Tijzb17GEQ2$ZTf6XVP^SOD?fgFmJxuuT5bw2s=n8o{`r5*# z4Ijbt$2&fO=4K%31uIO%-3De&EiDVDk9Q0!Yioc3>Gmu>sMo5#xLgS@n-hcLY04CD z+^qIHEAd!836yNsL^Cg^dHm4V4{}>#+v}!lt_e3whtbo`^11~DDg7*Uo!hdKT}OZG z^iwZbgZC-t$y9Bb?@~I#JvcuZQ%84>{CYf{w6gKGE_8!db273k8GmHx_*gVTx=-JE zzEbC1j7n>JC!Lx*e4M?j_EdE%80{`C|NX zL3NA$-@|nLx>-<*wEcipeQ#Pa5S-cp0e&*Y(K;EX#OX8{886#8p01zlTA^;0wuSB+MKey#iZbHh@}?P+so)bg<4XJ%pX9Rx z*YPBV__RHu!vsBM-**&PD1T*Dbd%G5ozE>h*Xva=mTTk)$WGjwNjI{lmE=4T{X2=j z>vhR^z{;SVY)G8K zJd~(%LN;zDiZg-v(?;6rp4y$43MLtnRjI!X?^QMe|A2P9f;VdDzx1zP8SMOoSx#=@q4pDCPN)S7c!z06DLzUfuQu;Nel35!9RActBA|)(H&h|$kJL|khP#=m znjWlRTaPpu#w%jrLLmI~*k2Bm4J%R-7%p?E-~&L>^3vOZYhrRQl%;CR1HWQOc*1un zyt33S2i6#jukJafB%IoKC{cCEs@TN1=Lb1dyc5V??O$#B($A6#9O>eC zxa2+{TLvq>zF3*Ps#4$|go0{WiRsqtr4)c>P<}kVMgws3YVoA&YHlq?=`8jxAkXCs&;CVKSENP56l zRll3q|kwaK4O4b zpj)FDO`PqAy(e$K?1Lx|8nal7>^^(^nh-Ab@F@2kA&pPyc&scV2sNN{V_?< zOP0QOryPr%*TW3>C*>D2NS>JKrh~*Lw78P4l(&^3THNtJIo&$>we}k|Z^MA=us)jC zftGB)8UO-Z@B9b30u-+w1~o4A(X8(yZW1ifi;%wX%)t(jdMgyn1^oPpZ1DZ8B8T_N zaD=3im0E_gL`r^wK4eJI8wX4o-{h>lN)7u>5JF#{auW0+8hAdN+dpn-5n|IPi2lBY z@Rt^}_79|(4onlC+vj6Nj{4O}jv5qM)>wadQYqYs0%1-AV!{U}4cHytgV`(>tY!4`Cb1Xgc4 z){}_w+gE|*DN@*((h?KLB`;+sX;J+b9Yn8IeC6VgvskfElYx=g?({%A0c8Bl=Y46g zbUVQPob{xU&NeXFC1)2*p0j|kTh;Vz{}j9ky4|EYT{FXb7u&h3u++5w?4&eb1%Rjl zqinZBDI5j?3Jqk`f3c?8R~il6olKla<}uctXO+_^mym|N@=^q=NK6sP4%oGt{cNG( zBOY_=T|ml2j?$<%NU=d?kK`C`Mb|JU-hcY-Rw>X)@CvWvLL|tm52!bvOy(omW`ry9 z-|J57kPmI+?o$Atys&H+;Pb3M?&a#T@+Zs2YM|*d2Y#9mT986D`%EH2evOGjCPHE8 zEyF@*^cevdPekl7;+ANbJqW?w z;_z@t#Dky2j%QN(XI7BmXhe5lkV*SD8~C_YpWRdPsuw(ZGPi!UNx7B@(hF|XNTiTc zo?OzH%W!qD%?Pe096S?@1u`nSDM0EZAiRGnkX+vX6l0t5p{4Farf>8wJ-a8{9Gmty zR}$GR0#=&cw>GS0wPs^ZYcki$? zd-L<-S3K#QXeTNvi<9C)IHeiuw{eW<-Pg&*C)+YwLXOMav0qTK89TP%?Y$=^NGppL zat*TQy6@XG8PpksVEf3#si_vW%&rQ9I8uVdYO@}rK28f%SOD5wH){FVtW{86KF8dg zTY8=9kMOiZ{I^~n`*mbt?# zU1?(-zKEX=ZULyTKO&8Mo#38k8kKV$q(_6!O(z;_a)8`s#^VMBrVkF2Kf6iQ^D-{R z@Hu|fKxK1M$9>FsSf=fIzH+|3-B#PO;)Ng=qF5=0DxEsCbZtJVG3dR$}-~&Io z@%gm;3xh3lrvBM-0(+x2PBLdezv!9*(!2&}>&+;aQ z62BidIthM0*G9UXxi-d#@#aCs*F)a2kRMGXFU6le$clEybW{zgIZ zi>=)flDAr|@4?*VX0coqX~pI)svS0X-y$m+!3wj=Q3b4o_$#jTJ_y%s)df@xZJnk$ z^xPa7Yt8F{nFHrH4Y9O>FWHU+m3);cN<{FCf&2|qKMQh{j@(HmiCK}2ulv9>+mw4{ zjbh=La>^7_jBXNhiR5qop3B1d-Tov17q>Qk3JDVB-DC;(JvLvl0EQhqB($}oy4sN) z)Cy)&>jDi`K2|^@IjUK?p0*IuLB4|&|24yfW^5yk=MQ@1$Hgwh*B2twyi!TnlyT!J znakEI;s!(g@yX@oXJS8=9mA@MR34uYB&G4QQVBoiod%kb_$6yt&<}iGTu_~5%se6K z6N&qpR)7rVRh&Jl4DhbDY&B3weO`DO`Ir!V;r>hSt)c}DwNDfcgW3hjowr4oaKb*| z8;d&aR$8g$V^}ouW&iOsvQF1IA)?ZP%6*HOVM?*yNe7Ya>h=V7ML!d48UDgXX$y>) z=2O(!zoWLQDY+5*>Ne&+=vsy}o!4HcU5#8jytB2&_Z-kos9g_>G<&FaYb z1STTfr;<5)HB@}Ctk>n96rKI`Ruj?lO))C0K|KZe$}HHO=4zJguW!%xklK`<$h*8Sd$mvs!Y>xA;Pg?+Qpq+!l6we$n49Xn%er9*o{b>t%4TYVZQe7^n zT81fv41Cmo6en^NYRCQ}Yx~tbSqnOiwk^ns(Yr$Gd$#Y&<@*RCwTr_9DrH1OD#L6| zSe<`fAys)K=R$F-z?HZDjL{nzxLB83|MLRO;8ZHYt*Vd;|LXrcIRQASc9@px_@`V} zdYU_QTDs9_dE>SZUvz}v15xm37D~}TDKoL+lhtDuC9f=d!8#FA$WSHrhNMs?B8Fpf9xQ#?Uw1%_Z=Kx|T z+h@Z7ykXN+KE)E?8Nrl293yUk9{R~aDSnj!YJpQO~dZL;O*6 zXkRCQ9^1>p_XiR!zi(&A9vlVCU$#6RH22H}i7Ck-pBphf@cy}mf1P0dGCW>CmybU_ zbz>H3iE)83WDa*~CPumF=e{&kXfdj}8&M0yKz}~nlo#;}R6`91Sj51Z>qBLa)LIfI zyhA~O&x4T4ox?Wy~E!{2)a)Y zDoQKvw$AO`8HgTwytIx7Aex_c@whH{Ku{V^o>l}tzhrb}ceQ0I)R@YYRFb!rTCk-M z*R(~dCf;#e0San`l3o(7?peixZ&Ungvo<-_OXhvGi`y$>f@r&UTjwG9!?ddx?WB5t zNO1M2_p)ER&_loYA6}sg^gJ)rE;~v#^ByBUxqzq<$)Smn@<|{N)U-?dw8HrLo|WVN zMkZY3PnWSM3hSWFpgKs5p>Nsrz~_0(-NA08E`)cSL=M`kgqJ7fw~y8$SQ9A`Cd`AO z+Pp!&Z4hCakmUvJ2t(0_)+=j&7}BaJolssm5!72uN; zW9=g){05W7q7Va>f$k0fAh=ggY1y;EQ7L2e!uh9O!LPA1pTfh31vY(Gn{e^FMV3xn z_Uicz+Z@~W@PT}**3UEK@#OI+c-d2)=UE@<^cqy+r_wR!ifa&Fzm_7Fg}Nu`=AHTy zPcfK}L*m9hl(DGZn?f-Nze_UQ!v`aR=1>{wqeONY!~=4vo=!!T^Ud0l^M!ax?CHi! zs$I??;^VZ{o;Gw}W=Bpn7^3=-_F8wRR@}M*B&`H&vek*AZ3wDrlc?crdeXDxg{CHK zsl=&_n0-@c=ezD5Z+TVZP3XTLw9_I%;y#oNYd|@*U31ACoB4#w7hdBu5G9lzd7t`zQFXn`BBcFbJf#of3t(K{E&7`#yql+miC@@#I+l z)jS=W!T}LZClj8gO#>yoARsA0lTtb*h2ly}$bLW6!z*xgN@<&6*;`C1dQB5o@FRGl zlDx*%$!tz^gA+2N#W&l^VT8;?)E#O?k5@2Yk+P}7%6zqh=FqA1u4D3te70_c= zr7LK1SY2C!GopfzIAh3fFoC*vvtV<4&kSqR$$?Mrp$w`0x!arX?W<-iblEZWMYpp} z53H8V<5gYPCZY&|297Gls0fn3Yv8wlQ0q-wnH(6)W+Mgpk_4jQEF9W6tc>kLaEU3VbBeeo z;$J!8g-)PM>oyw_JByuSEy-Ov&Xj2|vfV?H^pfYyDt8e%jMoy1fG#E9QMcMH_V!J$ z7FsyBcC^y{oc%vyVU%M+JBL_u4$oMV0+O7?An$u-EGOuew~!|2Jmfn{jvb{Cu#p-@uMH zQ&GD0WHd1%K+y=veJKgrd02m-*0llzC~5J$M&M2tFRit$W9#S5S{~#7(R7wkQMGRu ze^62Bl#-Abx`yuV9(s_F29a(-5TzM<=x!Lgr3C5jloE#S?s(7t!~0>rvS!xeocp@3 zz4ve9I~@z#==6ys zYQt7FkzpD`(X(U^*m47J>Qs}5^(5fQKn|WCn|2GU1Hn{QWiV?x#ngwwcH}<+KKZ-f zT_lm<@afNkoiW70At7nCczJHqfz`4a7SOQk$pj;IUB??-%&z|BWr`3aig`!0LXv+w zttt@o7wV;qD6>hmX6GYzuKj?$-UaXg523WB_4xGj05ROgihtb0jELbAaD4G~ve(5K z194QR`v3`fz%PjNW(hNzQrxV~`1H0m$^Wcp+F%kdQ`~>luRYJ$!qD(bRqwxyXiS9x zlhv|08fx?a)0mNrv;t(|&cOLP=2h0HZEOT2*DZxwajO<(iRY5gQY~|{ERhAfPns3z zY?QJ?nf(pcD0#tmy3w?~E&fa@e!mc{P~J;VR*A{nZ(xRh`Zl$`cZBgZHvm!>}CyzkI-9n(e|`>k|sylEvg;_>>gnhobiQ%DDrcS znC8*d(k#)f^W7QF)ZzaTftt0fFqp;bSnVm7uAgI@cGeH%e* z<02;u+bGBKzRBZ48i}1My8!Bf`HU=sj>rCgOK|%sZq+Dc^;*2T*yh=K_{>+?&uk$` zK@(7 zShVaHFASH&$FD_!6W^Sx*<*ce4TWF%Gq87V8#&lY#;3(h`ghKqy`8sPCzUU`@}}4L zdt8^ZdJ5pCOlp0+x-Zj9!u+OAB{%pGo;%FQQbK(7MtX|!!z+~b)`)*O{WJ+g4Uw~P z>$t5}_a_pcIP_TV5XN6BfMQGi_)jqDc&-siGKbZjtJ`%Pw4Dnl{&N$c8n-vv|KZz; zx1(yaiXwiGm1{r*Ug-H!yv+!(sDU_g7Jk6>x_a=2r#IE&q}F%4l`{`2d>G>DU691( zma)^UsGtN3Gf0%HRrS_Hm0%+|oqYx3tV{G;d%e$8F_g=B#L!<&5@w3{4S#Mk%e-e4 zCah%RKW+k`$t}x3x1ZO=4p<396Hh9KVqcU}xcjTMc>{PpH;+7kli{huPh3u}l1LDP zAqK^Yc&8b~TvKYiI#YeYa9TToZc7zO_u&FU*JMA(joDTu5^m^c0@n|T$K+LLb%GFj zar6nOuXC?kp-k5kp_S+k2u^-HU6%j(b5{6SInY_}hp z&a^usVZ7_#VZWYQNV=eqjwB zK_MJAjISQ`>hPu5-h(|?!*jlI+{;Shr=>T*p5Jr%J>3_b7zB0+)r)II4aiBGPX zziXIBG4S}Qis2Q?6>n*<#|*CY#u7!P>}ylH`Teov!T@JNlS=YHS$Et#r+2;1!_~#m zi4B7LZ++uqU-%ns$`{JMslhn~iNE3h%BWw^%m$Qka&-m;y!Nt`!hjl9ra{d(EI4(y zEUS>gflyGq;KNdEKxJ%$(Nu$7A=pKPwQR0*)z*=%LlUz2Hlkz{AL^daq3kTy|2H|J zjEVVFw{+<1JLm%l0hvwozUt=Goa5pbL-CcJ^iixsC$3kZ{I-gQ{G$*z+6dtuR{r;V zGevc3?f+z67BnQ1TSJ44dl46N(+H|80+;I3$#EEIr39aLKiz#4AMJcG z=z2Ne8^T)q(s+g42B{pzbjvo|h2!YCt;NRoj?if?q-iEiWxLSX>FT$^tKNX{GCko; z(H8Tz3P$p!RsS$P^tH)hR*`K~5J$zLYgsy>F7aeYa9$FX9!@P*Ij5_Rq4Z~7#&Hxf{yFP<&rVPC5`!X`gb z>mp}92Wf_zJe>g$fN%z?Zx@iQrnj$r8<(AHAMq-G-vrK8MneE4cuqevD$vN zNd0Q%FJU_}azyLVD6G3rWKOB5_$W6te9ZS-_g3-bCg#S`{~Rp-IMBY97rj!1Ak@*R zxp@(@?b_}yVK>y=hV;0{Q^nH=-Rrda>JzT{Nd;^Qvuq?XMQ;ch@fajFV)Z4u` zhYz;%4WCQB&9(6M@C?~HKmM0$Nm9OW=mD7W1aWN>{saVcHm5((u0R@9()(mWxo2%t zb+MzSrX{)b_0Dd3VxMoap13*My)H#HIXf1|zPs~Lh4X4fbzpgGS;G`!`fNKBa%D(^ zGeLpm8I1b{Zc{2m#QTPvmvfgu;1HH*_Nrt2`o??U6E6f`R4iQ}OT-jBUz5*_VHP`X zQ0?z)kt%8cEo0lLiu?~nny_@GW_TFg44-8xejT=NxHQc>yMSp`Hy$5xKtwoQZ`!!% z;^RYWym~UC^wAG3d9%~|BO+% zp202I4q|Xm#c*@U3(F@2U7uvc6;zOj1ngg5MlBt*S9g^B9`!5Nw*7C5TAzOkj{*(a zzmS^4?epRgb(Kekpa|BrYmDXz;14a3eFh+#ywBR!$;`H6CB@$wIi~}W(c`5(`LK}3T)`x7Nj5)nj!^#wmGl~|l z6%tbDEKD|lT%(+>h6uE^a`b^Ux_NSugo+|7ajB5&8w>R|)&NtIl-^SVI0XYlE+205 z7a;#Ivg%tW7p5TB=w_vk(I#B|pF!r_np=Uj2>57ib!-Vh_@UbMSu33$;nvlW4o%Rf zr(s?AyFmRyCJIg&&41E z9Q=EK;TUI5JE6H`wShBa|@jMAa5oZ0P}!0?fzg z)s@}{Q7Zon+1DlAg`UqwPho`b6hU#}hH(sTtxt}EBER1izOR)L+syr;VLPRhBNUZs z`s$<9vcilYiF6&BRacE6tZlbky2rDfY;a4=kapWmm)FK9dt4AA^`OEmPV?erywp#P zj5k)tyDil6Sm-Sh#bveH!4df8SV+detDIvXD(ih#ywN?DX_h(t`_P@9y~3v8dp?t) zvQfmK8M)lb(khY|0@gRL?&XXk*gjH`(q?uyQg*hC4SMb@WGsqvCU+$kMK5FVbm^q| zM|u4C4un;GbFgDVJ`sz}0O%&@?5lj*|WxS^Z=ZM3V$f_u5EUW25+zqq<+ zVaKS-8tKvKv9O>N(NOdynfR)GtgM351?K!K3Uf{kx}M56ro+vZ9B(iaN0W?gml z_aBR}XZrkB2AEkem~090%Bfjn`}%0ViRVre+0#&~U6@NyS#)`dPzsPo!7WvdlwFim zxVN8pi>6*0yb~w_;0q~L85LiGY~wPFr^<>;(cx~MFqV`gpq?Q127TM0Ou4RH;x=)! zCG#bn&`hcV`pGGW$+{FT^c*%1SrScGQ8XhXjy8et3uCISfnA%Ecw_V(lanX zM3#K7x)yHuUli*Q%w0Es^tIH*0S-_avHKdV2n4M$ibJn6b8VbPoqarix+gMe&HAVP zV5PA`qW(vJ6@%#yTliXnY4DM$n-KyuRhe9 z-7p4{F-AE)uS9nF1L{>oco6*Y5DgeNFf+Y1>qanE{7>=HMiOl)7pS5z&D9sniudtl z`%83Rmm9zcGbcOt$*7c*yn<#H}9 zITU{msaIn40GpMYlZ-Bji*RD%pB}gpNZZL9%|Ke!d-^riElp9xbjJgFl_dIUjdZoJog z#ZFFcf)N#pGRAFRdv1A7^QfMZ0f+z!lG90u;#W&ckKhm|c6tjz$RmmZAnnya9qCH` zIawsXPKiIGe?Gd3N=thWk)<59$%B@YRl5L%gJ+A;jf7Z?iKtIo#QtL@Pn%;mJ6+;= ztnb4+*Y3Hgy(Iqox$W8~B0IXBIb|Eohi13t_SGVjr8P@~*#+qBJZQ1I%Qz5}IStah zApH+fcOYA@%Nk)#B2>l%5$^HOt7?PHzv#Rft={w`KYqgACv-2B81TJoKhj`my?Cz5u~ zENd(AsRlN8qiWVpC-5vfC$A#4O85+D6hPa{(Dm{L!X-1P#g&CjlK$8T-uKjTj%36Kr8b5gH!!%09KJ2!R? zt6p!1y@E^r!Yck!8bc%1kDE91S(ZONrA>-KJ(b7rHz)a1)$cztaU=dp!{>nwv2Y$s z==e07T-w7AsWl};b1c>>D?v~_O)W)8*l((;Ss-P@6sN^xVMqTd+aMnrR6wN$+$+s6 zNfHz%!H)S}LcUL8dUPgBEpbkzdaTCgbxY?$=Nu9nWx4ckZw9zk36PvM%l5X5i8y{2 zv`ud$`EqKU37Ea1i8a~6^3JZbs&Kl=9@|vL(miJXas;{_VK*+FAhq0J4NXZHoF|o6 z74%P}K(5-H(GPUCb@+MbI*04-g0!IP;!2@4jXvW$g0QARij_&#_!WTfFnOG7&lSgd z*j4uf#AwsgFw?-aWeVidRP@XJkyO*u;VGFMkd!?cLvX8LQtl6tb{(CtLHf@M!3r`j zR~SZ%usNqyBU$8tUBM9f$@%ctZ?wZ$MjCUSWcuqJJQ-n%8wmSL*oajS8)n}Wtx4Nm zcrtyVtyyYO>%`TU*^gzl!{_}19D$q!d42PtY(xkHn)2k0{sYTwej?fc`MP_eky0EPG2Xuz_ zp9*@2bahavdEJmQtI@aDge2w*%U_t#Ss%lZ9H?q!;gxJ{mXg89)ei#Dl-S z-~Grd<1}Inug(l|;%7xJr)@LRz9>%@eL%flq=;NVfF$7Uq%k5p~YnH&ws@C-`gKq)3a;YjoXmC;vO+U;4x`8U4c;bzDs!Ho?u=#{?#R-s<76 zRfoUZS8ossLW&T~Wy*xIBh5{gsT9#Z>N-Mpb$4nW2I9~cC?kIhnF4GO}Bzl3( zsythoapL#;=Ev0Tt3iT9nMDUhaJcp}x(qSzM5$iof;gqVqd_`LauLC~i0K6;^V{P3 z;xiP3)VEA%-!CLc>asd4QQM-?(Kt7jg{)-o$%AVg)`*TjdUj#* z$M*zAp4}Xo%?>Wpj;(SdA;ZT1jR;_3BKx0CV=v{_sfh>t@)^kupdU;lQqrtrn*+SH z9l!luJ+B`ysRD;B91CW=lr`lnZ2G~x4JG-Idjiuh&@Z2bAv-wJU4BS+?<69<*Zm(F z$Gk5$Nfk-`>@psCmw&?k$9m#=s6yllvaqZJW{#eXu5$@U11ZG3n>43+YefKH<4m_02g| zSm2VS0IdU@Xs=c7d(yn1t~KJZQ#ZnBFR}EDF;OsR4^e3Qx#1)2n~vOUWXr|a#x00j ziiZ=M00_ax0b7AAaiixL%hPl<1|so^Yi@A(ozyy`&ZrqNshbK-n681PPqG{JmbcV< zw)di-0haxlD}yQxB?w`Z?Zj|jnCXrpBeON5VVAn!83Rz74O17pJzF%XR1&bBKlDf$ z$VM}j)zX`u!OH)UV`B4pK>+sCnuKVyC@iC#DLk?H?|3v20jKr9)bj@jknR4od}MlW zHFW<}e);-@ij$`r{7i&Tu*-^fGW26os*!&-cHBlblK}%|XOyy>t#sz@4%?f`<-$3) z3ZG!9fdxB+c8VuHcmD1D@3D^MBK!4Sw~e*6CkhS@#=XMPNyi0v?=Q|_K#&D~?A>qk zgwK13;bm7<6^wHc&Wkufs+t~qGz?3qrmo9}nb&^{E!&{y2S=nvfC>|kmPS#k^U_3g zOZ`+GCz5pmNT7&OdHc4?W3QMg^C)4EyP^Rt=Bdqt@3oWZ{V&Pi(}r{*Pv7J}Rl}!L`7J^G`23fI6&+3Z)^m)Ow|!%jDN$OwipFGoyIIb{mE-7j zaiXguKz}(C+8^Y%&eR_bu{*8asjdO)+4(`PHU)H|pOT@z7L6?h4$PFb%<6VNmlec8 z#Y!M4BV^g&>`<7^KezPVQ3Ah2sU+)xi4+hZ)+%rCQV+vr|9G@)I;D~ZgFRiaWE8n= z_s{lnj{CH(3Zn%+=AN%z`id!^3jGm)0a;84d^&-gPTE)5KGpy5+UF{S_L$?{tiV@b z_A-T;0ey%MEt^H+kNoNCFH1cbAhlB}v3c)N8W!z?(fSO%zIUy8iQ3q)oo1>|Srx$P zNa+e}Qd6m(Z>0Thei8!=g#L{5#$F>)Et-mhKQ81@j3(WgQ*2jC5gyQry;E#kQ|sWVH+Z zfAe4X2|b1wo==pG{hs1%eU%(qp0bPHe|z-wCPpuF&z4!x(glnj!6YvoS6XxSM_D80 z5s&M;qt<5Vz#!n+*a+7N&UGX{QhPfx5toa~yNR}oPe@E0%?d_x_az97trIoWn$ETk@#%x{C_N-yd#D53XbSpNGUg@f!6 z7>UUzDAd3g)C#kn8p|ZnjB+HXtON&F9CLwn2{FK-eIf;J%b3O;D%3Kq1{D-8r;_2G z?FeR+Si!zsH?z#`(l@LHfz^|nFI80n7S!ii=SuBPrrK)+ce75qh=P1M{Gd1Mn@Qwq zCg3sdS;gR+<-6b!k4O_MAXH)&Gh(kZGp#DE#m|V<%Uq_llh2f1#WSdsJ4UH~?p{ht zo33ue9xPg(PqvFVy7obVFpV6JaP#d`l+Iu=+0au}6=pkpOtc&wZ-hs{QNfCSxA?Ww z#Dc%Dx+Ik18Z>l3T31uXxx)LO6NpV!=Pva>0va7{zGH91)QO(s$}w-|Q#yR(yzT2{ zF!1jE*G1JHjg|?QGmBX|LI4i?#2Us4xrlCIMbl6fM8q|hm(4% z4ZWi&L=Wv5QN`%u8M>2sq zDB*EGifZ%edOU+Vg#V^fY}sX+pZ-{0~f?#lW1@i+23-oJaY3)`wl6x13!z zIHd9&DSJ>=SuR?DGk$%9TZmg>Gyrefa1yCv&GgRPY1R@GK$SY_{QNfa)XOY&tn4q= z${U!6fSzU|R@VokW9zZZY6Jc=6u9s2O0?<~Qgp~G)v10?H~ixi;c~D-k<)-jvt?&2$n z@^bnuDuh}BHjwkR5GrHa{iH6XwI%mME`#sQrg)+CQraBnA~`zw2ntT*x4s$`U@fJ! zTD>R%TaO@4Q~mSVGWHvh&EW^K^z!0CGG;CbJtX?=?etw&~DHV!$C>5y_cHv3%Qc%fmIMj@J?fkFQ3sjQ?#u z+v5<=QWM0+PcH?_(fykh0%W1`x%6hyzH=atR@1DAUl(lpX{P|!rX1DCw2slU5X>1# zpsX?T@kQdWQffr*FGh5P68ie~ zx#GMkW2U$dJ0wSkZKTNCg2X~yclKS{Na|t>a3)4VioR6eM$InnKgnM>$o(nR!)f)s zeP{4^Y{o=RUpLF+u51Jx^}!=@sm{c+1Agse)%%oo7GiKI%c#;zn$pXa_}r5zSmI-h zYg976DfA?wqdFv~PaQ3$S8b}HLzeW?huy{-`56;XJAn4&Yp~h@sj`0Y;CkvWT30u< zo?<1zItEk5gnCY?GNN~LG4H*Om*!S)9{Wix+x}`a1~)@*d)vKtKoU>SAl>dm0|uF)=p|JKl@Dkxr$>ux676k=_w=4D6Q4{%#Sh&fr+QN0>x;WR9iqda~Zp7w6PbJO^ z$IPH@`F+e0rGHk@j=w)$$-{Mjby|37jCj-*u%Zi&N8yLbzo2@9G9CG{a@AK>ClMZG z-m>t&n|T%`V8zA`e0##;tkf(X6_*l4nj2d{rvBnd zb>1t4qw1Cy8?tFGS8Ok3ITsMz?tGQj#Hb|>+r_tPVn@Ayq&1pzb#?8ts_!C+zzOi% zVLA*K>cZPhiDlpNQV8YaOhilaLjIjIzW7+0IvP+`HHj_*hLmfloV8REc+Uf9F{*3u5P9`od3c|&HkS{)c$SW;{k z@V6k44y)(pXzU@gHBHHm?|lj~sF9#X{wl7FuAzV=F#Fr-%*$z=uG`5VW(=4!3>gSc zY-R?_p%YEKM2#`XY+9ezE_!zIElFQMNZ*@>x^b4QDPqhi0RWCExP8;t((-Z7ARLpJ>Rz}Y z`hmiPf6aT96UGfD(>>%?^&@7K%e-L?11R#kFWlf246aGC-f})rXoJ1@K1I z_-g(gxXw8>e)5LOlxyg`U`Z=t2!$HW!F7W8%np~z_-*nycIIm5c>hN=I zyBB_+B>D;3xm=6I-`MhiO*fd^OavRO$z`=u+ZNr{>N|DHX9n`$X-k6T#ign67aS`- z0TaPMFmCN(1BWVcsp<7FXczS7Yg<N8Tq8+W<8cs;Us55s2Z`q`rt=OoG73OeE6y7YsfGS#&=Db5BYMPTB?OrZ&I z=VO|D^Eq>=Q{X*VmBv$?9H(qfnBBL^JN5Kj%7~veV(i4FdY|zd1aB~>#kkycDSQ55 z9iQYrAO<<2g`Xg|BZMETv-q!U6fJ+Zylw7SxVrrtuHXIG_zlzPS#9OKxJRqkv-|}z z^ASl|@M2Wrqn@@7R#X38U;pENpP@ud=wxT&FjD2@1-siQga|}o9gfH^Noy6)(qKg7 zetH|=of_&4Shpcb0Qr&aRi8s!z{TBl>#VQkEd@)uzE)@$q02cBM-oy%X$lOg{^hN% z#V_K2j7rVwC5|iAO~&M_K6MfK8zpEEAvh-R72d#=WspWY3bbnY5QZD$ z?j`m#E#W2wf6A72yHmJU4gEJul%l5*|HCSUn3C!k)c#R1E_jyN z^Pm9X8;}`#mXmLf6`%E%DL1lzq`X?-WB-;5nxMa4w(*KjR9{~IBJuCX#Vg$+kqz_; z6+yBDt_eOCCU&EvP9nyP`7&Jrz47bGWp*P)9XJV6fKojUGd6#p?MeFEgSFTQngw`a4jnIG{EyYr*e;87e z5z?QlorrpfC}7WW6_IPc9mXflc@mbM;+t0O5_wI;k>C(S!=9^@B6mvvzTBoX6~qcu zHwAZ{W+F}1F(+-?Vglc#f|UPOsQ&(vLedwOoXz@CA@SU{drt;hQM~vf4>LbdH)^tc z+@5W@*U~h#b>2<$Q{zXB(!i93YcCFMYjJfO^I|&`S|Ju4r*hW6{Q)f{vr9`W;|**E zRP99Ow-h3H#0>fkHj&aBwdyM;Jwx&i$70zP_`PrYr%86{2-yu&=)rPU9MjK;GRAZ_ zde}^p4Q*=zuBcz8U=VkU>Ze8{Q@1daQ4vaG?okRQ1_YwF=|!AEYdN0HVk3m#sPI)D zVQE873Cr#1Om~fnJ_VCfkH_427DKb&7*E#eP?+go*CT8hnqc1jtI%2e48Ewx8T>1j zgrMVrA%siy$DQNY9*z__M`-6szJkQc2%(nl%`S^8LvTN*iXMBAMPQ5F7rIa(Cz8G; z)$Ok>Wx=39l$Gx8{kcl-$2(pszZ>e=-m=h$+gt8@I@B)(?}D6Jl^du~nPOC?en>Sp z29=iV%l%gaMdchJD7^}PT#L;)Rz&$hwCeCS+RdmhW-T#ogzRyV?77KV!g#xAf%`sm z0_45`@cJT5NbV(+2n4Jd6H|TH<-v(4RN|(h^B{naFzom>>4YeTBS`)#TEr{ZGmko6&51xONz15Yb6p z0PZ>@2NTz)$D8!_i)bgu0Ya4D$y4XZ2$t^q-l`43=>6KqWgPd%QK|p*Y*u{K|N9wK z1?%D4@FCQICn6A}cc-6)xq3AdE-#^g)|>w;JI<_e*{n8kX^dMLQ++AE86TvG$;ft4 z_CEmon0H1(TMXgHjoZ{hH2WXJ_S>j=7<=u3{57*=(re5QaH3bm(K`a^4zLLGU<*^QqnHSF_2~KHF{1H;0>WoQ#^+xRJ##|wh zs`VMgU5lG)bdF>XuslQN_sI(r{QAOyeb(ZlFq4~naxY#%4_^t~NmCQwDwHq0p`<6= zyPUn)Q*8g3FLN4WPVKc|s+g_jbnD$&K>OpS_(^=$?<_4%)}hu7X#~ydgjFVQK94kE zH3n0EQp=K3x#{{@CFe1pvg)M+hnj+3B?1o6pyt6#Rh>NTPpR+HE-4l-UBh#G#~fv- z4H7Yt4?gWwNy55BvTC39^~*t-ygeOVT`%wZMA0DlA-Kr^Vii5Ja?Xr{euF0=*!e@{ zoAynI_WGo>Lcp)It0sExD`$;+xa-^L6J49=-ZD7LI=0@$>)s{AcKR2*^>(&<*Jd^sCk&$P5BW_Nb7b@fO8w$Tgt*^}t5g747(*14RicJIhRlfxLN z7lXF_A>H}-VAO#(y1RezTHJJCc$G6jCwCssl20GSNJ{9IUGtn~HHe}X*kuSb?&4FqWoR`F`O0pcH|F|F4l&~{t7U>X)SQqnF@&~x z_3ouBQRB1agwPrzby!xq1jid#1}BXyOU@5SiS8d@4|YSKt@E>x4Qocw(1vgs-dAUckd=6>RZx9Y z+I3d#q9`rvxF;zZk~wecH7XdsLu-Zc(4~7Hkcw_Y}5I)8u8*5bePx4660rh z9r4zV#aqzG?}cynF9ctHy=2L{KiqF0o@qZ?cXNRD(w!EMWiHn&+J}KTZ(~hGvbzFn zL6)Z8=QFfZ0+Z454=!e54A_10gUz|DeB@&$TxwBzmExODzN(?;TF!fO9*Exz(r5zZ ztV5+ZcNEUvKS+KSzt4?zppHw$H8+FO2doi+nCN?*3EfS~gANIRKWb(CQ=if-CbvFZ zJuzcU{5jqKVT0glo#W|84Xb`;l5w*TySMs&B=?hiSsWCfCk^^`3% zYqm7|uGR~fdD0A7a76td5>+WU(O8zab-jyQ#&fwWdxjtja(?RiP2#w@p`<1)avH26 zpU7z%36crYWG`}?x2raOpP>^ig=C1u$W|9ePG($1=xgm^YKvuz!pz4wufEffN&vLQ zEl;TL%AD_c)m7-tqW{zRbL31jb$3*L2MMTph_d5T9WBUH$5CmHeN&;0;QHERq5+G_ z_lDZD(G&CM_L^U`x|VK9o26-}BB56OOZ<>7bG3la;6Ypfq-{+kZe5zoNZ;q&ZMo|X zeN`48<*PlpYvedvrpp4U4cyF9|3arMy01EPWc3qlaPyij!2OEAa<_Pok448KYlj`z zNY>#KWAJa<*BngN{eC?@)wXK2|0$)(O+u{1EfXTFQsn?2g6!(`EjV%F_;Wv<`I%OOiN9bOFc`O9iJJUF6-1w_EQ0P9uGU<=mAiu?z=M!(WPpDhHr4OY*aaO*PIIQ`PN$=0vCFj6h7nu^5LqQEuk^9*hxorY37uK6btIy`GA ze%H>aagMuly`KMSLf@*#!vqoO#tDZX!|$V_5tn|r@Vj0TctA1_b@9RT=tLA4zqVY; zc6ZBA2$#5|aCUu6O9rM(>Y|scP^T*Uu@P1j)b%JX;;!(E^XB=#xeA>XTs4(*SjNkc z1NS*4BzMf`dgFi!g#mL`eB#1E6@f(b{H&KlG?T@)2fzGXfw+1m{NL)+V!K7{!rEi^ zt=njg2#$@)ROqYUZuxF#ZI4G4daTsgcC6s=#wGnOgbm1iK0)fD!RbiIH@Kd$AZ)_A zBkH%`7;$^4m(d)o3ucz%v^D5@vo`~zr1A*iFkEXbrEc5~5_qhtG!54nE!ewI(YO1) zOEi$z#P_O$!|xQQ_3Pz3>b4fKM>}$*fD!FWOK4T|dfPU~&F1PePN8#tSx=^x9xt;{ zDA_)FsZNA$lx?+o`<(t@4Q@6<^GoA<6%Oc#x!4VnrL18-i^jq(!>@)*6np3d5|}PqmFpKaVK;OTzGeFWCjf zWZk?JvF;_rr7A6!PfYh>lq|J@(EYpnO)$0DbO)!s;qB8^ZL%2+ zQ*xoKHr_;@I~|yNgkox%fGFTmM<*!%_1h25Z7TrxJBhm6EFSOzmSg$h1@{E?WV?{> z)F^1z%(HBaGyiKxvBLrr0dMHrrHM;|qH{@f=G7_zsJ8!4 zoh{H^&$L|M_AV!cE*(a=IbNdrBmZqgBAj7nobNU!UPRF2^?yKBr*gR4V+tG}084zC z9Ya%IcN1Cf>JU*1*Mks>99dIRQB{xDoKfac*vP8HI;T7R!R!1niP6h6d^5(tqbZ@& zwY-dFfZ^FK7_c9NTlLqp{Dcqa4qd3-R+$PM`GTDwW2-c&tt0(5({_&w_Z&ac-i1sy z;~ElRJL*Aqn7kFn@3=CK13{Nbs+SJyEO5w2)8;2Ccb3hArQOHWUUZ)LAX}wj^W6Q{zi-yxF|9@)dc?1;mC5LfJaXbnznTwS!eP7 zu4&tK>#5z4Uz*b$OdH?SvROax8Kgpy0#g;bDJWZS|I~_$aA7ZN;SCTScz|RfRjbF| zw`Ul}ZFlJ-4Fx59H}&1eYEv?rk{$ihG1e#7zm}3a^ch*CrwGgD*et1C_2%5;yj)#r zod?cY@6Gd*KlsLmRfrF@{*pLBJturpa8+$KkO12vwdcr25)f1I&iNBAF^T zVe4|AKYh&1yP_0`( zQ{IRKea=i}V0^3tax}6S9=v)Y@?b~qUnl#TRP);${e5OVBN4vjx`K1o(sWk^y6l*kwhIM~qFL(m<bO;noy7uixlkENpmt|?AQ$>iygh!b>Q~Df>P)VOQG|+uaJ|7NG{>< zl)~byN_-BmnVqlT3;fl?0V>r2II&h?w0^0{y|Mq==q4G%LW~;umku(kSC(+&3(E}( znQ8(scWIInblW-mmSHJbD``yv{U915DZ7Om% z*7gc>NLYb*zXF}{5f3w zcE|eRUa65$o{RNj6NvZ=G5oCTZ}|4us(Grl z*!=}J0AR^D_7op$61Lu#`endlrZVTc32;9&QT+Y<8Rf6!)K3z~B;y!{d{7TtcW`zKa2-eq}06#V>*Gn|wX zl|re=RsvF@`OY;*p5~W!TLo;efBc6UE}aM;#mFe{JHXYYb;t=Yskra;?+J-`_o!`v+ibvUyeqeAU3IZ8&W83k7ZC*VfH` zpIl;b<%E{a4l*YM2VFqd)3?K?ZoNcows_;^JdMxX##z)q>ce|-i4X56AU@h^nqS%Oc+rjnxwZGuWQ>lDZ-^`-uM(2)3-3 zQTd#5c}kXMkxVDM=-olWL;r)}JD-K@AFGxgy1CgM^QLakRl(K!w)ZG96=*2Ow8uaP z=<3sPJMxm@`8~8ExtFq>N0%jzr%5qN^0h9tS8lY*PukaAJZgrc#4yx?Ag0KPxC8u` zT_3DM)Ya7iKU{&}7(U~!9)KDfCVp7RdL;Ef=YJmN@V^BucU(sa+&AzWzZw)exmD+z z){$FO+OX0|q)QRWS{A9=y?dg4sUZ75Qo3EQWUkMu7$5l+4fk_WNbGqF=!a&o2#Z1- zTEV-BuK~KLfW-`$Y5~ex>rBe>@DFEXXt}a>BA03xIhdv6^CAzdOXeb0WJI5x+4R(s z$<|Da7b@I_sdQzqbvmw4{W9^s;m=CEc~8bVx{pgmjA{xv(@Y zupk|h(nurSt#mgCOGwxI`OZ7P8UJ>~S@ukA7tlsA03iB|>q+jfo1E_xD<%;4Bx0bGc>gi>H>Dhpu3Ra8>F+NstjbBGR!n_H zrO6vPHpp(M=$MCo=~uNYlA%peGl@XVwKVVml8wJxpvuWoR z0^M}STOZ=HUK7L;$*+G#Vq0X3nA=L!WuWcQEcb}estPfq?qd$enY*abUV@{Pa3|Gj z@$S2~0KuCMu_I91+xcB;Kz$Lwtu9xH@4)PiPZzo`rB1@222LYQ(kA7Ik~yjPAe==t zT68Ts(sy<{QGdsgLR>%Q(y;Zri{s9B)AyB2@~vojKCgR1J&kXW|F++wV6?vy(k9g#6Qq-Qv|Wz5o#v! zr!0l)^p@svkLQ8kk9m($vj3+Cb?eH_;ejt?sdDS zk1U+7Q+w5VCg7~fCzeXM5#~k>B_4X;nvW7CxMj>X5$=DS)cW$83{bQGKQkPrB@0-tb{P_-Sp zn;Sd{m*m4EZm0TUMTW^69+)i}^s z@MG3EPEie+*l?Oej-u365x8Uv>hAc_xfz%1p+W@J4U`deltu`va!Yp_^?=;NQ7}w) z?}M-w$pO+s_u^k`Xd+Ql1^$u~K`X9}0#D#cDa>|4ghKk-GT|21o`8JzPd9Jmq9wIF za(ziTRlTO92@EA0H4m0e7#aLX0t8$`v)Q7M0_;xgn4R*fjv)CEALBqw$u{hSIZg1! z!&-38rVnR0d3H!<$Kp&Er4E(Q>0NQi&PkvWRA3mcBX`Tg3TYs{@?pgUXu+WfoI>17< z97k#dO^l4?A1=ehSae41Vp&9`imWkV_H0d#pWEH!FhI+EWfT%jOt^__gw>N$?SCM^ zFa~s%>?6)S({jY!-yQSgmB-r^H{r}64!!Z$pP=)2bma-3h9u@A1R(L-cydaaTf(S4EWb}Z^!H2N%@#flzXC+sA!9y-$qQaC8PhI*5XU-maF0f_}WiBubov8qX50zxl4a87RrCPQ$?cz4TSOJpu|% z3=C)PLVn6UppyLy)fv)kztTDK{&98CelI1G6>GU|bJ*h7`}W199G3)|)W;QmJjV3u zZqs7o8l~`wMAtPFdUOTLFb&0D_}2YrCf^+=ay)-JY%ygSM->pUM`;fiRVchmEX?0f zj$A*nfD4w2224{I zl@~lWjl#J3t82mX5tnFPyY}<^kl4TVOj3_YStoNlStkac?i+rt-5Q9`$kSQjvHLn% z`rhCC_itcJKSZAOj3G4v$*=g2;!;6ljvJ%CWpAv}6g%Q{Eq#pAEo8mO@oC^%;E`0W z%jo*S71{?&sda4?c!a@!15b;;U)`8$EfV-c=XmIu#>n)@dZ?YAE0V<+*a zJOqUj2suzEPby?;IrRbsV#NaU22j*~6M!eSo&1`S=IFAfo)URsLZpnU{!kbgqPdqVdyAMSYc`Co&w{AxB zNN%jH#R6F!V>m_g`0|tLrV58rnqQ+kwjm--$^2Bw7stika zV7?J&pqy%`tR*h7SUkAX38Eq%u>T%L__TmbMahyE@R7@pYShe3|Iil%REo7U5c$!a zvvLNr>Hj{Z=_9dKGek_XuQhdQ|L7KU8`?UkWZL96b?(Md+S-EEpK%K5FwNCcW(VCZDUSS1gP%$q*=+|)GW&zjb$K%T%Y*9R{(aKHAI|o89 zTACzk_yFd?M31F{7J}1&Og&)Tmo(m@)odv|f>S&oqTjGm{b@U| z=ibeJMi@>yg1TQj`3Id*5=2ht9Q1Q2Pm%_lD@%_Q#s(|rSFkS%EaNRV8+ltL+ZxTF zllF-i^A9~QY1$4H1<2sv(&(Betf4;aoV)!%1-K9x`tee^2>ghuSc-WJB%IRj9R&o z*q)}UrUqp*RDYe<(OF)pSc8 zu?n|HT;VqiBu)BG@y!1EmF@eaD1(T<#O0);0Z}dQ8p{T1zSK*&iMC1WYO7IAXdTsy zwV20~<46i?^pU80q}5f=6sNN&Mo%I+$CQc192YNZg0ruD|H);ussvrsWF7zJ(Y!k} zZ|{qca{A1mnDg_80Y?<5k7C5XYGYCyR+XLdbE>&n(~!IVUkoX&*SC;U&wE%sIam86 zQXqjXL7o*ssvwid9(9Rz6O^I8&Z$1-A!=$%blf#*d=Uc7N(o*@ivc#}qAu!EgUUIT zlJ^@+U9aW!N2fhcR7*<=lYs>XjJEqQ`hgsCU zXC1Oeq~Aqa{WN0692a0T|9BlM^>~&AB(R5m6XfxeVss}H5$g}p^qSB|{ow;1GeZEd z`TLMcDpSq%f^cnq8fjT{SYLDh!*Oh(M5VEwVX6FW%!a3A^>@@8QKF2EhX;+?M~xMD zHgGJppJkXFI+sC!Eky?qaLtE)NvGDUOJ&mGfZ{-h{7S4MwKg)c&c}!s-jWJ>^<;!O zhaHtjTB_aAk`X^oQ027cLVcB>&lGuuQU#Hi@@S)l**5#ptCGXz=;b3OI$dxXuq!J- zrZZ;{f%$WZ=U%EZuA>mV4J%8@`#vA#+}rq-o|K&R|G+yPpU~DO%*d#v0eHwsmzHOJFgzO zDCv=zaIai|GJo7EJt}}hXiTNzw>z7TEpqn=SZ(NSLs|L4r{>(6{=yjevbVz_Oe58E zDA{Ey&;$SlOH?S9DZG}9sc$2ZVKABH5mys4x7k}Io?${WXM$vXxQTm1{ z90D|3EUVr%8n2u1DgApXd#(Qz#cVmgJNM)pQBT^67pue&ULKPrJw+s9%YEp6$x)gg ze$RQ`N?`ogB<0%tOhgOwX7udeRrl1?nfT@aYjj4F@se7T9-?}V6fDG|PMoNl>#$Jq z%O3FHbZACdV6&20R5X7{*V66+{9xoa-diSkwzvGXv)W8P_XoJdR#j!QD390(v|nO(q!a-s zE>*qNW@O1WURJJ5ieQS~U8-m_Yr+Mm`n!KcQa$tS-{Mvt!zDHrW`q@laOJd;fqg0F z&Sie_XY38(MQNRK0ihRjsdb1ppj1wXcOU7aw76-HL~Kv4RtSuu{9a_(#isvrLOnm_ zY_Iw;b8wLT9js2=R8io#;HOVS;SWbNLCTtzcqRMu*6uZ z4(9T&{@0<|c17|hS`4nzW8(M<+pMt9_8;eaj^z5=06&U|gbK$G%(^N{87N+%%DF$; zirnO|k1s?M<<0!af0}T6NLHxTZMuF+4I$_c-(yzN141Ifgi$4|4rAnLU_VzYdR2EJ z4mbXWL0~_eekghO#3B@q1(@L^ZsulEnpw$ zW{wA^4y-stxO$b1 zNOP5bizr4PIahtBH0dp%{3(FN?L5@%1B9((K`5wBNr^2meFpBF$JL+4x=LR1BE_*FF@7r5duoo+y+F@I>b~YweP@xiD zrQ{ee6q0hjinA6@=MOXdJ`c>P&@dzbV;EV<} zCWd00F70CdI0+V4pa(fGZj#OH#wpFjKe zf@|ioQeU%^o?FT!yhRo&%yJE=Z@Vs`>WJTJ~@Mqij3HtrcfZ3nTLB}1sh(cl>P^NS^UDwl zulK-f%<#wSfA`7=pzO|eiYzhC`Pu`<`D6QxSDs6&(0NE8X1zFZ_U$Vj?59?OusZ1u ztP?E?%V0r4qiW_lvU^vH4B(fe#_%P4$_NY!v7k_5kXtkqN=${|{nu<;JK1xD!}8M# z%yg%t!%`gys_7h))gX36B0+tbC(Y%APtc?jI3)c(_8k~p>gojxvV9}>c3kkZhAJLw z37pzNx{@61vmFEO@+aPBSAd56Wf0q)M5T^j2q_9ql9ViF%TSdb60bgj)j1H+G#f-B z)S^(`CziVKvttgP=Vev3&O??7Nn;^^rFT~}q0a^y1{n+^75QHlKrvrgL%LSgL)yXO zM_4w@jHK39>jSI1!|BIkVY)ZQiL;TxIdmd_!-~Po$#hvFbFyPQm?)bSslQna@1t@7 zis;ji5id{zifof5`DW9k*YfbOj4>tJl|#t<8*9OY{s?RT1W4n+RGoxS-6FP7O|Y$z z9=w9dcPy9N?`L}+JF&*dKeT1W_BKmW*DeKLzxO9PDz*k0np~q8gArs#iR&#a;gN#j zyNvAY#7W}~Xb#9Kv!kmsse8?qkC#k5w=9c5*$cx6!AC&CbBks?PzQBlcjwDfPJ{>T zPNakju<Jmi_?nSNd>Mn&OzT5kYZk{RE{3lzPoD*Pl_ zLE?4ifXVUIT*vsNSLQa~>CquBgq?OJOE?o#HojPOr9L^VuuO+LW}GqxvAq-!mv!DK zwdp~g@T{D!H;p!Zbo3#;F++d)JP|ZjfuYuQ{Jphg-XP&wviMr;bqGcg!pc|yP{BL=p<=!Z-nx~ z`8w&h_fJ9GcHu6>=IB4w^<2_F-c?>Za)qr1weOsqz{FK|$U{NCjXNPTn|>ejUa$xO zp}y_ZKm1+v($YX}!zOkz_Py3fRTWKKTwGa-Sc$(vqBSQ%6%GNGNn3%v1PPpPwMhks zY)1TbbP$vgdGc@XFEYngP{#6`lDJQUPNIG8q#h4>{O-dFO7NE@NLqT!_^QRNUQy}d zfz!r)tf)mYGey2>pzmC7@m#98Sl5-!x}=28=KTvz$9yA`im&e>3hYw?V(p?Q6Y85k ztL#P~zWu)Y7Z=o7s!8j6`*6}ARY*zd@B6c;g8jd28)LTX8*TUuM^rB9YLCQKXWx%7h{tzs;P zFj#!dXqY5F2gL+>WlR15Cu$9Bqr?N`d}>L;7ekUxMiq$H9oeXELZ*?wYE+0?7Dquy zED_^vR|3(Ri%*}sR?=)}*4-vxV zFYy^-vq4FYCpk)$%hD?fg^>6{Hia}DU7XULq;CGgaJQ(RE3%FcA7@?*&L||D9#`hD zk`y{*qnN{&AsUvthCJ`1K)}agcnSc?wBJUFa{z)WqaQQ!A6#O5k*&=uoA+fjk6fOHgyT0aep1~H6rZp<#_Hq>x3K}jt5 z?Ry0On6hy>I2Z%AIR&k=<74E~k>JPI6M}Yu)W>e!&9GH`bw=BQL_CrY@p!`%6C}0H zeb<42#54D6=Wz8jA>)(XTNhda^uW~CjzU-)$yo=8=Z*cBtZiog98TLxFnYKfh=@() zsb_|d{0_z)ywe}l;oWRoV>;!1diR*BPx}G_mY@mH=%d}iV@(M1HjSX_G0$e_C=nU> zJ1IaN$HJXLV5L(C{)NSQiX2gyq#3O40Ay%9Dm!$!(-ofg8lvqXKgTKb<>9PeQt8+y& z&?ku09Z_Ewy!_YAk90fs{Q)iGmHe^nH8a0&;D$6xf6QApdUPxUSkrA)6~7V;-lu8% zS9EL~KK2Q}?$hYNpu79K@`QvhOg&NFtd=t53&nF89Dt{(0Qjl{16y(@BCgt&-^Qbd6 zokGWt+fcqieruk@MY5=OkM7&3EvfRUJT__A_$Rt7TFPGj=$lMV z(<(#m*+YHZ-?+fvOQ*>VAs0LIEOKCcJbb)&l)Nrp<9XQF`L{&%uEL1@y$6JkT^rO-<7uCl|PT62rEmsVap1-L5{eD2m$m<2W0qJaY4zH2nOr0 zq~I$n555A*z8SRzJgY6oo1Bf46Rz7XMoD;fzVz*3r{LqG@yr*+L7IZEZxr|grUvAQ zmc*++I556IOzoz^1tN^uY7vEQ)Ol*@iOQKn4~2C$uV?^phdM#|`AER_`*^0m;nzAJ zH>L&E<*+ScX$a^xStD&{4MXU56noU^{N@9*m$&_5C#q3V%neF-WF!?;zo!I-)$-t5 z?-drj8z}0k+1ZAWJZ)DQhqnAmuOGGx>XgIH%((w%VZn_nzniU5t5wJ($oY3b08cW*|f^%rd3ycqq<<+u$=KXnU*@qOWo> zolfzu`79fwfXQpEA zvss^hx9~*$vP8hjoEr@jY{CI6U~eu^+?^zpP{Ab`8y+(#>cMBBYUtanwE*<(l zK1fs8hFsBMET+7UB&=MCyPM)^+unIx(~-Qj9I`Y@u%Z0OVY_C8B>948v$)xqK~>#C zDg=YTt>`h;5oD}n!rw;E+Ca8t#6BhRQGim?H%c~02~l9}@THz@VF`KYG9-0(Px6QP z=Ysmb`&cO`Y4p~Yo@!=wu0dN05{jZsH;o}^qFP6FP=*l}@zb*8mjlU@-Mpu#cb#WL zxLqHht8lGU;QM*K6@0G%QtY{=7M6X(%H=j67l(QZL8}T-eq zeqIrj9#Yir*rN0AoAR@vK-d|P6yIdiwd~`d!yUv>@6oaRzcahBqKmCSDoVcj3vP#+ zfu{`Iy>?>#J8k|OJ((r(r)7}u6ni&j?&;;Z75t}a&-r#X9W!v{Uu#lCsF9DXSzjtE-53yq&A93hia3Y5os;kz?;2(TZxm4gV?tA zm+4ZRS!*RvY5Ni?K0x0_x@CCG(?GSWjh(At1r9s{rd_*=fX=%XO^~+{wz1d2D?pJ4 zof1|h&-E4SaI*g=bLLnsQ~WQS_^qRzse!xPlvSj#kJklRlOvLeG|WD;NNbWzdKlTZ z$98#3AZp^cc;II3prVI(HjAESe{*o2ZXCXsR?jmI=>p@hc8zmr?0n{nN&R9Y@&~78 zb$_w~&Rj!jgnV%-9htB_=`KRaZKrumSM_y!Y%@^4l%8Z9LiDAGj+_5b)PkCkz3#Pl z$e(H~lk;vPL=LWk7AZyEZWHd#nNj8=IL}ZQ3{`| zvb-5)6F_pR3e%V$=kyplJbyUJqB3i5%L1^)rK+c8%n;S6J3f8}QU#-TYQ6VMXr;gX zUCi)&eBMThZ;0O)s>t2XU#!LBT-nxGv?lfzJHMT;&pGHOEI?ruLUM_w+69K*h=v(v zd%Y%?zywjp*^L%$qv38}?#sX%q@KG_p7tsy&u=onndID7h$RGtdANYCKKlfVbHL=8 zUn}SBGFwjP!$`w=?U^M%s~aJ7^aL5OO&d`GCq36!X1Df&HK8sUp*Y^>FX*GB-e8;Y zq3RTi37JEBBiAet1;kq3jf;jv46Go_XK*C8mgPY01Q^yGcX(9aIr}||QXXo=ol^Ga z;QA)wy6$?3T%6beiF43L+{pB}koKVE&^!xXMpBjSm{ms7vG$Sp!%g?5vzZkJsyE@mO6ohWBay>=LgX*?N(LQV_o8K92C5wd*Y!p4&@%lKwN&fhxjXjhyi- z5z(7@>$na2Wj()qdWQXeQUT{n`58JKi`)ZrRu%0bp%uQ^Rtjazq4#c@k(n04d~gZB z%$V2nYt9`1+AZLoYFV(}WgUJIip}XJosgBA_P3YP;L9yU>g!EXWhs zWB4)|doHvwHHI?c6i^T7j(G99iBeo!ZL0qolzma%mdC16QVr_PocLyc8(uHwCfWcW zg{YC7U&ANZl2WG29rgh;@~R+Y8?FPeD^_kud!8Kjcu4wlA7pO3Te71q#IQi!iPIsT z)sG9^qlvgaw@Kt|Lkc4sw?24F7(3KGfAPVe>oV1K%Trykc%T+4IkKVjW$6`k^n+qn zHFN#^7ZY_4Yj1xM+F4Pr0ZFQ%6!W&7q_KL=6ic%b3I`rEQx4A6QqP_UX*1HzMtS~- zcw$)%>~OdJ-T4Fu*V*o2<}C}2U{Mc;^f_Sc1EYzk097#F4Q`C zaPFIGzQ_w6_i6EM-%7aP#`e%)$~WRGBE!+Kdxg1Qf{bC|ni7uRqV&?!(kF|!™ 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

UCoK{;^>hM3(1h`d$I!Cz$A)x)qjZU?nvYGXVzc zludq7lPePB{*KbQH|9FZI-0;E+FS-g;p})xSPdHkXVs8S)H2-1wUTt+D{!nkJYtej z#$PGs0n|lD;96{UXn6n9IOrF^@dga;wxO}T=l^qSbmBDKAi$pv-3fI%Y6`t|lS-j(+>>S&8x}PS)eW<{{mQWq5Qq{(7OJGgf)N8JPvh0it8x8gx)9;yK`B z_*!t$(ixZ_OA47fSFYOC|IQvIH!s36jZC35#{tAtrTGRK`Hj9u#*sFHjn`I4}-gd^NR~HsE_yJVo+5ZdQ?^Q1zZ&e6DkKz$H8(hq{7A z3f1cW{W*LBsN3`r5l)9XmArBe`;ii0(`L--SGnj{aYF$Vt zT}FLH@+yeS2>6biNbfh6zByJ2ZQga!03i`=0+x$mW4^p;3LYG{$GJDa!YOnMtzW;* z{hckw*8V^8?&xy7ph5-#L6LR7fGfT`zSmu&t7BM}xrpt*RkA)aPzFadrmcKQm+9BO zZ9zyxBLg-#-`Ylc@Al>6tZqpOScHwApOK%xtm$G_VEQiFZS!TZd(EQEiL)?IA*_W6G%tS0dGL)a)64N zPywi2aV($B@=M$=BOs+(6~L@KYfHh%Kcy)10pE?qB6n*ZZ;SLyW=yLHvei?jGhiYu z^BG%CI%!;T47%sK61zOxChHm3@*pE}iSs^^rgX-AttTRXI}W5%$p;iT9O%iD@-Urm zRR`Y0c;~FKdTM~#c!M}ibz8tJ38ctr^3@qR&p4I#usMzo)mTX>Wn4UH5^P9P#m|P@ zUO^HjK2I!Vn9I%27f0w!fASMR0sM1LV;+`#0p-a`s%fc!BwMt_bCmnB0rH`o`yHCT z)!&wI>B9<9rH3*O=?9bSfgKNM&R~sDfk4=JoC_64G{iw+DI+I}t_RwB{x`~k*GM)Z zn1jCVs`EUUf5M%KcFx2;^mE=|qoXHuB#FN$jBxwo%}Ur*1Z5?|<@e^KeX}#91Wm}Z z%SEC5`2XZw}C-#tcc`^ za(`MApPTeFWdft=MF$0g(c+_N(vWKmnuF(n66C#jz=IahN4I;r28$S0Z<=PE0L-L-K9ps(TeK4eG5Fjv?fZGq;D&u{f6SP>6Mc<&?Q!=1qret`S?HX)7=060h7yub?t^vr~aB>8q`g~wz849EPm{#@!oELZ?gE!6zQ@^e1(z7b( z?WirBM6w2@(@rxyz^*AeP^3HTFZ+ITr~6gwo4t`EduR}4#lT01;6E6;Ew?!Mjz`t9yZatgqJDvQ; z%*Xs*Apw`T!L^ZP428y-7mzGC22twaZU%n_D5~k`3Ooj%rW|Ant^Zgc=631+7zMz9 zQy$+p(F#CGku-CaMtke)f$bfM5tL1j=5SZ$-GKgy+UQ|w_^I#P6DHa7em^@R?-Ey` zb&m>W+7834dR~;B!|;z(lP_hOMa#{8akYVH$NbJmv-aRGe`g>+ewbon_LA*v7NE7> z_YExk2AQ2|#UPpOMRJ^8HD;vESLXsVB--a^kuwAT&&y{hJ)o&^F^5xXtg!mEVWAP4 zihuoklcJ5?i;#OeWt{}T>Rwh>Qo=BVDyoEn99z*$O@p9l!9F|Wt2J>`?rOIcO#{Uo z(`~inO{+?-0Z|=iufVCdlAxE)AWD(*XYCIl=Aq%^5@~Ofq*?^=AQl8$YbFvbS}mR= z{W@*a>`GcHcxZB>goVnGc?BO~iS}UmpL>HlWQ1@CHtwtjVE|L*zx{49Akm)#q(6q* z5AE^$qfR|7I5PRR4ItCsrSRw1224K_^yej3y}Z{X!mJ9yI$w-CigfSelz*7sSEJLVX53<*a64Ibu}0GW>n5d> zRa^T2@}@7D2PGGfoo zB{cBd5l(rv4Z=D}sxv%We>s$*dxi2+Mgaqtt3l*dT%pDe0f+aZJ1pjIdPptF#pH`3 z-k8)-YfcI3W$jwigiHJ3po;=B?v-vLSpP61aE2Eqi4bhFgqf!ypFS_nBzls|;_yny zX1LvT@$Rrt-dcyUoWk#AoeBs6EbFyW(wG65Hh|0qI-*%y-cvW07O^AbWuh28s01&m zglFGjl8z`xQEF3C5?ZWN*}(9z4o64+@1sS8GBFXEZ-n%jqdzvgOL{hZ{I(Z ziG2WizV@&?40^*7M5${_%fqSMy{gVI&X|C^dwRJZTVg}a!OR9Mq$L2CtwUDk@!9;_ zniY7p-BfB-Sl{NeSkjK1G;N@AQn*T}iX0gS&+I1l;3WhZ_)>^olUk%mM)P7=g^ZFj z2nDdvoL~mNrbxpJdO#4d@9!I7FY*$ahu8idE6+%+!LLpKP3YKsTF=nck!5b$0z+F+ zvd^f0_{Tckju#r*MDXLbeaSx(3V2;PyCQq(4#;I@i4nc{W!3wGX4ob|&7?e+nQQSx zH}<|J+giT<#A`|(^|`}ZK7zB`3|#xyt;;=(Vr%~#we!Yzi|sYFOi zAYFx8LP_VXQp~#J+o39X`^JAGV&v=5DoFYWtciiWgHWmOdZ;V@;N^vVcs#FUN@Nh11}n@>E*- zXQh#yAY(nwUthcAk`MvxQZ1Au)>LYj;P{mI$?pFaiS)W;1!Rgou?6%@#Qa_=@j5o0 zyW1}MxV13u7XgU-sj-Az1JyPeI~K~!B5Xp?KG^T2^r>bp;`je1P>lEdnqaOxCtsEw z@m?ZyQoy``y*|HeSHHMY<}uZ>%C%)WPQ=oFd=Toe6jxMkCnKQ2J?U&=7Ii;v4r#e5udqGnyZ^%-jY3Y4^z~B{^|#?T zaee1+x0pPr*!GY&7+yNrkck_K1O#}z1=vrqlZ;J|-yxLHwAi~Qa-;x0obPXjXU!B1 zhd<&_jlEk&m`2bLP;hF1R+G_}%2U3B=^K6$*wiyE9*VHcyk|Cgxg@+F^JpJEIAG|D z(CWWAn!8(^^Utb$n$_}r3h!qahCf5>;|C{n*+|t!RIGeS!Lp6+MOI*ESKkvBj3GG0Wpo0$ET4`O#mlaSQzltVz=&{ z@6*c-#Gy+D-_-)B^~{2v=mCD|2mkM1RJR51qmkD`KhL`%zae(2T}i^Gp(#3&h=U*M zq|i;=6%WN?tsbO=Fv`XBe9^Gw2v~JimYM{56bc_*eZ1|iSk>R;hW!_$;hT> zlx{rzk77#|D5!nbJ(9n(#7t|Ic7J~0o81GYaoFy9!Y?_{yeYh;r&)!gH+eo5Y#(c> z?ZB|2@yWi;9QBU8#R-85+W#y*d3_%2PedL>Zjz>Z<4F8Yf2amh-18r%Unzk;it;P< zaIo-xI=JOvUl%H@Q721Mr@TAQi0yPdH72Dz&7Q@!;85Rsvx~9neMLK08IUk8bR!U! zbW&)(|B3QrIWvb9?HpHAkjb)f!mgv&fU}pF1&f0`3J9kV$hSk{w53Od*4P+xWLn;>>2J#V0MlpiBc#nX;`HUL z?QFr%xW*ik!z^ED9d}(R|1ScNqu%|Q+%QRfD<_>k`vXLoDm&-FPggfQ(NH!<0Bbtl zqCiMj`AD?vbs8^tr=_{`qBtk;S@wX^BTH?0Wy9d$G4Lxa0GWMR_`jMjWvhRz3i+_Y z+Na^ulrYKF_(R`WHA6v%CW(lVw9Bh~#@|8}26&j& zqnkWguKkk1D$gQJOP`#ikW6s*6;13y>j^?;U7wrqvEyZ_1ce7`$Z#gWq@Ym4d7OnM z{8<_UFDZku{J?7Hy`uhfMiy`%4mAwoeXk@Zz8qPv33P!&)^Bv|0nm!?UJl4QhDBed zZw2`2y$&L?_IlZTlYZny+ylF03P0(1Gq@T=MTah>c+%Nep(_S~MceX0yUxze9MIX0 zz}7yyE~aTG6J$(4u=P(O(_Et)RV-oC`Y)RXOd)bEIp}gX7iSs`#vW_Zt+JZTlH0W( zUHiwuADz0;fk4uB2+!ci%p{huF2i97E?1S&*REARPB7$=a!5ClM;f(Vym-RgT3!B! ztl|?ljw$uH$F2>l>X3}USDm(73Q(wc^OhVUPTP;aC)0~!_?l2sSvoJjvtAF{4+Hq0 zxHJ{MhB6=<=tA6(F3SoBQt{0R!=Qe4=VMDUn9@@r$zVSAEI!{IrjEh2r_lz$=KT7L z8Rq*hW&A&bOVrv^l-4h{-D|*>28cGL{!Mc8NLgQ;3;@O2)06>ZFTQz6UF3>b2sn zz58om@gpTLjRVmF_O6?KBO{L<6=0K}9wfER{WE$``4*0TIKqFQC{*=&-vk(Qs>0i6 z+ae_phpUQ{hNoydf@_tp)gg<6E~mSI999{WnIK5=%!-o5gn=; zYKcf+chXNRz;5k3+uM-d&GdGX;v>>M>PwzrxN@%Au`lw6Z};jz7t%Ts_+1m>awUME zMMEPZkTTdT2?P7ChfcJU?5UINKLFT&JKS6Pp}4i@wzaMQd`?+UJ#;$EgoDK-q2gdG znn|YoMXKWh(w2Jz0MT=d6vaXLE;IcXPox%J?aKQZCf{E`a0B3j+#Q-JHCyl?>aX2f zwO?)%HCW5)ANwHNM#2%?Mr!BQzIoPuWe|;tB_JrYxqlv3e+DYTA!}ntN-P^S*O|gP z5jk~hTg|Q&R}dR)KW!%qOvDm$&z^jtD)s4b#5&m5LP-+>ZArv#%n6%zrO;u3r6>04Q1m(W^_uMU*PLMsMQ?xQ*1bX*8d_YM$-a z_rHh2vg{Zc@oA^=o4&jZId}5&*VdTn1l0+oxgl}n(aR5U-m_>k^-3Wtj+v;RdL``~ z+!Eq(oxQ~gXTXzS>b2tqFuNw@k`lrL5)9%Mk)KC)n%gb+k__+#wuw-Ce5VCm7q`m^ z@MMvFbRE__eCBSjP2(2nY4?!*$ziDRQg-ldt`X+ylO`mQSehVSdzmD(z}Z_M8WA;> zNUA-%;(c-P!4k_51ZWz|?DijnCgk?=SxGLJ*_*`QP;Co1ip1@x9s3`f#+;{h+;INv z=gGR;?!O!Q7LcNloV?upjJQ%oJ2i1*TZYqtTma@&#moK!3Q>)yI16t(HlE^e=dQDw zft3aFL-#}oo`}u4hj%EjM4zzvRx{~`;R{x)Z&!-mjjePO3oJ+6em75DUOh(hl+n)_ zdFM3W&}f}o#%KpmSi8zWu`OcI?wf)}$aac>`&xi$%Oa(dTPNpwNhwF#M=hm_(3=UE zTjb(Cq^m(pRYVn0NWb`?XdBFhTXbB>a14rz1m0G0(JNlGEm~bGN0#>=~?_DBHL>(O98{M zaglDYz~1q3f?k8>-FnI>G1LTurO)PanN#`g-d@pHw~UcX)N~Rli_P9-@#qmwBln-Y zw!KeUigLZ|XXodRS;go6dp)_#@ns0KbaEecBXJzRLQ6{2^J|KZf@=)%fdmlMp)Cm& z5M65)!aCgUD0)G`7D+rEV(aTq$9U3Qt`;u2#HLspu_F5qTm0k)tb%Z5-bT`4aNDw9 zGh3?y!7VFO4h_zZTgfd`$C^Jryb}zrnu|}X4d?+a*fr$BROS|yq8UYZjr|h`1_tkV zg9CXH(}L=zCSxYI49q~Zf2jl5Y9_@XRNds9&Fv&rfVcHckJfKyjpk7M2>KH^xCn`J z#py7Y=!!K*Z|^Ser7CSw*n-`(FVzW)ptYobvzt6z@uwljtT@l_Sm_xV;W3?HaYKBR zK!UvSkqDK)+=8F6`vMJZAnz?wWWNMI13+XY)Vw|BUEW@0OIJM=$0W3Vt#d;VvHeo` zwS>4q@?ADaM&Q??B2-5rX?($fX65+$I(Q^WTdyjTeRfc^?>^jVo}*ZHZuxQqHhzBg zveL2D@!aq_)Ey%M^hz+^p@eM_gd z1O(5^WYF1UMu4xNPzG$jq4Q1C^*OLE2@tAA2=zAwVdF{M7PSJRVyD|o?F0gsPjur< zq5j+3|LnuqvskQlU1-zPHZzT_;qR>&k>;&$*ey zjjxrhF;82_fgx7p zrwa(P76@#;={!lzC5{eYP{Z9s&@FTFRX)ua2Q3}!T~QI8&+p({@Tyr^K?${&F{w;? zW1ecFM|$j|iE*mgwENf=EgY4_D5SZBeR2Kcxn}7sI043#TIAH#f>O{U%it{_6um_s zC^kQNh(9y{_a+fFH9@aK0W%g}?8~+jak#i?o@KgseHu&YOltiSXQ>iAoG+&ko&=z4 zL*ZDLkWrN$BCbsV6+y+uCo*8G%h>~IZKW-Plzp-d)7fF-^bb<1*->)bT8*w3zy5>j zZ{8D%3{PI9Z*9?t$j*J~hyOgr?Ww;(6SKXIQQj<+8m9! z_25;8$(rn%%Aiqyo(6*XzmN13ZWYvmlN-+i+~N9`um#!WoOkvmEsDz{t65^!QJA?a zqb@m%LlgiKuZy4K(P5h%;<@+Rfah8dTIA z0ZE)Q2&!$QlWGE3YkJhhdP`@jKCAb`?~*I^EqHDBo4IN?G85ih@PbebTj9lg{cD)F z_V18Ox%yU%Bx7+mC8093JT!}b%BuLAdF;*jO4jPvS&u**D2!~O=4j-sp@^$5)~kqE zE*zJ^hO?Q5$5`8{)1lkn?JeT5=)`%N5qbyrld;Fr3gR-cZh3gZGA<3Y>|%Fx>b1Abk}YlWyK@tOljW#g2k=-`hOwTvbGBnkhh6u z#I^E+dS;2oSkgdUu+-1e$H$KpA(_AMW(B}u#~$#0LLC-O8UCD~#+r8GVS$Eeg*j(h zc9j(a0{RuA_W1o5d6+R&U$c zpriN8W{s`$&b+?Y0U{3;zGe*N96KolzLGfk)x|9KOC|~2hIMNBxu>7>GRi){{p4}3 zw>=^@#Rr0_qr^kuLj2yQHaoCkjF?|2Zpn^C@U1;D&dtsJb(7H0z|A|*ms7%#HH`*S z%t(`W^o6NW_lPONl`^Vk6>7(E2widFnC3dWg|kPG{O7pZGE@1LiGP${Uxy;FRCE~`0>hNuW*TXe~G@-eoNtS1aP@rVpUls<` z(i{g;w=IBU(Xi$n2`~$oo_35{V}^pX@he{6RF5Seq1CtBaXu9ne0=^T`p;I>)nM@n z>+wYfqLXbeM>0T}7N9Q^VOCjat@r&FvLy>Sz-uSkp-3FZD%U5*z*+UV4efXuySb7hcVhmD^tA2=~=U?p9Tls>u`{3s>Rr3=^OJOY&XoX@kr+4Tvs&Uc2uWec}Sg z`VV0xjfzI}p5r`E($$Pja_Xi~ep0#=(0>tQ&8|G8#A#pi`-8;x8t0_jxG1!9e^t4z z`0lIKj_1o04|(#BHOdeFqzaAQ)FsA9e+D@Lu|Ryd->lI)k#?KJ_V&DY~0_InS%7!_d z9LEt}!X>=rk&4XsJg((YF9F3)OG^s;%_=n-kgH7Bf@~wNIi_5(r=e#ZS6cb_UKfja zefQC4zI)>-g(-Zip7DTADy_mG@~@DW7;U{E6F9EPK#%eaGU%Eil{Po%3BEjbM^eN7 z?XV`4V>_$8gN!Cw>6qA^5-}uaW*n9|q{BD|%~8yyz9PkEU}Dr#_#t5nR$g9Sac=Ml zFSShPwbLSDJo=R)C7vg8@ad2aw%zO*_^qjgQGVL@{+WDHw>mji{(}7P(bAIMn};~7 z%{Cc^@J)ltIe^Y>WI9vX=(BA0#y|h^2(2--Ik$o(QOq^>TOz2YqUN<}5_kN8eMw0i`h6_L%jF#^+i4j%sA8xFC zAGYZ|`k^SNkKxvR0&oQ53i!4%$W~|q(h17iMZ>Nxabaz_2^7QPPi30JI_te02+i#U z!yni(MfV2dZ4FnKlfy41AUoVD%k9nP9ikI%EQ^lBJTqWNx}~ri&_>P1P=#VMs2u6lcLD!^NtU?IL|K40luCxCa~AF~wiuDyQeI z-o@>>0`?bs^e?_A{nLL=PFS}1g{zePzf1MpE96g^0gKhCyWfnQyWq%AMgh1V80Ylm zxqeP#%tolmr%B_|#W7gRgyO%A0LF`fHt$e{(F1c!h*O(cBBXlxqvPsh@vpWI^rRRs z)-<)OEccr=+lvH@qY5i*3|l?N0Ei*1^>qX3X)p_LpK5tPwyvwSQrJXCWBi|y;N17F zrX+qOmHjFV@xn~DT(|HG(bQ+QD3%<`IEK>idWmzWYi-i04{+(@3dnbTmRqbTWX$ya zZ2^giy7 z_PePGSTeF>pjXO-u1np@<#5LnpzKvT&ooDg#gi7`gr-ye*8UKcrZ`=R2G-8^iff4l&CJ_G4YKeKJJ@qP8}A zZaZFC`%S6+S}|XdhBHNH@97Ojx*fJ#0-i}y(^1j|gkw<6x<5&MC9~=IDxV z7s`a}zZAcWvDK6?)~NL`r!4&0`Qs)a$nb*$)AC5oaV$8tIDNphRM$mlB`;P~izl?% zIt=BziZ)Y~O1J^8m&;J*)&Wh+bF}EomT2yokET{fG;fOa$Hu0G>cgz{EQVZ{*wJnY zA~;lKb&B|JyjCndUIjMf&>9L$CG=4I5B4yN7XKd=E4c7?*wuv$9L2d)+DYNlPL%OF zHvj~Z0OcNKkD7B<`+~m^{|k_vdIL;!L_80??hg%;s?F;R3EKO6y4%k0$riF6Z7pN0 zln-*mnMON(~;dGRf3l8ge__uBvcGl8Zxat{}7sQDcszD;BFWG?=0eHNi%k z7BI+uW6Skt6UE$ycuPX{2u3H(pqPluztNO(^nCv!knJWlXE^znX+Ui~)09&+VyG$C zjI-8&D(Z*RpPwPqjy(e1({yujl#bEI3PkqY25Y7no%4`7ebjDj!q3;&ZIbT){G{aJ z&bT@whwL(iDNtiI%LG%rG6mLTG;%76egUvfeFLMMzrDiFO-oOkUT!{2Lc!+E1F)6~ z940M8$L1Usa+3~(k7U4+PNID)Pu{fS9i5X~Vq>G_gd(YVSTinM6?ZH(nOB!{~S!!Pl_H01hk%?@{$cOws-Cxgd6$)FCAx^=utn!I25U|FrxvXTh?x3ik zG8K7cxpRFn1~o^&pv3vdGfVOfMy*t_2oNDS9mT2ZaiBHKEzez<_@gEwdC?ZsD=9Sb zSO;&KbCWS#Mx(jwSIUfy)DrKXm$Tb@zfc8hYvo@2rj`~eM3p&SsUX;Ta!8?d#KUa- zOp1zA1s46>m-eSd>S|3X=jO&i%4j;HKqawesWYZ_AY-&g zhTE^!PB;`K*}NTrjnBt+yS%b`^>C!6os4$SvByn1e|82^&h(#|s{hc5gn1QICCPgf z?Y*Sd6palJlsyfrzL7W0<{qM)UO2C%WZqBwgd$`ri+9w#2S_S;#e|YwshqfSyh;cG zln65N&<)ovE2S~KY3^##RdzNc=h?HU07W&1XSup8N=B+P1h)XYk>``eM8+VEJPKV6T3f>yCw z0%VKq&x(X3wZt7L8A@aT&8-6ck=3kAl#o^I;`ThL#8^Xi-P|F`FPB9;5CA>WEf^2k~v&Z9*5JUjvGLa%&h9=~teNzm>M1T#%r z1!T?{RpLWkd@`l2#`aat5i(QYd4bXG(NPf~wrrza^s$t$-cTEw-1<%J~y4AYC#KS)C}QG4=D$&eFqbUyy?!-(=#_E9Qn zTgXf!y$wdWNvgCpuV$j;x^BOaTuGmITOVk&l~nsF99xJ%gFdlZbBj@L-I;)+Z_0SU zGeGm423TXqb1jj$Zg!c{jW zMhcz&yyYFTEetjin&u{8Xr)O=2yr(d=R32LO6NJ-|B zB-%+S&;BygviL=1J)3l=w`ADeaJD{WrwbKp>w`CG+-{Lr3aZCe_-en5>GTg-C()K01{O>!tT+!qe2#-z=F*N~DfZ^K$GNYk47CiV;cO{+;8fwt{^o zodLn!*B+io0kTf&Gd$y&z=D@x$84u0awmqFnKhgdf5IGsT3~0*FH6;4!sz_nswX-& zPC5~JVck!4BXG(%>gl-zS33L;uLopW>^!`Fp=w2cixCasi`sTSewae|yHRJsgtP+g z3mD^22JE{m>O(@l7gX)fd%Y2M$aXq#tvliFB)NPh{(5LfKq4nF^&3&kT1`{C=yzPN zj6`8hi6I_s9Q3iIf>degON(vag=^60*j0~z$KK#H9gwQkSG{5dcnEkZDl0wrR(@=&+;t)QGCVuO*wO%6Fa5$|3Vo!|L9nx!DXGsTBD zYKGFVLbfCgn&R?4oWBf^N;U7LZ`eGN+P*Ld$WTn>ZBN((aHby~JeHtPxSHG(L1=wU z9_g1d7IH?7*6K|ocikUx2yhIVNQSGe+rU$dedXo6-SaWeH_y*7uQNbHRvhm1(0Ok+ zm<$k!ebGboSK`#n1Rli{gqAGYcD#L}>IfdYGpDVY1Lkmi%{tbm>w%#h0mpcH=qJn~ zs^QkEMQ-jWs=}aY>v49)zlrZiw+1J8^)ii~IcM0~JFoWG_DdS9N-(~)U8Fnd%bSX? zTXrfq>CMD(nxM+$*)J&wtpKUeFvDbtM8%mBo9q&sS~)Ww*rC?7!8yRFM7G;uZ$&fq5{piT=>`+LHZ@+ZqV)Q83 z@z1uSv*U&DmGANSVru^ZE+*}vwL@L>30-d{skD^GZ!IpK?hekDYL!mNxiR^6&QSLL zx6m9I-!Ynj`vFAdl0qp+r_}s4+YnwH@62h8$kL#iLVSvd3|=54*BATVo|b@CGR~SI zqNL>Dfk0zrfQgatwZ>r+R*t;z51GF-IgP&tKR^Gq?gJez;>24TC2+wP14K=NRV20dnvzSr zLck4Q?{@bid0_mqX`9(6n*(qODGZ^-0_)6>I+oCggtu(pM9g5#aRU<|w&OS91{G}T zUBHi9G_cdx5r4 z_WWwc=s9el89#J({l29H#(p3SzJu zls^78h(KxDWN5W&JN!3yB4l!h7}e}6bS!d5wamWj7Dz5;Msu4nu0_`^Rr?*zVpD>l|I}ZsG*sg@0dQ-={^%y7R_of=tPi5N+nQ+_euHp4`?W!o?AEBbUadc2m%1?5Et-a6d z=%3_EmlAowiK zYHSZHEU)zZw{1D^z-wkqEMv1&1qt_xh%FtS*)L;g`sZwwY8g%g zn+3D)q{{wv8+P}Ja!J}c_`~V*@9}NXG_>@Hpj0EIlzEb5*WB3fJvjYWGM+r)U9nr> z4^F9(cdo^Cta!fF8O&D@7u0fGfl&lelxZrd3AEWk{QKp~$EW1cuF^q)yeP%Lb4LVQ z{G4of^5*(-tLfww5DY(FJbe+ zTOlknExEGzD5Pkva*{g{oiV+K?V(D)=0LnK>c8#L396zxF4p^&Sa6S3brtab`=h1@ z+^KN<_d?&8ot~buDn<4Ics5guOc@AoPu%x66K>^-wp=6Ltm*XkiitRH&iJEAv1F>l z&WVmWrQ$lBKSe~HMP!m%5k_aii?hQw< z2OmL87uKo7p2_rD>oyjM$;c&>P^^&`RaIqH=?BX(nW=tIswU-~WUh`WDSy9lC>SZB zi@0RbU_FI_&>W2atAzd|u9lWo>+`=Q#cAq^(O<(^Dh0n&hCApqC)>{=Fb0gH=!Kng zfoOt)95=xX{tqfs-zX-IVP`HLe+dT>Rc@ngRg$7nLlOa`%D6iJJz_O z-;QaNY1*oiZ%>(Jw5GeD@oQ!(5t}kDT10VRP$0W>X<#gsi&5v9Py7 zca}+J&fzXz^yPiP3*j0C>DFGw%;nC*V0qRdZMP0ab_BxTnfK;!rB!vE{BH%f%8oA? zBZZnuNXG_U`|-(48RO2=DeHkNmiQ|;HFtk#V#L2oGyE5^a*^;+UAMMDO-cb(X`M;t zcWqjc-ZqBq9)86hP_dqZ29f(Moq*>r$rb*^dJ9lj@0B&A0O(q6=TF>6uDVLrWWTCT z=o@{zCwu9C&CGi3n*(->_c5(kr;ot#`nkM%0tyZ9-sr*IX3v|jQe23`!>zhUnWv%0 zvTjryr(U3_)^)o!pnLZX6YwHCiQ?s&bUDDFsB_LfAk|GVi*XKE&?|-mk+kM^VeO_lIDj7bCP{+0bIefTVf4zJe9t>0% z?)gm@@^X6B!R4+xZb4;sw4(z70(!5T3g3+iVb!za1i%p1(9sRg9HUvQQeU^r;1(nE`W_6fS`~XaI-_;Hk=p9f3k7kEiX`gka#|nD zZ3Y_K<*$rvE2K9}qa*L?k3eaA@QngNYW(8+NNP4Hw#&(0oyWWxiPj$f#N#x9Ya^Xn zdW+-E8kPK#p6JF1ooGC=aB8m6p$5mNrV>T6BTg!q&T$oOLaeMw=?jPj-Oxr%-kIWS zH`X`VHZ zH%|0(##Q$?8MsxPs*lqqZbUr1uc0FBJ&v> zLiAT@WIs%Y&$MQNY$4MwSX&?0*>gMObVyn4Vj~O3xMQ+;4YoK1Zvrsnn^W z%JPOkQz?mILUQ1<1fA^ce%*6fm0uuwb;~Q{8I%C?iL5Ii5z6`p%?FcFW)t-|ZBmy-hVThb2FeQtfBTARoSi2;;6J+Ixy9M@M>jCi2cpsKGEO zW-3aN<(Y$dU`d&US+L3!~$C>J*q_s>6*}MD|R*OpMU=ST3t(U zA-RJovb$QgcL4lmTk2sqNC~43c*dGoIE+=@A8?)BJ)PmF?c#%J?)I9|E_QB%|9?n^ z<^yGwn@(soNt(8Gz~+}shVsK}6*aO&z3Jq7`5+VUmE!>C$TT*%u3k$TFF&hoONV)b zV-1s1k&~d4aND-i_q~JCJ}wte3U!>Dt@7UN!Ee_ua%Cq+FCx5@k)bcw4eww0(2J_kN_$zF%DC4q8Imdz4HMukzL@v}^(q8E3?Q7;^ z$1hmUmUZyMZsD*g{*$%~P9bJ9?*}x1gpdRGa7U}o1dA*D=HQLG`mXHLj&Ju_ZWPYw z*>;8O56sZ7O7mq|yCV0#8*>7a$kBnT%3LaC&9DIL47%j>vjd}Yc{S}BcRY#+FO!rI z8R1FhYNI4BKf{7S$8Sq0h5gprxQ<_!Jps%#JVzwtn!%SRvTvf3)!JNr$KIJ`*K4w$PMAM)~bgN+T)y0d8~y zDaX%09?8>Z8&p$yU!2}gL1U?0o-C?T!C&v7|DB^JB-F3Lbjm~Q;n7SoPChZgy1~rZ zt)0nbyZx%O$D{L3460hL_W~`N)ZOn(TyHOYj8*_z2{jWm){Rnib8K` zYAk2gngp$6(WQeCH%7wmu9^ZABvKUbQPdv8>PPrPSyyM3eXW`e`i(AUzzHdNfhyCBzp&L30Dn1d#oNe*% z?n)skR)*hw-J-neJ%slJGmnc8@X~ciBB%eZmEwWr)O#YfHh#cB$|m+4Jxms`W!;FM zV{#ksuH@9CaPbrPWV6J*^w@dRx31Q9jvqT17{vq{l6j+UrRr^lMZj(5j z#!xWB3en`U@9pjRl_46M&4`xLH0z0UM(b$W)bHjeV&lR-EMSoBL20u|Vz-O(Ea0NK zvZc2R5zP)<4s85kXA!;Qqxqf3Y`lz?C0aA-uR0VHx96RjW<`m;(la=^gC-u#SBU_t zE2@wetm+_pIXTNdLN__@ER9Fsy5jp>U!~z49y)!t+OB5)#zj+UsWeP8`sj)j9G=Nm zq=QZqyX8Fa@DK|_6jSM^abh4HydHn0ZUDXPR4()EBZ{~Y1iUlbH|g14iC-Q!7n+-! zEt=Ry)45iuw?{55D!DRn2mAAs|E5e+T97I+!d@r!r25_4j9a{}z=WY-Y)Op_g_>c5 z+{eXsef@yI1VyyOvg&gvbf_D|AA%dV`Bl_K7%!mcPb0`e=q*`n7*@jLhGb~=>|KtV z`O7wq9b7+(w1)s!YmwU45x z*T1L$+R$FX@n0(|XXC9Wr`}H+S2yiz5BLDM3QMKlQCM5d&wdgVNl@$>wvc%i8_uP* z{f&-zh2|d%M|Q(iM?lqiPf%Iu2RR~7SJxnAF*V;YySa;lIkr{r6Vqo|V!l$#jqFjL z+R5_(6>;n#a)8>}uOkNy?VFw?ln;KL%-6ja%wN_&t3?OGvL=lnWAqoI@Vs0H z8-&0XAb6oywqj-OyQ?FgkVK1>85yhCyWYrZOowG~%zF=Qy94p(6h$RJ_Jh0AwEBgG ze3s~zRVfZyr0n~+k%NYj$z$##qCRnxCG7$(87^`X<%)AdF{~xb#{8%cX1Oip}@7TaCmXum~6q}rdzka5MZLR z#G)jji1xyezg|scJkl#g=KD11I5q=czm5)JoOlpBVUCOK6$}4XdA_4%H%Gz^mJQAN z=V_vO4|A)>t;VdYH3Wt>|A{ro&$^tTI{lUk3fP!xxT&h zU-l8zNO%hjkW&D-Bw9+Aht8l7Vg4{ts`9Owgs0Nf)O?+P%+Fe8O< zUNG0_#b6lMnia^S{QVn!eS0NTntSl?GffDGlPfuv&H4&=ZawQ5!SMPyoE2syNGccp zku28B9LSOIeL!w=_~}u{G1m*oSyxU96S~pac;VxR0jihdypCyk!p;96)$A{OSk#sq zE1EmZwBI9U_6G&u7~V&GEB1PbSqPL2C%P0=-+$pYy99(;+vt~voVR`S`9x_`?o7mH z&u*yn^8x@LJR7d?Q~R# zZf9X7TMB_wa$@KgO+|;M43*NM$7+i#n`qt70&ngfveL+S@C0-VDcG-MPo*gF3_pt> zj_DyJ)OF(A4h7%Fs$Rf7R&U;p)Q11ZVAHJsw%ys^6@Xp08X!&IPS%=sK6)YSfYdEBU1cfC^&gIt-dz`|#kO7R4saQme5}Isv+it_0Rv!nY!`fI$}wDA z)2#>H=UJ**6z!rM5sa;OP|y|X*F566dr*PS0>)$(V!-&A@A+Ca z*TR~Y@1kYEh_!o)er91&+|IuPI`i?qVTR5oc4A#LHT?}%H7ow2nFw=HZgua2B^S4L zt5(x|Jj)kuaAc^}cB$dT(LU*t;*__4=T_^QZ`{VVlBR#Dnz%!KHI~u9oO|D~9}^p# zZGhy|k0~kX+1@J_1|Ge$jv|)T7MRI9w&)l+GgL8E%hjT>rZ)?$=hz`iB}(o zas2jEJI0^R{2xEwubw~Z&&W8#f@37F9<8S&m8_AabdiQKwr z=ti;HG#ps5<^t4C3aXS7Bdl)v`8K85uH1cwV&Gh3H-J&5FMVOfvi;`kp29@kvNHmO z`&u>2e}q;}N=$nG9f0R`>P^EgIs%S^6}TrZLNwuZsFDRmq4W<+>#W8 zuYAH^(L_k`j`2EDYbv{X_}(>B#?7NQ+fM*AceQFG*j40rWwkBY5nXWP)Ieu0`z?cRq?Sb3lZ4LwtZJ{k z*hFo|PD{tuYB<-K7KJICAMGdmOV>67W^Zo%Tj+d#nalHW$@KuW^XNX|f2-lIHk$ow zmOD`iLfX70BU9pNgsj7r*>(~okoo?j6-BqV_QxHmj*%t0@02W)iwQIUEdL25*m-{3 zKJXj6f>DpVU+(<7{-)6Q;NM9`oX{8SeC#6;GyA<-pNwy#bQbT!3ndxKP5o;+2c__% z`Qs3{bPlQzo(Z^@l4`!{&z4Gn`s6RWjBJG8F%||l81n2cXOX;tSfi{`J|9uPQb))Y zYBk@$b&M^q#v@{ego3mZPTJUWEu9x$=+2;5P?i%*xGJZk-S`Yr6aqWU=L!Egd3)<) zo7QMp{z!|Sc3C@e_ecw(Mg6d@gJx;q_8m`h;FR)!{tOD#vCkA5G&Zue5yMNkT%WNQ zOZMlhK^vAP-NeM(*LPMvo^{=EEuLHvby?%pasPg+c$M9CdAh}35fV1t$dMErleGO9 zV|ITDW|V$*f+n9j6GT{0Nj+~TeM;Whtb#A=k;HB7w~?{7p8Yic<$S>Zp2N(KJE`ii zTETZJ<@(}{=>p&<#(CaxqW|59SB=;Y%jbFnuH&2}D{fbK7DX=MI*w+)c%9+{DGr&X>mxc6bv zN%VDm&f&Mps>%}9F~K*Mh*I|cmZ&tb!qL(ysS={Y^!>Zo(#l}&CwNv(|J}Q>zL|(U zF1=vT;-7!GMT6FbKP%1Fk*BibT7N*iV3$Cf;VM^Hy^+lzgh!q3XxU(BYzc}?eLnp@vP8FdwCE=ZWj z$4X1?)l@7#e|WjW4w>McpLG#u9erVu^M`e595TV|=*SrBmL+fEP+~VQR@S-c)B=tl z&TG>-IB8Rm*p1Z8vg#46UHjt&q!fC|W8u^7^zM5yG>#SKZj#q1v5$1~skMD`3%Yt+ z@iD*Tz~!F`S}#isUkK+|qvUufh`$R*M&*~*^APhh;;3v*7W2-e4E-duz1}z@C~Z-Q zt5K@!rJNjPP%o`@0i~%AEE24%Q@@9byI+Px*qIyhjPTU4XSI|#>nhc=wowjtYq7oG z_v#dRsafF$9PS3Xg=Fb}A*FWd-I^q@QTYD#q zwd^n{s<@8je`UIkFem7b>vfkmQN5N+h;q@w?fD?N(AphhSFLVQflj)w zFV^{2WzDiPs_km^&gX7k2(Y{l{N%SbZX5+S$SsUj}K(5~zn*LIE?)1Fp<(=tu1o?~f z^V{>SbZMr(ALjmH5Z+|sNo|1~mmwoPn|Q5FC&5n+F?z3Uj7&^}*uH)mK`Y%Vk`pb_ zdwYI9$@Wh2(5bQy<0m2%m|>4t{?{05n<8t3Ye&py@<_(^JC+CiR-=& zSSY6zC;|PZ$WW;qm>a;Km^T<{FGjADzJLGT_T{{7bc7D)rwsHE#UU3hx-SbaE;GnA zaHVwEPdC{BsN0N5Lr+dXHylrrQSKL3SWphK^Z&^^Q77xPt9lAI2bI``@VltJ3DbtP zFd_hgFtjCQfkiJ*b;r&{oE(?u0ccoMLok-QAjDXY-fnjUYyoZWR^DcqO&UsgqIDEP@_XI$p5w(_zK0-yDgv75^bn_S5y5%$oW z&Y5@!rC2^0)-j{=Xq~2h#kr4{M3>`W%&Ko1er8!?sF;-9EU6QxaMnM@r87U-ufwB-N>`_JG{VBwJ0pkk>Sx!?VDkf*jQL!Dxc%_h}qjD=LV~fO$ zdP+|;QFOthD}Tg#xIZlLFaIH1o(2(Pw7R6`G}QU{crN(aY?joG%Qrz^iMotk8DN|-nx%mCYGH~C81O?dl^dP z%20`ezw^?5p6u|0e<(a(&3zU9ZFdxSA#>{*SQH}=|ta#4{9rfQ8G;nHZ znlBW&@s;v>yRRH3&@bSKX}&x6hppedppAzs8aDqk)gua%)#@ zk4yHVzW@GeQ4g@>*ryA7A1VD<1-yOE)M-WO056FW+owPkAM%+-rHJeGTM=MLV)Gx$H8MQxY<|6BG!lX6VCm3&A8OBV|b(PIc z7=yx)??mdAzg9eJ+Vk3Q5?v?PY_{bXv(XnST|YHT;Dv6uHaHk3siF8B8Dj58<3Iyg zE+_{Pfn|R2g7Xe}l;Lb#zwP{C2ZJF}B1vNd{rZm6Eo*ZJ2l>AL-QJZlc}6bZq}E@g zy7yA$?!mVp=i59*w{aPGG@ox?=X2FJzlUt!M4*>A6)>f(nm1)j z9C~cHHj6720{G1DAOqJWZYRq7$wYDFgmh4?nQ$VECmyrcD+R-FK^J$6GXCgT@wc$+9WhmI z8k+)`+U*Rg;=1W6s@0l;9%CYGQwWzX3D-kdy#@Gn!Uz*Q$naBKI{Cz!YzFB&p6LhY zflYFR747xg{3huT1#Ml9qD0wvIIo*2JeiK1Oh$G3lM1a#B!&J+Fv^i~+;j*;+*pcb z{;oGLnvXHs?duA<&3isZEjB>b7Xd?92}7-TaMBkiu0hqwg-ZNaS6A#8s-{3`Hh<$m z+C%U0>D#L?kFu;jR4;o}I1#!bEqL8)Z<@3`$ zbH&MV3uG5~XM=_)P0EAWNn+b99%|ianv#$%SAg&S>^zr2B}+TKZSL%}K{EBj3a;4d zS>e%90ive4Bdsmnb9ntq&~UB63&s)4I;<=&!=E&<*90XQDZ>=yH9Ll+zi(4ojzN3$ zS03I;3B|)Ll`3D&V0Mj&4qx>(hbX?c0%gp&w?6=e4#Ff*C9!uqq)l7Kg`>NZw$go; zrfgC`cg^c`ebh`yy3CX9C6Ut6npaX#v*ckfkdd`@_GEK1hOlg{yzf}c8c)(*A$*x( z8TC4hx+0!`*$;k?W*PS8JTucU~+Tc{bPd!fW` z%>RtC2K;s;Vo?0$=-9u5hV6D?oh58@((rgFafH+UaU7ebKOwlfw-90eTZsB~VR))_ z<%zeC^UV89E8j-iS0AD5A-iAdyA5Pk>VKAUUd8*22|f4IT=&hhqiks`gi$e_r1y)3 z^$Z@#c)c|?kBi|=)z>#MF&kX-zp@@ZAm2~*k4;9q*UUQ1zd^bo?DG0YS0X?!P^q23A>K;f zR`toE;}_4Ha}&h)WhAN+hCr`ipP8;RDfha+R_ljw<8v&lJi#Lz#fOx4E*|Ep*$4eP zOxQh7*PmaVtQD2AEBA;-=o2t6ejzg~a{C&^C?uyfZd(ZWA>(ne6~ZIuT%^$dg~>$N zy6fl$=_)hWDpfW>7$MRT8%Kf>{$UlSa8=68NOF1ec>We6L4+~BG^9zd*ybH zyL#C^wxvcXDo|+65eHWFce@a`H%4e52$S=kQKPtVTscE0OBk#Rhz~ag34nmS3Ot!L z9BoC4=OR4J&3^=Apwadmh|wcvYnpIE6bx1QM$OpwI_GXO_sY-$GAQPrWTu;J(O@>+ z5L?O<%w5;IxQX07`Yf|T0(K&Jrf5$;)!fo;h&{1)N{s}1$?1y!BdJp6kBEz@Y-t7lL{ad$G_&fosOxntfPz)A0 ztTj0U@%sfnDG-Qp*oG4gGpM%3#msi5(LiDCd6I((CHx%^A`}g9? zUypwUqaSsXo@y7MHSt5SG1uu(S;jCGZn(K52hpOX+iGqMciBSiXIA;7`Md=|NFf*%vk&GW=1hOtI{|Lz396w zTM$1qpfG0Vx93TDP&s6H-R8_v(HV+(J*pN}_bKypFA!^sP9rKFyXinkWE)jhD21?b zrlwJu|NAj484muak9t1CO#uBa#vncod6T(CTVqxcpfg$$3`M3y4)dS0Z=3wx(rOE4 z;oO8sn>H2ZcDjWgyNT8EVlNo6DIoa;~u;7sEHS9X+RwjLc$1J239f<5>;bk5osX8R9 zDD1LdqO=E8&#}xJt9v_r_)=X#82B}f)(F{uGkpBkg2>39GgeRhZ|D6N*FI-HYX%Yz zmD)JVNsEr7Wz#|vkU7a+!miV0BWyH%@-DJr@!`$@dbO_0J)8$ry$MkxY%P8NUEW6j zFQP(Z)$>BFvZ@p4HuC!CKO}O0S=o8CL_vS%+D%3*4EgYPl|mobWrQ_sPH_LF7xFgQ ziOenLLyqi}f_`ErFB$45paouN}fXddu1ySSXeY;En8P8E9F5 zYA~_LoZJlQvUP6EDEvGn6 z<++8p#bV&wMZ3?={Wo_YF7)TL&ieP4sWoG#3*w`RNTMB935h*+$n80TQeRwR8@fqZ zY^N~8u0CNB8L`d_AtNX)U{cc#YKTQPAj80UZZTz4Z)Xt3=>@`% z^|P?wU+*7M1lVy4N&Xvj%Ty$DQ4R({j1UW39WHLZz}Z|ZpRip<1Ki^KKSbjU4h`7# z3P8aQb~|o9r_oz1e*DVjr3VaZ3899P`$!(mzS;q+v)CU5L3_Kql-ia_w4+3YBBMMy zrKV^9T}K@+P3&qJkygFcZBy-iSLaZ;>qLeCXskiOxDW@_6c#jXQg~|ijoMZuvuZ)9 z{Ziix3wB?Lv!i_NTrRjipj+mA*tejQ8~Q6o*~F5s zo_?gD6w-j6A2%J*i|n-$=vSSDo1i{FV+s`DG37hA1!h}_qhCQhq+|y8iW4;Vjw)5*(E`qZa*(bmh_;GZ@w5)HX>?9 znTgIKZqCKlo<1k(17BRfR^Icf!%#l_(XHn;YyyY82qOa$t#CbH{Ud>`R$R$8Uq(7I zm(Wr&Wl$ai(pGxzpnM>g8$VtL+BURUbgTkQC!GU)V?O&T{!x8UuY<+pgrmRt=|}tD zX7LW^#8RSk&(MYZBm|38NDzGAiKtk9M5($^CzA!K%+~&HL?c)R93Yb?LqbuB9>XSP zChKC%72lf2Ml8?jd~G83^Yi~u9-zo%I30!*qn&hh`5Y8*rTSHbwjzl;r(OtC(~rh} zQj5+)l7M0LJi+lwnuIU;+(yL_X~Sq zOtd5mzmdvqNeShUcMd0KN(7zYZ5d4a1;fsIXddHN{07LypNz}rage;0T3+qpZe)jy zZo%adw1ha%-0D^!YV_l*B$Z=RyxSwVujx^cHFNP*5g z99~w&25~7jSqu{eGpCfH+1OB(&lM!hN)dM52s!Xc09y_T*nf4<(z8TqJnF{Sl~et? zmQJ%fBl+G@&3Ig)hp!(BwU0WohISre(OQt_g>^PBy4JPRzB(@Elz0%6U%4@5Q`r$F zi}IdrNL6Zew$&w+wnX@BtgYQ>@AxQ5)julGBqB*?K-(AGyyNeS37p(|OBu)Ai}p5F z+cA#ZThEaZvkZm3Ko57_;v}CMtdSPH0u{BV&QHoyi#jvD#7JB~35ZGhr_1K#ulKNfs#?M~v^;citzBvDL`e;p` zv#9J;8$Cv8WVO2H`%nUO2x40m;T=PN<#Ma_8h@pBrsraz*n(OCgMtz}6GOkQL z>ncyAEUQfmrxF`+a70o@?F6L}Yjnx)rRD3f8DY&(iPjNsu6@;>L2ghv>8<>7W4)SM zG%*xu_%m-&yBYn@HgXWWd8vtFxcs`@zhU}p&rQZ){(|B%GAmaO*G<8UZL|4qH~ zZW5-rIiG62jfZQ_H2+~==<#R9L=B1aTYIWXGZSZ5VrON<4(5k5GEs&2oArNYL1@~4CAzi*X zM5+7s=igzH!@OH0-17m`E-O%W^?ofM3<~`9S`c%D#4BhU-->*I@86;dL37Ef(bCdV zRY|4;6UMB_|N9jA_LRvVo{aN zZ7sX_k6ZrRPme$;=yK@O*2jCCj_c)*lJ?JadsC$dA4d7EM$O~0ZKfoQhcZv_>9<%k zKf0L`<5U;N(AtD_r?h?X6)|Vf){SgTRI1j44lgvm!G9}SV48&L!vG9|5D=opy4~#J zsBb2*<9{>sAzKn9m!kpY!m78g>OpTBdRnJ4SU}e>}i267R@s%tI z&?4g*)$T0|*5lc+Ykh6Qw(9wy_BbVRKXCu?X|HpmFDfeAPrK?zoxYC$rpyPr+j?h4 z&4JC&F?AI_A0t_Gr)H<$wL&HBznYG-K_IirsIJwC1lx5>{LTlexy+Ar%(e5w_4TT4 zWi8@z;o}$KWw`U$uA9Fb;95Y8LBF|*c2oI#DzX)#Jv;ZiHyT-@JAazicJVvTNnhW+ zdAj$%*}KLfG1(l{k$1}w6)ei~^&jFJGic9Ia)?M=PO8)1d_n>>P{yMN3>y)+ zU^^{7LDyGWJQv$mwPrurr!dSNDWYmZ<(NBoAn5RcVj!s~S)0Xt3!EQ}Ag_tH-jxt3Olq&z z(#iFW&EUA-1&);AhkMByE#mu1`!&YwUEh;RkT9=eTri+FZE+8~+M7TI)k5%?{iJ93 zoV>ii{poTD>u7rL2Ugx_3~529-yXI&|C9O{?ZT`(*QVkyLUU~zjB1UPENk5wxFz9~ zT>UJgg!HP<9gbaE-(50Z;PJNfp(>}A_um|rl$x0os;{_ zHS?O^gW#H#EZ25lrBk@v@>B(yC6&CA+?4!OW0NHM&Adtu%<`GLAhjU%Z6d=cD5`Py zRrsI-yK1uuNVne`z}5IvR-jv~Lcj^2CB$N`ll{kSAeDBn_pv&^-bxk`m6IPpscJ% z!O|_e;qow5xnub7{kxY)6xR|cTH7*lkSS5+xR$k>G8aa+_JTe!u+fyTJNoJ+80}zA zYc#bUo#B{uM>YJpga!+rk(?HLBDsAD(!8eF7J)(S$g{AAb;5O8_c;6eLZM=^f<51M zr9mnK+rcQhmPL01`_ey)*(FwW3l|VOf??SIVqeih)V9FPOLxtxiFLAaFj*QS=<@+* zvcw$Q;zLFuCcrc7uQcH@6_WKj)WoES8H&I%=ltfSB%HQ~Rr}$#!VSc6j$dJU-n?w; z>I*3KYW6mQ-W$3fLL1S`bc3cT^UbO8UrA`b$5Y!nm8$PiR-v^+_JSkQ)7UW1!VKAH zF!238H6V!Kv2O!RoxDJw72O1=KfU-%vx1!%CPVUD~oK7CoWhFWI$5XyyCrZWimPdez zP=kExg7i$O9Uz8qdk?(foQC8V25_ z|DOhXqfAp6sM%(tiVTiWoS`$}A|gP%4e?lFMO z$2$+76YsUcQdYe((l{E4&6xLQT<};Id1DJ_(->)kS3-=XM)iHB3(mg2jPPQqEt3Hk z+7j&5q6D6)+@!Q_tQlJFyA9dBEGCMo;R}Z!rO;Of&qCGfj4BP-$u?)W(RrHQsnX3{ zy#%~f(OUsiV-ckeYBJgjAP1!>?z@-oTXC$pv@j0IcC5BPmmY>59y&ZP;y;vsHV!+F z4&tici$q%A!n)IppQ&30kXow5zSY-dZ$SeWL63?ilk*NKZ#N_(jYMB3SvMEg^baR_ zr!Ag%ODo=^8T@Bb73XE(2j5c&l`d87;iYA9DOLU!pKnhEyKg6#K-bNrG|lH9r>qZj zrdDrx0MWmHzd0u*z<2o{sxG+r>XV`a;=*c*Df_N%7QsboUE zVq$+TudC1=r*-S5y@Oi{NDR#dmVdu}Zmd>{s;(|@7af%LWB4a1_r6nNZ|dKrt4O!` zm{!+VAP~XVy%gz1CjUe5?e{j}y>H%Kd{=E}-_%U2!lizwRFL*Fpqh*R#m;@}NkzPt z@P_AJZ#|O=bwL3sr(gNDIu;}M%LPI!|JWi@kfgwOdcTe#u|Dy1p8*KS9^U@XLvVHS z1t>9>m^iHJ?6aGHxKCPafm!U$wp7u^JH()K10JLezkc}y41`>Cof z*1LR3#C+Fy)e@d*33-J5;V@ZF`y~FiuTxp8I(8SXyiaFN^og!F9HK7yfuo!-S$;l) zG-C|CB5qYId*{+n>{8y_nw}s^n~~JJZn=cod|aexa1%^b#J!G>@Y-y6Z}hk-aqlpn;eG$RovMb`rG^#ST`5yn(lApW~JHtWK}d) z1zg5Zwg_!NvuZm}JNS1tK&=uqFjfF|`gzs%Z*uc<5BJ}rD|*O}Jmc)>quAd*+CspI z23w~~#}M~Z-a7wJlHqe_ytXOvw4Z%`S>+#Oqmj)sn*L`jwyE>N^l?$*F5B?S@eGIl z3(&J<*RQsJ9d{277-!h8viRwvb`Y0Kw736su=%5SdE|z`+0}J-T_lQd{O6YL2x=J zvn=wYU1+pjp)-1{ya*^IvpU%%5{9@{-yI4|+1P?1V^p}G8dEHf2-0YsTf z>r$F-GRa_Cg%>K*#WUGN#HeaA1eF9}G}YwR^eR&(@vu~UV6}+md%Y#dt;#zOA{z;* zUcb#%+~)&#k4+M{k;4>buzD-lSrk_Is6&C^VeeWLI?R9b(+*xYBk@K>F@4d{kdTdS5(7O{lPp zpLM?=1nY(KLI#1PR$>Ta^SCaf zy!Xq%)YC_$iLbpTa5=kF6e6qe0@$<{BqKZM4#F^Xg^3M`DCQK~M_s2x`=%yO!k9ADeCWn!pFnmLHvZ9ToUM`iy zb`#DL98XR^HjA@Y2Z~D5y5(i+@3KXo=ut5SeT08793LMy z-~<%*EQ7drwg_1b+md37Y9cLdeL>Oc$8FPRr`Gl!3$izlK=os`KMH`28~5 zhtu^(-*Q5sI8Ugc$#gF_T)EpU600y+%}7XBl${8x*-<=cX~`gIew*d3gTklH8Q40o zKDhUCFO3^cn^o|8GFb z>Mhg4@|%yhUmlH@+-W-q#gne| za;qhCLn%!qMF;YX>E&(%do}_Z=r9Do@mjK+#cb%v?iSc)pv6bV>P}q&W`V$m;~(6w z%k^*s4!uDuE|*$FJh3rXWwkApTMYwMXS-BiA_KE4t5H(?(rT%jX35)Rx~MJ}4HySJ zce5*DP?K`Kr9vaab37e}gp5x%0_g5jyICrWvDZH{{yW`@9GH13E{t2ca=3AxeH@61uZeC60k*dzHBq~wC5U=+r zCCP_9e}E=L|%`TWOt-X3dbaz=RRj5!v*<{_(v5(8dgCUTJSq zb?K|>WSGPD?Kk7;7Ywny@*(`B$uo?3l#R>2kao__TBo;}73x>h$JTLNFpdzMo07Fd zt-v>16}n#jA%Bvve+L9oYlg$JuRnsI%{CU;ePyzuCJeNa0{@Uf)r3(DeT!&uG=5or zpbxf4QkiuNo-RVmw7qn{gxw*K_K&~$Bz?&(^3UMw_@mm^zhCaF$6ULb0!{RpMqnsh zxk9fju_&^EwEuT^9%&rOi5M7Lr35C6&Gz{nL|zp_m|%lof-C+Week}rT}F(QshD+m z#%A;2A9sdMr#`}R_2_wj*uK7-NbN&r+= zQ^=Ht<78gJxgcX817W)LBSA$ZF4dpl13!%E9N!g{S6m-W1k~E^)g=&8t)&i%49P95 zm&+&}oGZl-biA>&zF0JctiIlej_PAmQMKwz9l$=Nd?6ndm*k4)uv}}lmS2gMl=xhe zGpCYU-y$V2;;IH$wI1}4qyenTZjc;xa#)Nb;ym;S(>&VE@KVH#NMPKBjRJXGBM&P;vZu zA#=IgUyBJ0nzji)X3SRAfY^YZnxm)c*VlVSR^KWc&>cTwjM3hkkv>i!{MC02CwV44HF2#`-J9Xz^V7E)dN2~STNB=)XN{Y-G-8QDo(ZM z$K0Gq^vvkx4lpa(R(-F+?ys38fEGvc&rQa#cTDN?D}5`xr$;w99T)8E0uwX)A+G+K5hrgNXSjPsmd3{8XdE6sCPd7!FUeOR`|# zdo^bCFG5bvmw+;-glb@MF`?3wxP9knm7;8}RF%~4o7rhuc=^3oDtZfQYpQ4AyZ=d` z4}{HJ*gQZMI}4K`UjrUl3L3?vkr?T1D}gPODwk{yKq4Qj+WEx(PYdw>XgceM zCf_cM0}4t=Hwe-lqXeb9yFri~qm%|oB}b1g>5hSPqrm78MhHlENq4=^_lNg~{Q=wa z>^|o{=ej;uV>$2brLnnr6xm1x25-;-o(C{u-gITVS^_o3g#3^feIBdA5&I*ze5X1t zza@*1P7y0!FhQoe;lK@qQEx^ZroFXg%QDh{FRM;)FsS%%&;Gv=j$yTssF zld0!7zdF&nfUAecXx;X>_sJ>KQO6a{c#hciTpd^yIr1B?iSz6F0!En2#$4U@dCyOW zYB9^Eg&!SrljusikbO#XdXbno*;IA;#C#-Km}LZNynLOL$zChFe44f)N72q*{xrPs zcXr9=`~kcNL;3|yH4Y4LxXcsTWFS9kXs;q=#mkeQo0ri0R3nRNEU=VAPrk`SBQ%ee zqCY#0Ig4N5f)Jcqf05B>q}|3eZ};s;<3+GEiH7cp5|%hn`C(Sj?cW`BcPvU$%W;UG zVLYnL;gbjnMQ|@Pc*;tInNKSk&m&X_Ycq6B4MsbHh*Drfo39EV1AQ#Dbd!VN zbk8fH^qJ%e_1f-H=wn8GkdKs1qZ=<%f)wMscU+P;mpT;M%?vnO$y5P`*a%`%%Goi~5Z*v_4yJV$p1M?Qh0y>Db z{*>^o(#qN?b(KYnsas=`Zua zh4+}A+)&I(iSLf3+ zTwillu=r^2R6$vcX7T+2l#b-V$g*j9?ID6bzRv20p=JFl?<>`U-nU=7$Fs|FXZ!4+ zyT%a|Be0ZGU3sd!Hksn`*L3Px=HK}>vZJEJ0Ni@W;=j*e<&&sx4)m|`(HI|~G?XPU zm#okF;t%!>TchLtu>s8=GcYdisBQrYq@&^-QFrg?s9?Cv_(R>>YZG(7#JUSHvrge{ zpg^wa!o@0ztQ|Otq5?GFHaI7;Z^gBRJO)$rjY-0n419*QXm7V0lOvEx3~Vnd`2}L$ z{}7R77=-H4&ueQ)*XMnhB`nZflZoG!9gw$lWG14Hn(0@HR*kWYtTfo4^c#B4jVRdi zK}4k*v|s%rGHVy^ACIkw5<7a>`2erx1}0AF_5bif$kqF@ys$&=VH%?2*{M9^P^Cgp z`ZJ^`zXp*9@`uqfL$O#}2agiRh0<~<)-_}J5kdURJD_+mSI z@%!K3TBWmZtVIDY6(ByEnEt}%>>38N1}+{R9$)Ziw}+4C%#+i0U3N;g+_*Unr-#xEGNR&Ffq5jBXYSI-@J zwLjikfdo01CC-5iwMx+1geK;LZ-cQ_{pQ}SGUfZ^AEZeM`+Va>V~vJA5Wb=Y@(StB zb>zy#{BHpu5)%M?6qHkY=#UYKFilE2NEGYOsMc+tuKmZ;Msg9YHzR~&u5hhpS#8p` zg9K?p#SsqUZW=?aX=(cIF+ml|dht3+q_8w8JxVGlV>An|9kqf`=BtW}u#YmR8lfcC z(OgT)YZ3*+A>x~=vjUu?-x&|=dI0ky@MX_;`90gHoLl0%-nI0W*+PHF)-%_K;nUwe z0Ff^kMUEm~h!gQpUCJ|ARjHpMO!9@SFgPm{Xfw+BYlU+GRw&FC#GIETz$c<5Nvbyz z+lFjmz=-OmC-9MD)hcw2!Mp1j)IBFM59g!SQU3l<3O1BPOa}TPB6d{l%_Qi-U3QD!ibI&-=ST zVI9nn(;Y&nE&8fQ@;H0w9V%9I9{ZH3!wi# z!m}TpxL}!*oabH+3yE}N0&3n8LnL}-1L$Z&l1LBM#PqG#65iYX*%FH`E&)mlo(Fd_ zKIKGR*F-+=ckhqS_1B7um40ST+Yh?7e9V4PtRy5^lLbJm=IX8}N(i>uTj2%!r0$22 zjp>G5h{77zi`PS*oIaCEC@=Ux*sdJJR!nLx5!W~R%Afzvn&oxInmT9rO;!=;?-VF z3U5}vq`9|i>HKr@U&NrqW1_@r=Fa&JceThJ!Y@AZWwTgjea*_9dONE zFHa6i#1r*>{5D+SKqKlsa3GUcHB@Lvj=P3(R7jA~pDMsptu}c_B_D%74>U zD{%T=zIt!37QF^NJs*70E-JB2W&VT#@Cj&7|Fs>ty0~av296(q*IanG6W_SLBXRZG z|FvML*2y={pGro?qG<=%fb@vgSt066zPiO}qVmj@)}73pY0E&6Egr#{}qy9 z1ck>3$^pM<m^SNGT_LJ~hr%YM`c)}b$+lEZp-g0afg;wxl$t*pH&d7~ zIosOn6?LsB6wsHXAyz&(p+uNf4THv`vk%=df+r%eF=*U{DxzZ8-;+th5@fq7nL`Fsj}pld&Pc z*0+8zK?*BKMv6-aJ|R9xKCht&ZX!h0)8Py7H`G5)342$z@w070both$zOw| zRSgY>NqTA$en}l0I_%>4YKt=)>AE}h7bwI{U}Q`9b|W_VEi*(GL{}WoB=cSQ5$DI( zS+chQZxRi;QzKSyLi7{>3}2m#qBXT>l*P*?^se5$lRU)nWcrZ)$1v1&G|IUPms*^RN--c9b_xpSn6)U%%)tMd+yU; zJ?KCdn9_v`>{5DMm_RR|<=yBq!>p{c6#!8O+^%eY(mLBos8U}g42W33EwVv#s1$j% z3Px9UNU31TL|}Xq{#%^(n>B-P`aH4UD0qrCC`@{rjGq%!X3A+o zT(f-S;}h^pSRII?7Rux=FSnr-i~oV%FkQ5h1a)dI03!?2pjv6vbc*X8JNlIdS3Zx{ zADi~)UgK_l9m&Ee5uZpND5;f6qg*Q$9bfBQ6cA*y2khLpdl^Y!Q^?0mW7nMT$f*XR zR>y|5lY&|A&rcxWh9VmHW$=qn-*(`B6!onPbbV^XY~kjM#oqbb##TF|ugmTHzbAHYeEtK%@hvybnUomt zoryx#F_g!8IjBVu-msTc;x9^Wso3s)Lb%Od+1da0@j&9FB=FSSd%vpb`a~n3TUd?d z=So3k$og{=W~nudKr+~4b21;`8TfA|qhFDn*7;6lLCDTVg^nzpWn5K$70|rFWfzUD z{?Otg5Jv77YDiZA(t_o!en2&FaODqsttLFnukKsvBIl zpCnSZn|cy!n{*?~I`GB7>Djw~4nPK$-&y8=DJ(}%n^eo!7#ca^;I3utMC=-46durs zAiWk(rO3@2FL9)T`1*%qkFMo90OL?wtWAj3!KIe1HTgU;{2P0WN3UJKs}VtW$dy>B zH}*W8+ODW?eac&A`P3xuA*T3E8crDI{xpVzug^o05%;U=j!Vj`v41B-XZ4zPaV7C6 zmNWWUPBfnz{J%9sj@rw$*&Fd3xp;cYzk~upRoE1XEyDE&8~hrZz3I&ttO)#E(PxCe zDh7=ror(%sNhwBa?aCQa4N}*T`Fu{ZzlGF5Xjvyxk6G-MY*nF5^~-qC$X@#v!k}5o z+dOdF;@Y|siw8&R&H2ot{+f5Un>AOSA?Leqsn9k>K6K%D;(RL(IvQPe#b>t)Tj1XjbB~(fYa#9F&3SA8+voM2JNaZsQN$kQPk&WDob6B$zulCL~Q=iCJph zC>eC3>sKj$k!9zzWR`6$^Ee?cQ=)Tq3E*C09|rhWS&|r5ICSziBPZTw;fc_L6B?wl zj==@QQsu-~$!+8Q_S>g`v+i8AU(LVrT9UuTa3MQmRbtBnHs7 z4DWwxkDW1^#E<5Zsda5iHvTzRjw9@f%FTY+Cf275+|V=YVt6Uc@;sEU?WxM!yYG6c zP~9R!cmc>M!?ShH)ADsI!09ni$=)9vjwjOABr+(icS3jK_OWTfYLT>xWyxpzb?0S6 zwM$*cO_4ta0@QGjmg|AdS{g^qNeh37P26h3nB!X!*w})6KV7j5e$rwn_ zOHTA%fBA2;V0-O8gKYfMab5cX#7k6(_@xaLA;# zE_`6yT3qC5p!nsq8ou%h#5TK_D(5*geyX<5Er9u{qT`L%4rgUYHj zN`C&1B9z>8w^*ztlM4Mq9ibI@@`*(;T&Vp=v+3PcQZA>eMxkJ;Mo7-3R_9zWRXvDtAE;QzY>r-Cf(bSr98*n%jLHNvj6o^r=aURO4fONGb!0YsUlu2J{Xhb zQKE2jo%sGkYGD~ij8<4BpAj8V`AFG>c=2=U>aXm;4W2aE^WiB%o_X4Oj(x{%N;MkK-X9rOm z(kL6P&fWl(uIsL?YZ!<`4L^Sf5hwM*!r42&vU!uE&H1?J;rQn3q_wGwi;L12R@G}w zRcf7-kWkB)yQUsroF)HmHE3P?BzPOYR$-T8P?ChRgG%Z7DZ;+`Wbe)c!Q6ufESGIm z7H~a4%JFt1tFR23PL1ZVSR=APEWdELtuo-xI(ElDj1a4w*7fZ9;(?bynvz4f*Wsq# zE>JHZ%qAU}hB>z4D6?#ydLE_UPRgz_L|IihC;IQ>nJCEF$nXKZ-yEm~VDv*_f9o)#aj_r|)G8Y9?PE+4)QV}tA- zCsR8mk&f!8x$!m;(*k_GCv_4E$G*^nI!|b}U=*>t3YB=f zI27U?OQEnhI9g1xWmRYnCDd6onEo)W3Mwhtzf13{(kq78zd>G!Ih6pxBNj=-^=X7x zG=H}_nQ+6O)$zRHl*4*$^T}1Mf38*h(0t9oN*0Ueg=e^%v~sWJ8Te__;Xf6Rbp|Wm zxeNXcc0khU-@^B~4b!0{ZuiBL3#kiILEbuvLz={~;_|<_a(6k^z=ofUQ-;HeJp`N( zwspj1A1wrKw8yc9IVxLD;wAAMn^qoR$!>n+u}waY4x{$huLeGYzuTG8)DnDcCoao| zJC$W->^ytm%^fc6wJUXq`jPAyCX)Qh+g4MWM+KehE%8t_;j4ha9Yoc%(O!9ir6g5K z6qB0H55!aQ(G8iof^ULq1Td^C-Akz(r7$qynWObYaOvZwzQ8*oOC%e=w2ji|Tit!< za+kB+VmHU2>_8fYyU)3>&lP}cXn+(_0TvPg*5skBA~^gDP8v8Z#38gFydatq`|n8W zwa=+vl}9%)Q7!mwknr~ie~j5$Tl)rV!<3;S+AqKL(%?Q5nQ1P!nS*Qo{I?ahDj+ym z0;yyR*Btxy60AEY>Sayb!;AI0Q>y`atBxm!56$ zD<8jz`)h^e&F@Enactw{QyNMb;$QwfiF!yXHzCfQo2~ZMtKK3YtCsh9$ek}tQQfk6enusuF|^RA z8i8`Q=Bnb2UYfb9-u(-NyuS{HGBF$C$QDWQDoDg3dzlp9Ngwk;PNi@jUDG{DQG6}| z`NLq>&FoV~;N2#V8|&y~MQeQu6kwsY=}Z-BV>t*Y93>X~N*!p89HC>5hFK1i_l`Jb zXvFN;H?%u(wr_vl8FtqXdI`giTwR~-dVJM+QT53UsljPKEE@RN>@kXyO}<}i4kla{;m6CDcTBGs%5398pDGH;Puc}5`9rAA{ei5b<>(M*opV`BLYcRBpW&DIErKp zVDSI~7#N@DO&;OB*ZlbX{SVn_g0TLwAo(;ph0o_v=H4=snDxY4Ry;_js#iz>n2zNS z)BNvgOd17gxMSQpdijY$Ti01rg-Y%d9*gv#BB<4Q_LZ4sLlpsr5~^QsbdBuUYX4Xf4KkrbJs7$JtseaGhjlXCX<(|tHRtB#^TP05HCK_OA~ zb5p?g8<|jnETaJ)N=toT5CAvzrVx0kB3t}O?`Fy<-<^v^#~X$zIJ!BHRkyW^EGf)V zcK7Txg6S+Rz77dJW~=^-mef4t;@P=hVt{kNM zJE$ncBF4}mEs-%3Gd^cVmKs&*e9vV{;I#*Q*sV$!m*qC#Be_D0Ij8XYXFZe2u91vx zSlxkdKN8i3SnUG|(I<{=?Aj(Fj~Jo$i4|!PLR47p>)030@K3_oqHT29&chHK!sLivo*-3l&_82nnWShb!BVf)Jfx|oDOj^GE&8R_s_oOR zkRVwq*wO2CKz4IyOWz9^X-v<+^fbvjtJ2r8-P-Y?rDZ1py=QyP2Z=e=dyhysZ7>d= zzPeF#;J4iBd3#RJqJwR|s!w|fY>og6_QY3hoj18zm*bhZ>T_C6cGx%O8`TSKfr4BF za6Em>{R`-3f9;Mus-AkXz3FjP2_U_j?pY8+=oS}BCsOtQP(>x0x7L2{@V;J%Itrq- z;5duM1#S^{wZpD*7aTreU>mn1rn|gOOTSlCd^WF`ce!@M1!6=~+6l&6zQ&`Ge2O?a z>GU?HKbFo)+k&;GK*!lD6)B?aDBWm&^sQQrFRo_vtp4B5OH<@Bt+ zH{Y97XwE0PO6m-IA%8o~>v6+ed&7;2pDU#TVxBZA&Q()#N z;7%Mkc65#8DRPI_{MDHiL^ASyFHd-hH2s4!L&~7VBxhxe0@%*2Zog&=Bp2qklFp6t zMpNU4(;w9qt=8ClSqIc04*}!OO$gUdUwpgFTF%7vtBSmZHz~sa#n4Y-oHaI(3ZF_u zw|>ohlCR8qd1MxU4=+r{EN0%-S%kVDt-jat6g-?{^62yOv(iCvQMpZSZoO?_BP>lJ znW6-9o1L4R8)*IUQ`Y{HUCMW}*7ae&GpSq5P9>;jkRSJ~nB8hT^0kZziRU#&NzC#G z{ac9Gm@L)1`c%dC#)Mtd#~# z1uPbrgc5f=vfL|5tZMIVZmCHw0o$VCD1km;6fj>(!f7a}QqXWvCQs%AW508Ty?h=g z=;>6BP;8)Q$c@75ynZptgCOMDss)H-IKZJ|5pB`7VEKLXCTbfUj(Yr{9s z2k}<~%Otf%QV_xY6l!B!oxN<~D^k$#0na)D%mD#X2~ud@2WuHOpOlTclrPSxN3x_$ zD|*N&Ves)UH&Kj#SQUkblPLVeccc!{cbzP1F4Zt^Xa<=@5%~1BXREI;~3b)Gkg9XLeB~J+DpQ~E_{L}kb3H+KC-xF!* zvxF}d`gT)%aH9*h-1jK@iQu)4LYaiq?Y*=L<&^U?E@06UH{Kne#r^&_Ju+qLr1K_` zST@%Dbu-FiJl`%G)Oz67<-SMhB>{^&g@bmVlP%JcL~D#9g+P(o4&VfxKB4@%OSd<| zB7eqs>^tuhBR~0{WgI;~7;jSItc9ylPj{1TTX2Eui0B$=_PBzd=Z z;vGH{JEy_nzep~1+L4O+^+!r$mGWh?O*TUxrKJFqfgvtRl5}S5yisg8nP^~u+I3xa zcYg4L_f$R_jkOfW|K&A)+C8kZYxAU_5d}5dg}QfO#JJTdygR)+`PDq~NYV3o5_(Ut7$k5sMRYqIA2@X8}j$6jGg$tBh)#Sk- z>CEEvWdI)2({+Eiymb|Dy&@Q3(>$p(t&y)miX?-@XQ?EtTG$aPr*FdYBFB#>bpD${aTV}#N#%-4w*0Wf45NGUXW|=GTs<0HwuX^@e zWTi5HZc2E6MmLlETOmF~r}(QNmYmL8OWr76rcM6fEV6Zl$Y{B-JjYOsc9aZkB0ESf znMMpG1H=qxP8d`AfW-T8O*qob>6(z4xiIa07G5Rc8+S9|m1_XT!9zR;$rt@ zQAEvZMWEgYL>*WVAGbIFiW@qTM0uzBgIT4q#3X*km$LN}0!1#V*ONwR+7YPCQ6Vfk z35v!P428YIIMjDUZpU^NT8StW32S*Rt*yYp31@vqyi4?2Hnc1q{keSxhQv6)XZ>9t zWl_T}>5yLrxI7Z9p=j(}oGLJ>!Mm*DbPMtsMywAT#8DdEXv#i3;8r}~K?LEwur$W0 zlQ`*rkeR7{rYI(c%OE2W!5NFLJ990d@&MtI&+N9%Oy7be$KJ3CMhPX%td)~my_S!X zNNNzAY338)Lm*i?HtuTY9RK|5MOx08BsH=QEj7=5tg@lpPF}oWt_rUS*Iu)la(XA! z#s!j6MO*4KCa0#8_lsb4Ds2d@q~jU<5LOHfoZu+D3AzbH)VR+;Z1rJy;@)F`rtpac zP`W>HBN?|f{RaP>tuHd}GjWZoGFu7ytuC_MuPt92Ed_v|&9Ae|={2w(P};Q4d77G; z@#2UDo|Y_=%%w*Dm%q^=TZj9i??=BLTK%n$b+)x#p&m2SxUi}>Z7F*X0zm<2aOQl~ z6+rEApjGx#`mzo{4XmVw8*jlG}eDifEbSYe_HsvKi2EHUImNIA-Q%K;$FAo0xORkLruVILzxhpi_3={R<%j`oLM=~Xc(_SV$tR*y zj;>)w@%v+u<@-J_;1FKgI%y%!gTk-RfSwt78eubXAv3 za-kk%t43(OBvOV{R+N>ec1r zTFfRd&_pA#W;%^;-9zc&efa%C87_-_@I8LmgoBJrHzLbmW_rXxH)p zlxjia9$~fakBT%7Q9ffyZxr^1i08gO<3Lv^CsyOq=}WVXm03Yt$R4K<4_m9a|@n(S2Nl=^%N?=&CpV>z0`;bJLnRM@13rn773V z9bDNobM&toK=mG>+UZ#U(TJe3UW!Tuqq0Ky{rmp(7y%=F_z?mTdL1OIp<<-cqE@1vzmV}KRXK8 z_PP#Ue)9K=OzT2oTiKK)nZ`7xi4eA<`-C%Oq)E^9sB=0t$@S-KzOY`QsPuTc6 z_rGty0ssomU0jLpOvZY3OY5>B#J@!un7{9a|JA2SZ8ocbe8>CwD#WyOZSvCPm9)p{Km9iv~mArE09;^(;G z@X6l|U!*^v ziGef_iugT~5t&6~rPMPqpBF`tt$2|l)^8EbNS8)br~Tm&6wnY1ms^+(m}Fh2`8ZZm|t^|x?qbTu4ke2{2qEA@)=&KEm&q1 z(SC7u{-m$7(<;=PcD*vkA6udWJ03)TWV}0KAO4&I2Q2!$Y6Such~6(gi*SLZJYrV| z(OS#oOhhSgLTr_X3rbm9_@jjGzJsMp`7lf5o(Ik3DYDXZ_Qm55J<)Fz{#lFA z;)PqXQq_4gZkjUUrIZu?4K45Q<{+GH8}TZ z8ic*y>t^%rcTTZyUn76C|IfM2ZI)eBw_Mk-;@U#|9U&4Aryrh{8UM@=Ge!4&zW>*r z)h3nFw_l}|P8g-`ieDwp^U(=^S0}e3?beejxS>6ph`N!=YphqU_Tti+%i^D%Z4$j(VTdk+ z?BUYlN=njkkFJTDSvbjUQtrvqbEX>B7byz%{k|d$7rCRT63DE3tvnhp2>L2Sl3XED z1Z``x0;!Ggbk%6IY-n*S=qLl4=vg2dQG-9c2R5__p!`NXdrqd-!Q;SnPWd&cwcA%? z)Y)JN*y++xO$Vt589<;a1wpZ60=79j*?1ndo?yai9*SesU5jbY~f;PMqeV% zv!BNhjMkVLdse4X4^Ly^xiy@%bPuMtbVmp*xinKje`zjAF1$g0>7ZjCWUOW%p8JPW zGOqPf&}5Q~a$-JQ(~v(w-9>m&8-Y=!3Y|O)A^*pMSsmLezG)n_pViXYxpt#4ZctOo zH335wM!GfGw9Wlk_^vZe@&Ezy-{&5)4xRfC54n);vK;=O6!ilzL|{sW9OqLjM5qr~ zn?@NZRftmi>_P$n#tg7sA`>r7BRLz|AKxCu6MdAtTy-Hgm~}8=KB(5s-3L1W9aIb9 zYv%K%GT9%3c3cUOkd*c#pM*6HRM#`vDWbxV9k(~w$EkK~;n*8#eO&K!mV&O~VPcV-Fx zHGe{gj*_{e(%1oeOTy#t+J#6wD*Hq}3zT{JSy=Hk(7)ajv(*oluia7g6HqFP8O4oc)L{V``s*t`*->wyMgQLML2RCS zi6>?%UH@hQ=@Xq<@B81*r&?Gfa)G~j*A9w^X2}DIoovjF@7svq)4v$X!vUFE=x31h zo-duPAxJ*3Y%PgUnVBx~;XKPr`@A*c5O*~dA)z<~%X+q?@GUhjTXemp@PhH*Kj#5e z@=VBlq==zq7PjPfAW+dA(e4Xt=Wgg-Tzrl6eseANjoDXqsI2gm#5Q8@_q#m3e0ZGQ z2ko=RgdptUThPGqXPcK&BU4EU&*V%xl&MLbj(6Sje|A|{q;Fxqg+vcNCwZx)34#%@ z>ehx1{w8)356u@@ujG1>l|~$sSVMOS=K9ppvAM83xRE;mfg0S1Umpo_hhC*iJg!Tu z#&tc2laU3!+Rm6vef|axuL8-7@2-IMI<w%JyTI_*O{{KqWI8$*{?7+LsT3?g$)Wb>W0DEn*CEdjA*{ zHtj9(gXwC)*&ClZ zv7Zk~HLh#-oeL_U$6~YaxVGnxL<)DU-SD+Rkv=3_hm-Kn$zozV%3NxU5Z(AwvQ!iZ~dnV?RdJ+xk=A?tPl-24QVZx-4pV? z=R6JSS=B5Qx>Bo8Bc<8xH>p=oDAc7?C_u1|P)5aEZE7tvqGzR*?MDKUlMQLpu|0}I z6cvJPizztrpx&5Tjx@_WMqDP)W^sHTy$){C}{dJ!$;aK3}~a@s>jn@N;TS?0G8{j+!o zyw@QBz!j{!?=44QDAQ{r3?xN!87&u*`tVZdBE1_~eeo34*HtA#rVD-_^Zf+|Dab}-=y;y$AoG9EB#tod# z$_^nb*?1QYF- zld2lb7#Cf=vUP$PSMy1$dhO=1NJqWQ1XA8)sW5^;j>z&x-3F#jU2D2c5u`DOVenKN z7!0nEkXQB*Yg(#LMlF)lcJuY^pPm+M>tHA%syotBwx}fdY_y*|_f=m&txcXe4yh=7 z@thXO7DJHt+cJO;4(bH%L{hyK#I1<-bWvEMCOuH8NJ@3na$)4)*nSEpzH~`FqrSoF zyS!aSredIN{NxN}TdM@NTh+GtfMJ8kGk#_*&*R8~<(WY!bBaK$1Ttrb=NLF{e){)l z_8G_bp3D-Ld~k}~_sJ9eA|W&JgX`ZI8;dGMYB9NzVrQc^B?-ejXzan1$vpF#>QBma zVIUT?ZP#uugWI7G^xNzB{4jQL%+)J1qRhJI>`Oz>Rco5UoXQbKe5z1`1 zzavyG4@rny5fqre`WiY+%tFj%yXc-ar;?<@5zasKH3L(eSB*t_M7KdTyhjO#p+U&e z*u|A)ck*1HBgWB)?VGgfMAB%QYY>lfld(hn5IK4b4|+{NgxiuE&a!~ma)m<~i~9{8 zn4{HsydVl7yL}_$5zlvObbJfcBKnb(vpO+7CM~UBEI;$~5YEd0R0AQ(qJ2KH4TY1g zzoSL-A%CvKli4o(W89%tq0Pmym8-`#Vb=roLO=KGX6LPY=Do_r2jquNIx*~ z!8zQG1S#;lkqdY)Oqc!s+jBUzZLZy80&596IfPf|*@t46{|cf_*K*aC)fMhhde+gp zmP|Cj|7=jp>%;DNF9+SP$f6U*ZlMSNIrI|sIkoO+62}`hQqj|McWR?7Q$Gvgc~i)x z!yZZ+tjHM2*%vX>N<6pFeXdDwA$?C}nN$sI%_lv%YMR>uJMsL%RXag3y|Y}lX;EIG z-1^(tf9O$EIllZ`E0f7nTZpMFAi+nTe)ZBVan5j z5jf*H65AbDonv)iT9PmB2wR5eI`{&90V78Ax-4=^KrM!+uM3DCSlq`TOb6&6-Gk+*Qk4~j@}r|CsvR>{>pts$lZJ(!uWKl*eq*fG4o)utd&@7oQ+SWQS3Ljwr)+~g{1My`&WXuR@uqR2j1_u@)F~aZIcN& z$tV+6h^wtB)ztWEM~ApDhqR+b0CAf2q!i_*X5!txJWFUu#*@%99BcO3ywWs(X$Rn6 z_b|5VsG^)hEpWqA#WX2aGynuwikm;(zP;Wwf26f*i#gCU0^wM;I%BngDKMt=VrC*C zO1W&1_k=Dj*PcqQ-`LgG(X^Y(jg7*r=QL6`(nTWjauUJY8%iKU7Yo@EnDK~Hdto)DV;{HnQI#fDF5^>q6n8- z>yJ|~Yxn+jAO#-S1%>n^w?E|M#{6k#+6cwH>U;Y!CQ7m)kZi5S?*(^+R2#g_q5Ni`sfwDzor_R&jEWGF0{@tdmxYeEv&rEyKbn z#n=q+th6(b5YP=8*rD8Bf6~B>8H-bWkpmO_H*wWXg`C$e$JPLUY+RHL&XhJI)5;X)*XWzUK#6o(^A|*ov0#|{$-{y zxPf?)#scel9|&{`kHG3I7W@ZSU;z(oT`(4-eb9|bI(WVLi= zN!EpxV4`>0ejL#a@zHic6&XN`Sd9$jl{y)*!!cJ<(w+riMJPD?n{UDNEQzD#qTVg6+6k; zIRkLIThVM@_su7lR}uFTE7{n$aTXCHML}d&{yG^PN@K8dj1R4y01#e#-{V0mqeSBt zhXz;S@fhzZ9u)pQ6b()ZA1o`$SM9^(^u>Hy~7V zsl?Jb`pD)!%5K+jOhc|gN%H+Uk^^)U`JO>d|k7XxBglhU6yY zb55Q3h9F@?<(CYFVhHD5jRmdSRe3pcqDYc#lpK4_0EYB&VVbh6Jy2Oe>TV)Y z#3cbMH&GmY6T`9(M9eCcDr|e@q{!Lsc><+k#7xTi38SirLDirbp4bago>(8!#95}= zdM8TxDhdTC`zvu?^)(sK#!qFY2vM*f`JCfh_eER>hk=};YtB4gVXIvYF`(>uI6FIE zpQZ*%TdoI_>C1rvNl+Sk`$?CxeKN>PpsnS0RC65nqDu3!d1q8k%Donxu%fRo|EBcD zJ1@Yq1p;tYm)5fe;HJ}n8Hc;4K50pjw1+--ST-rF6)yaKp>Fvoee1m~?GOz~**=yY zqit@fX>em0kRf1ZXjP8F;{l+TV$aU__sZ#?iv(vDuhx3&?TjB>;^&a)kVCKsQlkhe z!6*VVUuR$onSeJKw&*-OH)mAFQ?Bhsd}EG3n1z>(JPFAGGDS$BZf(lpl?~0xK^RT~ zY|7vFz&qjT<+W2)Yu<)_1si)$Hii-y6lO*uU-1(9;lWbAp<_joRj-ok9R9|u{_PEZ z(>EYI%8HgA#Fv|l?hZmDzKylki$d<~vi@M{?__}O-oePFGAXN!8+}J@_ciTFXV+8BS0XLQ}!JQFv_8y%TxZH|g=dT-kNtdqtU}-uc>>B8NWmvMfuaQz_s8pcm3( z6$7b>Eh$86?Qc9|jOujSzev0mtqM;|oyj=#1iUT9Y?Gd0i)U^KwfzGDnXY-+<{cMa(KMwy0obpE}@4&ut z#?TPbzD8qSe6>{l!;%jHdnpQLkKQlM`rV%+?mx8fscSSR;P{~E>WP^fn(Z+2O{CaR zRcu}P44#RBZys7CLO5tNYFV7&YFsufdD2e+J|2!z>Ac_MIomlUG-;$_tV$ezxR35#?*S`s6(ZQWqffjYOot&V655vNK6QzD3$i4-q7UMnP-4 zB|>&oXv*9JgYQR1tB@VN$U8jpd=IFPbx~q;T$D9s`H-S+d%jyffOi+X4Xy~`DGHek z8#1hiG3!OYdlufqCDK2gX?r8gWCOPSjZsk4hadA`DxP>9#8>&#moMGXCfJnH1;wD< znl$c}#GRh&8|K`v6=XcZG;yCs`1x_4S>1?|igohxS6*A&+=7#0ILZ#D8&#A zriA46)!ylgi(5r*akd=mO}-YV`C@Mr#aDdl3_YLoo!SzJ8IVuFhWPEYZuVyQ>*Zm} z@K(rxqN(HZl>^q`shiEuNj~)#egk%|*fuQjAB}$WY;2fYN?RFQ#ysp<)w2!aR(Do} zZxzd9_t23@A}D2W)?m`MMQu@zr3Vhma0DUyq*L!TZ?gACoyXV;%B3&+m|*bxrKv=l zvP-g$hSnLbVPj_eaR~#K(bt>;4Geuhc`u7F$u!v!j3@@vE0r8(P6?f!zFs}6yu50# zB=q0*3tRjuMguQB^+ zWlRR3c%4@SM_o^1otF|%8jeCKX$rTiiqS-%FO`r#&O+8hca>DqFaN%dRZUD~F0(G> zaj)CNC*J*PMXexyyTgFn&#?O8|PSIyw&1&GgLCHEeR2W@@_m-k%@7e}MCN;6CsB{d!&3 z^}P1Vr^^s_BNy;mXxv5%3P-`EQ8|1#q-9@#Pi?3PCm$HF#{ef%hjSu7uW;;1G@&ff zWc1vxOr@^tCuIjoLKX$9jKZ%3!jR(0l`;XmHj7S~c>zl>fs79*30HHI1* z^>aPWaGg^0o>GVf+!3FPjRbt;;31uMTzZ?M-msRZH9tec-h&L#TAspKkDtbV27n3E zk@=>ZfypjK8&8a}-1K4~eJM$sWxMaHdi{KRkpO6C6dgCNj2|jZi}h%axcPOjL$0 zm05JI5Th8{qc|d4gAn9$uOs18nRLbuVpRsF%!q;VO%M3WyiTh z=3&Q&C%%je>S|eT_P-r7Uq5+T=^dJS{5_R1r46*2GLYAcsl;l8|0u%iMKiu=R8HYV zkSKhcUg9@6+G=MH1`q$RC`yQJH5W7Af~thM4E(wzGJ;;+zs=0d+_o*4gXW~XAe33F zoBA-@Ty^RehnH^6o60La>m=|!?Lfm?`Xr=vFPOg)!G%s;AHO#R58>>`EBPT$f^GqR zVPeH6UuRRG`9R{j190uH*PaRT#eBXCCCFjp53>N5h&_!*il(5r*RM{MPpIsK8shYZJt?-ZF@<1A&Q` z>u97pR4>l7Qv9_+*~7Qj2VL2WNL4?Qv#JkD@6=>2C*9L zidfbxHkhQXE6($GbHYf6Rh4DX<;$s8o$p|*I^$e5ExCCGmL_RJI~EX9n}~N2DqP*e z8XabRtnTX!&H@0+D%$Jzzp`Wfo-=D$Yf&xZl9B%lR0co2vU)|CH0@k<@!3;@nGJUN z{gkh?mrb07+PR3>aUkDXJl5=+FFt4|kmGcEj$2pLx^i|U`q9nDC)zCn)MFNXeqi{z z_xwpR@?AO-;~5KTzf#s;kAe#OnUVW7{rCnHU{{o3d~Ue)r}Hp zL#E^VQd~J!r$90>GeAn;qaqKz>tVLmm~Y{8da10`VVXO~@Ekk7$-TMX9W{wlT?yc>KY-3$PV9K0L)p1v$_>w(sHJ9yl8{rbY?*`G?Rk=mZl!NvVT{b7- zbS2-}pScAd-dwj6uNiFtzg6|-@sHO$6!eJ&T?3uw{-e~7LDVS`Sl+71q&ukiwGS^o z40m0q&+)1dsV(5JU>=XD14JpLsw z+A){@N@>uq-81%mm6HgS_>v-Rm<b*5OWk) zRjqjuiKm&H=k$Yh_By+4-}`P(wCnt;v-v20ozLy7C|!IkDVCE!&W5}*lFCKMtWs@` zORee{^3Wy+7=U~FU`7?aH~sxZlA?Rbws$UG2LzZK0Ix;vEUZ1iB`{$+qJJF`NewKi zG9I5yq=?}zdT5GNogC^5vwYy~XeZ{Ee#)P*MR)BSl#ICFgA-8Xjj!%`fxjE2t@Fuo z40}=5Y`3eLxiCa>x{}p?C-7)KFh1y%>&Ywd&NdKM<}8$#AOu}>6oHk|0ymy=gzT8n z5TDZTwe@2~Nvxk9^i-(wuSIDYbiu4f(nQ2eFAYnGNG&T&2MVmt{+=U{H)Ay0$^i^=Wo)mL2z834V=cuntc7f+*KLli>5$nqC)4f? zlwBsMV+9O@XIzrRXZ?Bw8da+2g%Y?JRl91_n{K#S3Y}O3Pf3E3&7UvLccJF@a|=Lye7>&Q?oNYtD#|f3K~}RP?)QKx+v3yj-zEf~hwdv> z+YCyoGxs^kR(&5(;c^@Qu9iOnvjq^MSDqZ*2t3{m1}88lu5H}L+Llu4+YfPLZ3KLv z6t+g%Rp~~`IWr5Zv5YIJ;IByGJ*Kskp;b!^t~=|AEHbBfS#xpcXL+&oM?h^1SPw*o zdQ$?9Ocy`DHsq7STR_$~GwJrWoUA}o47}I_8$rwLQCS?B)c#duZF}1BrMsPpu~e8> zCtI;(8${A&vd^TJ#zZ?aY^%1A%B%D0(c=wNV!ck|8E7h6cRW!REA4ks`SlZO3IBjb zCb1%y2I(V}S&34J;^1kD8^*iWNpu5+WJ3ua_qmA{dEE>~lPR0B2ysuxPsW z;g#r;I+jw+(bykF-U%<=mf~6tu)hDlz5<>E-s^_cx?S{XMmPtf2J)3C5V5*iJ3r`s z!nw%_+Wh!$k|e)z?L^jy4=Zj~Gq|Chn-ig9G3yb2$Ep1S%5X$qrU>6UckPg7qo2+k zNTkiuj4K`ZR#!>MqWA5i1-F$I&7zeZOjS=9;Bs>TA9Q2a<-;+*epz?V`!Ra+@IJrt zy<@Afr|9XcS=QMCZ0j%djqQL;9lffa-Pxa4I^qz9uN&$eEJl$4n+f@mT&4%!~eLABkiTNaZd}Kprxg==IeIfQJy|cU@wf53QM% zXy~J7UN<}SFMpJ@II|r%kR_0TkTL;R8MIkG0%^h>>7*iuc@IR|USvp|cRkz=K(q^A z&fFXNPCWd92H_z^0tX&S7Z%^Jr}tVB8_f_F$g%~GJc;*U>n8~xl!w5>8X(NoLL0CN zkt~Tok?GP))Gt#K`Ixl!+ZIo#wSN5~kC$_yZ%t=Q_z9u;JSye`o$53{M6f$>-k!i- zQrw1ml~8r%uoJ-b*Dk8cl%!pU;h)S3(Qjchupa8KqLeP!e>+MzVTeEUtC8BnEDTce zCEV4%Cgr1iY($%?**)sBvQfB}J6Bf;%ge?ijzOl>$2f*#A--N;Wk`7G)5AEASYyF4 z%gYqlk)oWOlfsgcLy^fYLeowHJQFMwr@wx^^%C^zy4(Wg)NcMP_}VE-sKXLXmTNoh zt*0j!lP^KoDy!~p%BFV1K7vtf`Sg%do{=IAl51N866rVJJI!f!hs89oWD=zbN`#EY z+(%*^3!POi3L_}tw-QvOL&+TuqwmV2f(vY2Sk;qrq5ett_HE*pZ7b6_o1=06XmR}? zRD&r1=62hSxwXVFnrNJjrlK!75oJ!{ zt_yr@a&DCHWZt>72(8AB`#_e7aMglJ6TDPwDhoWo+>*9B2syVVx09LZT-uRQk z6i*NVSgHmQ<=M2d3QbDqBZElgD*g@hb&4nI{)(fT%0SN^zbh^C-g;x%gQNTj0h|k( zTMA2xgnL=SHXw$K9GfML zJV=^^yS$RsQ?7?DNFDz*%i*IvtX-6H^rv8o4ukBi7;3#ziYq_%8FV_T9KkA6U1f8L z`T~gnf`zEh?QF+F8fV?d3MVS9HcFmLJ2P78@jm@LTTy->kXv9*C>ceE(v%R0o8(Oz zA-E%q0hq+8Nbf>ADqb5axL?w}nQ2z5Xwx=Z6o72yMQ?NJ?P=><-?qH)aDC%DOjoqm z@rEQbQ8^4)QV&nfu!IH7FOcicP*t~BF@h1RQ(|1MsU9^R%R}a4?q*W*_RU1F70*~O zY;3HnjAAQoUj%7=n2vrNbUJ;$yOj&H^nsU;%3kmjP)qnR?dr^??O!zWypqY?*O*uB z8Cr;I;Q;xR`LK*H&bxbUsnS8>=@}OsDT7W~`ug4|e3|GENPSW2Zk_arhUi=mK zxZg?<2W(ytzj2^xyNBXKv&k-$`vPxCNNi2a*Ki*S9tEDe=6I~el_rNeYQX~Fc!Js) zKBw>|bm)|(GI)w$b}Rc)cID@PZwxn&{&VO4&7qGCE5~2E9|^R>fR?7FN5)OU5yJ&j zje;JvWlt0H0<%rgB-sZH0uNF@K`XKY(eeV$``E3%N-zoLLokD!9@ z=Hr){#8x*URg@iI>9s_@%I^ZjQ@V!lz#N2`tEgGUlBp9w_Yyy_PkP*!U7G?N#cm9@ zQq<}uhi?<9L|ctxd5pxUg8NUSToQmv`>5HEQ6aGkM3SdEfEIS;|Bo3N6Je$4patvJ zzOn(4rj3%)_Zc?4ATU+PZk z8l)D)`jTsmP!%hM4+7b*=19Fah^z4jxgstmBZm*+_CH{LHEY+sCG+Rg84w4UO_9!E znF;6Oi@!$htFO~}P^Tu2UfbKHGj9s#JBO=hy@~8UUHwshh<(154P*rfG%LM7rA-MX zq^JaWdJcMaT96_dZY`$T>N6!js<6qiPmBik0%5OeM0-49FC6xlUCmhM7dEuIlrfAY z8L$tJSw%b{e*Jn47+orieb`&%;eN{YtYpXV%`}oC1JL-jRKK>AL}^999kgrF@n&M} z!Tg5#d8^=GljJ>b+~&7a@1$WiOFL+L@8?W(Z}xqXT2~g?dxLo*j}=VlD7z~z+bg8u z*PiLY-m?s73EGu*)Qq#@o20g&IO(M8xHpGh9?^)fiL%XD^9MRW|KHqi7IE(- z0{0OvRWuO4_OAN^UDU5`fNDqVeXxj+^Q$MsjOS37^ECX%oWmlpR z!j*yKi@oQHXLs`A{-N`It25)gz%3r_U3`Sa6BKi%31!R#rkj`fpO;hfekr@27Ir@OK^o zTWq&vandgb5v?^~YZ1|S-dUk{lWd`KND||vq_NH1_}@$Ppw2OM?=UQ^(}eYtb!@V* zDIIg%3(tT1?;OEX zh%(ny?ELp7zELMrNOW==Lq@x@zcz!sk-v#RPDX92;sWkWPNcd{ejGAq>_v&p{xBai z1QS5eI})~D;BR=)b|*;F1>CPQ$b6?$%PqsJ!5XysI|#|MgyEkvj8JY(6(5arJ+2y! z8EbIXPx4@wlyBvCuxs_jQBJ>&lyb?z`NWxQNOgM&qF5|^Db6zCY>TT5 zSJiBU-E3rXe(e@do_R|h@S)sdq3I@A9{AhSOVvJ*U z_%YC&67~))+%22b38c>3lqj_O^{Y_1Gb>?nh4N>QuG&N|j(QT-xnjYp${5Ow+ymep z0$kUe0Kib>=9{U4AVodHrB)O=ef_CZw^Hc!PY*S(*8cl%6B8V<>c*p@o-nMe$VmiH&t)HDepx)aQvVpDL_^*RoO}bfzxTj@GU`Le+kqsoCnDRJZ)#3uT5YE_(tV1MP@Ev|$xpYbH#5$}o51hUH`B)2fPt|Jv>`(;tco|Gt^l zl{D$#dpQ|{3eDf$D58r{HiBwT*AC$-3UB7Zox<-#?|srW_!x)GK%_%H{M*fQXUr$P zQ}-vmT&Yo#yVnoVnZ@-(R<2ymJJQb{lSFqxbole2JkIN%JndDn$wk+Yd?s>FpH^4( zo4{{+M1aEsD_!$eqnrRNw=_f8=44Fw&&Flu8Oy+a&Mt7B{(Y z{UOjeb-O{!P>?ND4De9cvKNMU_;5Wnl#sx_{IW!3S`6}+Gfyn`B(5uWv(YO+pt6(R zI$ByRPu>6GL3s1fTqX2DWKL|V3SoI_2u8zA@e}1y3rOaPa^$d$>dUklE*?KyA+(wc z5FAvTZO+X*qP2Zsh~E~Q(A6^yRtGy{yrbhy7l63 zdC{y)=#X1tt}Dc}FhK|G@)Qw0BuX#=yAsX+_Q863yz2BomCC@^#nX$Las(j|{Q)MK zj1^}MQ(@!~T$DNKW#QV7&igM_;<8;t@HP@BvF^3zz7O8sK40zL0`F8cU}5kqTD=I- zmD@l}9E~?6uI?VJJ&2zgfti0W=QicuQAtkGsY$%S>PM7`?y{Lbk(vAgsB#(~)cVCV z=)yUl7+-%YOk<7ya7MeG`+m-ClI(yoFFCC0$T=37>12N=yQ~H-9DtG?nj(<;KFI=H z=kzxsY)CK0Ma=bY;`pqTa!>sNA{5fQj3-^}WDKl?q_eh{ignGb!$DK8O?bjt zKQ*%bs)qy@q$Nvah|xay^+lzo$BFR$VfS3_+5?P#zRrC2&!=pE2CGq4RrK&{qLS1; zXGzbdAoclcx<%~d+7mc4ePKMt-*a~QDa;PftvyA56VJ}au4mB`T5rM>Ok!7zcbnbA z_B19}b{b~;mkVOq2PX z0>FZ3X0+6Ub5RIATBIn7jNiy4;-D;jaetc%?I9kN!%E!w*X`g<)-Fb~gXNRO$flF} zo-3n}ZgT-Xl4%mzb8_lTBQImf-tKaasy4Z@d@{o)esBq^tOYgyXc^Pyi%S(_DE={6 z+yVheSKV8z)RdHk&D&dBVIHhWG-?n@S>&1u?kPtBov`Jycl%Lu1O9dzv-w>t!?F0q zq`A>5{zH4dcRkzx{;`m`Srb}^D-$qeKal|347VPV!an75nhBc$ZpGg}_V>y3vva7e zKKA-b=I7@AtLnY0^8Y(PA}l7hv9aMlCqv?+ZbtZJIQLGIPZwDDG9(fEAcXLdV=X+TGeL1%fJ^NE%vU9;@5hrX_2J{==Myk-LTi=xRF*% z*0v?GN-TR7dLSi6BLH`XL7QaTuGy^5*8XyKt6Zg#==LaR0Ndy^aw(z5IDPS%@xzM&|>ZaMTDBQPIT; z|1Xj7yYrfJ%-VPD6;|#HW8-i9&l-i1FC^y-EOgQ26oZD^rL;0Asi^ALV#^lgBG0rh zNV4~a<6Gm@Q|J)QlS#W7E{FS(*mUf~nfaKFYcYgcp!#eiqP@rrv8FF_@!Fm-WteUqB|C(?q3A$73l zWLK$qS|A%HLKxD>Ei3*8N~hDjd$9&akINYME2+*hV%s5|k9JlM(@SALT+RTFT5PTL zl5d@+^KPD=oN>I~3=@NTpOiL!%JKYaL2+ucZTK?Mh`eipOj;T(AMK7X7@I!jY5$w( zC}=FdyU6IK5e{^OOpdd_3H9_E4UAxOGoGRVjx5KCxh3AOdAdxw@A828VYJRaK_hU3 z;JbP~nI7YmLW{w_?+Oy1V|N|fa2npvwwC|y?8W2eIy#AoD=9%yPZ&sRP(;tgB$Kz> zFK%dO)EKPa6q-EUl<=-@yx2GVsni&$y)ce(K(zH6#8aD>TW?1Fp}B9pV7Nz{Hs$l? zrsd)+Zg$=bliHOWamRArEYnGq7?)hD6V}iW^H(?kER)XFc0?Xz;ejIdwCs;p+lMlr z_cU&}p0}$Gy*ACwm!EMl$`pcQi-!%_GZ)4;x`vouxvhC&S!(G0BAj*&T!k~xwM#Y< zJ})QoJjA|>AeQisY5dTTM7t=2N+lt-x!$kSn0EEc^KA+B>|(jiPAm?rClkXt$1ufu zRO4Ml!B6uaq+ z9gg=-w(Zx;!}H%=w|w)pD_eu{6qIk@LU@5|D2n>jG2VuhmQ9+Rx?b&D$4bpB_qv#2aA&@V05AsU?NyC}t$d7=ALNZ6ArDJy3e`_IF31>AZ&`3$|ai z&s^L>qUM%wP5D-GU_kEO?BPHatvpjfVg=}Lhx*jUPyUva=)TojiT$CF)OvpE^8#t-f}2JNtZ54m7o{OMW_8 z6-Q+bJY!M|y6hP9z<$#*#ppfYOwds&^TYtRz@w38B0vuPOs1|yf_%%g)t2;QkLx8ZT@Fcv2 zE+7AOnYRU0%@>`%b4B0?2N~Z=KrR2lPfkxniOusqk2^kss^+}CeY_DX-P)iARJ}b+ zHFGE$moa~TF{`2Vp7GJqhPxvRh(~3 zj#&~LMQyXeGYS12D2?6+f$Er{l{UcsMpOCQWdMCcfzB3()g`U>LIUf^Wnz4>FG!wG))ZXJJ)G>1w@4jsT5@N1;&Z%?iz zacWokqpR9u;Cyi*!!|$2B5dw2+LW7n4djyl>!7MuLrJ>4!?eV%21VKNbpF}e1HF$` zoL{w2yz{farJBH0YSf-5GF?lymU4_jJ<|?;?C+;A>Gh1=ZbbMYxrT9&k=`M}-wZ_x zdLL(NxE{u)6@2RXE^5c*C zMG1iV(Wp9mcpZI8ymy~pdq98zQf-({r0e6cp`sK}>ksVJ2#)*iEkn#FFeW#XJ$~Y7 z6Fm(Wfj(Fpz7{J&f17CkNN9Bv$cE`O*cp78IoqT(Ch+!yk=!?oH}eLKgz>+P+E_>~?7YTGi_7zKf6V_OkWdYhcEwSeX zD-L7^`@q&;XTs>fgz zwrSDKXo=Wpc|U*?zwtVV3{J|m*61^Nxb1#nA#w8l!GWv*i;Wd>$|&&KU* zMyl-~mJIw)(nN0~Lij%1eQRF*WbNn$tSo_03ovH5<>Ux{{1oV-?y3n_nW0FpRmf0@ z%U4YfF72Beu}efXQ=PWde|!2)Nz6j8D9vu@UOkTE{P+4})OA7+P<@>tHuW`dQ{-h; z&W#qnI^<{AmfXL?rYUVb{(jGMq`JXPV@}Sa9ohZSwO>qE)(L?n*2kVp;^(WiC*z*WrM7>QaGOa<5w(d#tAX1BylfVZ%-F3q z%hSLZGph^>dw#|v#I@aIIbhMJBE@8+c_*@lNYW$U7%}ax+`i3+@w)uQv9nw_x{hM4owP;vl!cQy8#dPhOPc1d>AZ(upi#}j& z{dd&#cgkVq#he{G4b(0Ci1*NWf64i>mN$iV1{1;`d`xv2qhC zH==oH)Rs{uFX;*`6=ut&_7!H3m`Q`i%}b&9C-;T_()lA<}-=hQjJmLz3fM%RN8Me0Gvxi#_WX0*qA%QoEe|BhS$5{E`wdm>cczdNtLS zEIXatmh>&>5z<^noH{|Mr53fDd-K+Dp12bm|Hk(5h21f(&y)G~0Ey%zv&1QH*dNm0 z#MHdW#~sLmgrugH%|)!@9{fM9>10eeGsUhi%5SoFCHpTA1c#gADovQFW3X$)Y2zx| zvD{eow6+%5Wr&a!$((djF!14&12h51*2}de)QBp%EPi~;&SlTUEfKN5>6FT{GewY; z5joG!JcdGqVumeY_%KNeo+JpV1U@II40G7+@~#WNFiX!$%+GiB-iMm+rt6!*_o?O+ zz^8TAY6T@g^G4t6(Dq6;x=ig>TuNKp-KGcilx+xP#TP6jKT~n_D(;gdVO>lQ^Ozf^ zF@rEHJ8~k4K|erY&2R9Xst>ckLu&oUr!R9F48#9W0>0t>Cbi;`32DHDdcb&8FBI)d znaw|wW~(iBOpc}f{_K|Qf)QuUQYo!ilkQFb zj|PJbO6s?62aC3U(9+A&wX*=o?e{-<+?)_R7v~SXI5$5^q716+U{1yNWPg3tY}{fT z2U=43C9mD6H~(t}ZCrNV#z$S`8+1uR*L=;&%sEcXUC38Lv2fyb)Abz|SIi=px<$m0 zM2kp{jpZHGvTJ7vZ6K4g5E2u!38)`5P&<_Cyb%)HhZNU~?nY^u+e1pf{e8K}cV|I^ z{svr9`<{{-boUM`teE^&A7Jm)Ybn$)`dDOx`NmT9L#SH`p`VYu-Y;4Alq4*WoD{i* z6WywCv~@G}q=GAiSuvTNqIp93?6>sH)0;;cm-JU@*-YL>6qU-VGijK={jT4(=5Vmc znZEiZE{kZQBl9G(9tjb_Z%3pnwZzYEiOzWR(=U$i+HgMJuXvrD*F9fmdjGA+`A_Ki z&|qL{3Z*f5{ci2?;X3`Zm>P(Hj2zDAQ&i7XLM?Jv z1b14tDzw)QWwA+|(~YzI@F`!6>#N5Bv9;l+h5%HOZ7>I%P(eY#?fs>@U2I}(PWg<) zz^YC5aB3OLJMd(h^e4)<5<(j^Qgb#<>psCBq8pvhd`-i`LDNpXLix_VNp-;|Dtd7b&1!f@oH z&>Bt?@uODlisw{j?*mbOz?E<#`!3MkVpyr1Wxko!xICL$*+Nv(c%f=zs2&HwW}Ql< z9mBk<1tK@OfeRK+gAOfBQD-w3V4N3>e45_FPH^UUGe~#QmYw z)B6I=kZB^RA)|16_s$}B7QO7mI1NqCnQUcP2+J**-4Rc+i9Dic_u@i#M!?(s#mr07 zRiLuvt?7Kpk8^|;TV9&oh~Vfj8?oC53pb+hpRRqPcSRZ=3W`R)nKOl`sxUa7R#rYM z!m73k-ypx7HlbRA?Q;YlI2{a=s zG1$6am`(`^!OG};8UF;m#|o8OLRwtEIlxMsU;?(jE`e-Jld+vfnb@PfHo@6n+u z8Dmh=fN!oY-&&x8K7a3qge3fpU}L0PSS~4P+zH>QM@W3K2i)yW6P{)GpA zQNw0gW13c~e{ABCdANimBwzs_-D4VUZBk6bV}ndokbG+mV;g1Rvc*dIdW6DYp~c;r ztYHUuI9kh^&KCTTgOQ!4(!^Je$NT-_J=%QZPc8_lcFlWRIxL%kc%0f}q; zFrP1c3G_~)x3YG*6u3d*iW&V5rnA=$HVjsVSpmLhU4QoWa8L3|b1ze}Hd3;`UTYGE zljbDS3y^4hsUDtDnX!6rP~~2dl~#g^nAaGsy)7}Oo=n)mtFF2}(S!f}CF(=jcY;ny z?_mmYQa4P~)l1M0Ar&uWzgH|hFe1z^9zLD4bRVsyB1({oONb)D^wNyeI?F15HQ>-< z;|`{^enX3KCm}NRqs=aXrHRqQXP7%~#BN%9hscA|C3V&3bK-^rwQ;iJO+G2wFV4|8 zQn=Q^oj3^vN}$TWi2l$R=sdR1{e;&iPHC)-+oWCO7YwLkO=Jg`U?Z~JlhpQ6DH7VY zkjimxrc^>~>Q|h&pW;B@T}Z`NaYzpuS__Av8L?USS%eiO|K^=0W=17F_0p4yvS@Uhee$c8gKh`{WdO&=!~j zXnj5Z!ns@@JH8rbT8B@IXjMfDPgqgPpA{>XRf*FMRKJQ}fI7=b8wn zyvw|f)DEGW0;OD0AeWby^WA>~Mu!FyhicFW&&2dPE%p7vkO8Ou@6Mp0j36-=6{W+% zYYTMpA-+?zVV<1k1c?C?OdWaz4dHq%0q^&Y;(s9)djNNSrRTpp)Hc-okrfCQfb6b$ ze(k(mvCD5}E6Xlsf>t-EP7lnsq$;%mUFoE8>M%EC%NWB6-Go-xDP?**ZV`Svh;k?o zv8DK=53Dv*siVTyCJdN4jxx#)K1qnd4%``~eh&>Be*`vs50fXKo}U7qPJ^BkZ?~rc z-&<+Rg-{Hj>M~v>^7+vPMfLGwmjr)nFRi`7C7odLlaGq)|K{8|K(5klz%UY3xUz&P z(-6nQH7j4LE-{+PL+-?37jCksINNfVBfi9vaR49ILS{jr9zJa300@X9xKP8PC+ngjS~+U(})sHgw*@758oJRyo{_3i`0NcOhVM()6Bw%ECgG)Tz7WS= zbi64$K(I{3TGbu+AqB+C+p9%kTDmFc1UjGCUjbPrCY-#R{u88=kDTc*!qy&-0k)Im zO@P3%4)D}HXJ|bAo@$&NApHf)Z8Z6D5z_Sq6w5y*7ctFNYu-69GR+&ky}gI7KNPXh~!Ac^>L&bkzDl>v!P7AkED(W5G$04naa*yO0GOXB+LGskCBJrU}g`d_S zo*b6hE8!Z>?nUb7Y`Ww&WD(5eyQRwmq4|js15>U;?PlT=@)Qf-s4&t7A9X|hA0g+! zj^`qDbG5teW>fNx$LshvK`;7q=6Z9lYxnMke{bPD-{)y^-52mzvUf#l`#R**=(#5r z$9?{1pU3J20M?UhJKY$&&PHyFbqCXoAR!q$v|q>@`+dwWhk?ZbVOq@48Kbd1IbvfD zV#Jq!4QNC&AN0WR7mo}z1qMm$b*6Y2kSE17hUi8XP)zQ)O<|XqBWOvM2|KyWeijv5 zN{1M2D3a|TtZ2(rJ8xWZ_oUpz94soyJo(bp7 zvyrA07nk9YX|)VsUEjCBN`y~0@Cm4nY6FZo8P@wWqHvcE}52hV?Esf;4HTNlPt46_?+-s8X1%h=vd+mvah z)8E3E>*G#vY!mIGMthaGfv8UB<5d!2mU|roh9euW8BTZr@1y`2<}me>3Wj%@qV}|` zQKKsPDJdNZ35h=WFivaFe9>O+?slg6&f#HPrg>oaRM6AOc{|W87#b=wTKwTKQg@5V zp*a)XnN(I#R1Fgn$A0m-6{w&#BHW72t#$J8_Em0L)A<#a9(pGUZV zTEH0gC_~6-v|Fu@CdZjJRi5t~Q&K|T*w)lbczaV?9X%&G5-saF$AcwPOoo|0Z9qSm z-Ocv_JZP1cX_sJ{IZhQmxpXc5U)#yr&5q#Wk>?595fzHlUvS<2FJmZZRC}DfJb+&7 zfRTR}U+9y)Hm@9&ksI2tyeuK*2QpP2fgExt2!cGeEimH-!4il*r;NIHdn?>|gk6yT zOTkkwUD;N8D_(GQ>pJKe$9wn4>*nS2AL@T>fE(@fG%-LmMYl|H^1`gN(9P`P^pVEg z$7~$jAg48)y7g89gWG`6x_IvOCx`f8KSRh{wS7kLAe)k42I-39XOd7-M9q5@Y#f{s z9yUDZFF)xOCMVT}C4`fd?DbL%dGAMpZ)ELIRueD!hXdDmvEmVQ#<4i`$}hW^8>k-+ zCwNAJJ6dHx^n4$4;Lme*P@6)nU{s4&(OM1yp;(*T!P$*M|Nye3hSt?(RhT}j=V ze@AIpbH-rq&ccD;Q9R5tDnr8rO3t_6Fg&ZwUa(AMkQIx(@N*7!p%fMuPsjKP3RzFC zu^J^`j~6`t2M@aJ{jV47>~`u|@ZqTU(%#JbiP*=vJ@5#2Lj<_;W$)SoJF`zkjj~|RZpO8sef2WgZ%O|<;kjHiPd($a|%_DD-jDJ zdifHHorpcs*g0aSD+!*8V_&RbPNYiC5^=@B%S5M0sTXMA%00K^xHihL-ecexYZ9}V z0qSI$-cI^lrVIX;6`AzRVJH>jER;|x;JA2X;V&a6U3uYv5RFuh4&E@u=QJb-+3)Tt zqDQLH@ANozDjatw;m@#we{vwcbyX@m2AT#}ww7dT$MUeTlkoN9+QC95{ z6F&*6;-4do?qEV^{7$vEAZQ#>q#+ezOKE)N7p^64-C(6v_vLSHXegtP#p03J(ic=7 z487V~sONn|+2ZSk6liI==jXXG?r>UcsiD(pkLJdGS7o~dwfSiqd`o-PL5tQMf`SL3 zsqPE*^76VrgF|~Jc?|f2q^h_tS7xk$FT0*0(nhU#0@5yWfFMLw-n4G`Jm&H*uB9vZ zY&K({7@ZYXW=$zST3lMoXfNa5hq8Qx)_H~OgqpPEC}`*Q-N8rI#vuY+Z&V{BB%}y- zr0#*IXlZK^>UvE0b~DKrQLu*1IK^eq&0~-se;*e5`t#qymbs>;ZbZ+=#p5gFC5D)F zlj(6CR+od;IqjXxhX84eS5_D%x5kN%18mQibE0kS&v35OC1~C0-9g>?09RnL#`E`b zVS_ru1vFakY2Wf<$)*gtDfs#hSn29ymk zONR(>#3nH%ZkQG--kvNKo|!P6MZcnRE%(nb9Cs_>yLU_i?*%)2a2^b#!$3jIQ#ek+P&ytzg`lHN59iTy0t^PM~y`jB&BJDK0dt#dp_$DT#K<^{yvR z`EKzB91+#Z`^(r7>P~gTJ>gj44727rh_h z5Ur(MQ!N03v=dUpEt0~J;KCwe(onf&emakf-`(Sl24rK!>NBo7;y*w7 z7?jz28OwFH_SipleyZUgc6k@<$}S`#RK`y1T~4N;hLus9&ez1wh5;@Ntq*C&Yuds^ zcE+_&;&x&LS@0yF;os|%kFbvco(V{c(JQy21K@EeW*I&cQ?e0BNK6zG5mC-3re``D z(-@37=hg^bOo|YHTV77#$RtA#b&5I6WAKED5eRPxVcRble!x^C=3Q!C-+*X*f{C`R zZ9i1^2@g}bMd?T_>|P(Q0$YRJON(#e;+%({CbiP}=h*lBl#a)$gPlE0%Xr_B!UH4= zJyO0Akags@LdhbMD^pe-fa3riyBTXKk%V`xPOJtwq)9J(~EUum# zkXgXCsL92NhQ8uO-%pdrdd6zUFVdHoNL`UOb|b1IkcQIAM!sJb`EgLj+3D>K?g0Z< zSU@5&_8hY6#R&I|-xQ=>84E2Ny~Qbu{}POMn+b@XR}BC z@7(HV$Cn2PVw^<))9()hs|NUs|!N2pxoxohf!JbexnIZ zkBTP8J^%R_SitlWO-{?z*LTo)zJBqL^l2vG!fsACLW4$LFz(^D;91z~WOe(%{8^;; z3EDd;c{}us(w5)JNHdlumU@MnjcG!hjVz%^yPNX9$DX37kBb=aB__WXmEF*k8zpo( z{~1I&NJs&%I6%v;*e#gldqewqf+s)lZl5`0;DvTv>?@*_*5tB*)ySW^BdDB+G`bBr zsz1f5#Wi3%YeZ7wnEd|aim3m!=-PmAr2J_V#-n7bhsVqD^W1^r467GE#e8yM!K4FW zo|KHjbWGyso;j-TAb~RodZ~Sw5L>!Ekf<}GAs6+E5zNyZ?uxRgcUe;45GQLr*n=_-NwZD~Cjut(f@pxk{JAJvKu*VZRu3VE6y zYA^D1CXBGdAcW`kZ~j5t53u+LPuTPe#Q}EU({;Q-#jpZx2s@}T!!;juERd3u`N;@4 zt|U;2=K$T3W9|T@xg*t4N^)VwNGBbcXJPAsn%((AO7x24oN;l1{GAM*cOiY#f@-x1hdGzA^LS#!~^zVx=nr` zk;N8aQEE?q$}x)xXJplxAwh0{g5?)1J@iUw7?u0_`p7Nc7=<>#e6`%pLD6GBjV#lA zATQASi0Q~r9q@J%5#62@6#|yI;vlAv#I#=apEJpqgvmVZ@{(=e$60mLO|ep$IkrC^ zlLcg|hyKg_`X%w-it3rd&1BojS^4uR*UdOrP;O`mzg{-VJ1M;a0{Mlbm%nih*)1Uu z+a|-W*PNFoHUeQsKuF|~Dcd6=Dxi_3 zMrHN1F3aYyV>rdTs@z5cZT**aM2n7IZb%~b22)d$4El@0{-k6p!E+H^Nj8P-Y~T!8 z%(+&IlyP|m5cQmOGoD6lW2wDS%y(_VbQ;8p6m_}(R&+75e3Vf(AW-Q})E?~nWm*Zp z&WLS6lwBqC;iL#UZuwdsu#k`x^A%RR-E)KsE{0tBxGS`N)GS{VDW>bsQVUdQcP34>RPVb#EbC9c z1>jams67agV&~y$N>h5D5u*Lo1re!enDM*ABetU!Fq>(p_MNZ77fvXa%B;JMVrwPB zWYOA?-I+RW7}~&dQM-{|xP7H<{AxViZYH?}z-2wlWK+`JxAeMZ|Cfrt^?+}8zM&++ z7yc16#P8`8Vf!zkIJ}C5>8g@CiD|HNdud^CB)*JvAcS>PM=SG1)sg4e(Q@136iq*U zSI?<-yI7T|wnf*9^X;HuPv7JHwf)tb=Z&Bz68{HFYLdmFG}R508AUIl6VPk0m*dqN z3B^svY6eBsdFY)XtqBuU)ePO)U5J`AuN;H6;bYh&$8;n*M4mxYpFi?tBzuO6x(1hx z^!qrinY_xFqK{;QNC(!V6`CVAT7}$DM(qF5be3&VwQUp@1PMVJhVB@;Ly#W2yOD0B zJCyG378pQEDG?Y-a!3j3l19LxJKp_#c#nfWz|1{+-`Bd%wU*(7AsvsxmOg3xWQ2Pv zvwGfl)pkGX{pi&CV;-XkUOMD@DEy>f;{qI}zHMyLwNJ~CzHQwY5E8;Olv{nnU=O!} z+lhGu_>2J0!u@^p7du-8+o^Vzd87rEi-*ezaPT@t*ZJD1ZKO()a)*ZG0OYdlj#A|E99)$O1%AV>l+nxTwT;@lF-U z=PRvewV)tjbXn%P!+Vr9s2GXFN=cKy6B!hoC`dea21zTmau>Z2`lN&5ug zelef?+ct75aTW_gqwlXLPsHI2oA(3w$(wK59;gV?5o@cjpNhW*6*_93`bu1bliqx_ zP>I+V4&1l*p$GKsR};;UNL8^Rc#*adhtD=kLB<7ZKJSN30^^rR;uzaoMmTsjsoO`27R+`qgRI+xSV_#QwXjGB9 z7}LQ0qv+goVAqnN`XdL0pz-^rwBNl*AsL`LTzXv&xiHn4)PL#)zFxticCB%)RnivV zQAUWFCed_1PG6QQg$56^*7SGL2`(x(A0O&Hc}tZuG)j?o!IJCAv%gtMVa|)roc7C| zIVH8cAqU-eSX@EdB3YyOI=I))*h{j_MAoosPhuHGp`-mPY}5ITM`0nU`^T(`#j@s~ zx2Y9Q_8oduvIv4>FWJL1lF&bpr>PG|i~q>X0*@1~gE6(I?xh4Pa#+41GdjA^*nOz)V`0ukGyPz!nB}ct|Xj(7Cb_EW5zS?V>2Vd%j z+yjM8W3~@>5_jHjaNZDbmk`uc5~P2THmRvJ4*0p%f;Z%wU%`x-S+&Ni*)o?#fwd<-%bplfIpYhhUQkUf+ zEopO`9J9w&T>2@K=#M3{779r~Mmc+ds?BwTb%Z&YN0@ifB0lWm>6 z9;`^Nu$!KJ_jfcS)j=uvl677e5|bK@qu%Cue?{SR&A=1P%)LKRD7w=1_(rJSyprX0 z5IPt+!;txF-9Tct%1nFp!M$2nl-igek>s6NI*J9wAc_`dAe5c;tTg3Nj8SiVj7>C@ zlSX9@JaTh$lVjZKIzIm#&RNFz*}27RT%3SMZmM$=#X__sS}Bhoyf;4cOCO5ntkyV$rIIWbJ=hTag<18UXJ1nRq`Pg=< z;l65GC`uMnw=@_YVTLt?iqxcoMO`1@wJ#Fq)sp^0^%u=b9`Cs(lCQyeW{I9}2-)?O ziAOP^Xm?$o5ont_p0o(Q6uQ5OY8927rpBp~*^og?_m~#aj$^Z&UZrt3jyv(@JLw8z zOB-4K3CLQ|7VYzW_ieKCgy_?u`K|HFk2dRhlas=EwcA~D*uW@PcfvXL=w)27`C8he zwrVl-AuvtH!K#YnwPdM|P(H`p8*2!#nYZ+c#YT5{W?)QD0C6m2HU^nN*+lYrk zbgJZ;sX<*+U!{_0 z@~P)8frY_-dr>vTl#$mbo;Z4+6Y*SjB73VZBLgbItImQRUnK;sk`dk8=Qx1>a(%=6 zX`*}kW;1`$D;A4^5qTRTPL4b-7o3g;Su!s0ls@3Zyz5fFxWR^^kk{5SNe?0ae{Z9^PA0`ReLYvzXp$ z&$4n7+3YwoK6AEH7j*anoPe#$(P2LVb$a}?gnH2KD3HUdpTV!eWa8;13j{0809R%@ zT|PIC1j55tKj>bN_(LZlRx|zd$%!ESm1$DnwHB;puGwWY{{swoc;lca4?ACeXtZQC zMo9da{O~yU!TtEo`>8VYX%ZZd>|&E#<%lG8AJS8PU%|?T^tx05r(Z|6+{K6@jHk`^ zGcm}@{tEk%dzdQofnD-N{XR4|b#ECz+`?-*hLVD!QCwJo_vdI1Eb#i@`DyWE&Wo^# zSlY7hfV`pm+mB@0jh2BJ(u0=mHS?zKbFBw28`E^hV^V!O@Xv)9>k1fVjdnTnS#I;b z72$btXQoA*(JqEoshgo?Z(dOB`P}<>`%XfUjVTTeNOEm~*PL6G6cBdXP+}`A9+cao zgj6}l2B5J#RtQq0{mNp+q0H85)dCbdI&9gXqN>Bt>R-B$Zdo-@V?`k+U0`z1l%YaX zo7{*`Kf*qWPFqn0m;d%Gn<_?0NeLJy(#$H08ND8VX%R+=3E6WW+lD0WOW-? z$9!EEU!R0RPG`FNnI%r+Sx|%5Cqn1gl3O;*1bLW=6s1$gs_J-tqPVAH8NH$fuD=Oo z@3*NS#=AClrIE&RR^_NE zi`viAL@vu_$jWf;1xNaZ%(e7DuGCJ*r{!W5;U7?f4yNjhE{4Lt zg+i0J65pR==esZ8L=Ta!x(zOd_Z0ea+UI47MeCb)^>GE=L0rXkJsSM8<6oZ=8F9{K z*eK3lVs&z75o6fn>H)$c9gZ9+m6)Hx48J9-Gau~S!Mw1DG`unKodch{3p5U*{aprq2^Vg-iOrfM8gv?1XD0Ima-fDG#9GjtiXxtP7$ zJwQmJNpg)_f8%vy~>TsOa;;N=1ZRcLW{Rm~Y+g727o!xINx^>y0v&ca|bV&*;<%xNZYGJ@NYte$RR{ zieoiozg$IiUHS1T<5k*Dh?iHyG}7KHu%%YnhG~h}EISEXvjE-Huejup3p+qF4v>c8 zzfah1+=G1bl$I~Fyu1)2%?N)RC487y`HUz~Da*ju%hie+w;cjZ(XawwN~UGExS74@CD4^Yp&urk+tLrNv? zH@tX2%K0gA#*R*$P;ob{v|DO_EO@=F#j?c&-xBv$QVC^*UBBWwcQ*V>1Bs!Elpe=| zxd}Hz^{r#ep#AuU39>bv((BnESpRhP>gdwVp*ng(`l3 zdV0d8V`Phupk$(zLRx#zspEiU!^VG6^i6FFD*4h2JpIZT)B6Yb+RTH)oq^Z3B1>EE z3^(teRL{0k&AV^smLE>3pYghH;){!V{{kQEp=`XEG%SNK9-_K_DJ-FsCkO1Jh$cjU z#4{IWyc0AudB71D-`wJ2`woeHc zg1WAa?^3^hslS2`L4l~)wZ($Zm*`8Q;so@nGj2fpR8jT{EF>Jp9^p@h?63L0S#heB zI8wQL4rA=kNXQ-N#na8&oNU`zj@{HLxh~PYmuB z1%VjWyZmv~LS*Q=ZN z^(JA=O|JrH4=ZWY?6-wJ7qlN3ays!VZ3=dz{{EYC>8LvI1K1J4tWNH5 z`29r9`p)|Ey#z8A+9A1t@fi>mNqq+8dL%b{o)gXHzqpMp=n9F!A=r(YXWC+N;(awg zk*C80i>sY;8njRtcIj^`3TSxWCfpxAJa8K|GZXE%H^YEM9G@2+E}(1p2L{wrG%Z8o zq_Hp#10tuRN~`*19lZKk(kn`F3HnW&lyy1_*k=;9(uH{LYO*L`dMcRlyayN4<-H?= ze}jK`NapAMrr~wtxbU<3#mdkp?%;No;UYnM;Gab>?dc<;%kz;t2!LKxvTn>GPm*op z>OOsU@V_r?T{JN{0pv+TPLdlM0#5K8J*(;r=U1z<4+`^6r9sJM`2y>-es^6KAV!F^ z^^2GOS(@9ZvXSR$@l?=hCZTCiogN!7?iRg2mv=IeC>h&rL zOaDnx*m0-iER;*8AS5>=G*0Sb?Rj{V>9ptv3SUqH?CGxc_8iE75x<*C*3!=`|Jtwo zrJ%u5L$H}Xc%Cmoi(Q^3>f`v)N<^7}E5$)O=c=*?c9exe(!IVpd)M;Uhl#Y%W$|Pyf+`zNydOx!IY`J=^ zO=5cek2SmILv@FEQ6PimoJL%$Q?WQw62q$mr+tqCW>Y5yo7Kh zM%evoF%WzGx~rQ+`hB}{=wx3Buh zS(&Bw5#?Tl_SL!96vcxY;3@FvnKEm)r0yI$@#};v^ijAgT4^((t8CQyW}Sqv@Ouiz zSIvj*85x@_BWb|n@wy0YD7l28lZ$z(m-gi34}Hkl)p9{WK}_1Fw!dAsvk5Fr3z;*5 zpAq7Bf82VT9qM05V*Y@y%ecZL=uRhvlhx7FKg^i0CaxdE1$H z{Z@dU)6DaN`+L$>Ao+xvXrArHL(pgn%~vZ4Afj#Wx~fHKKwk*a7L)+mI?-g0umKC& z*FlGwzcbR@^$Xx-s;4!X)F~a?35OV4-{{h>qGN&rge8_s`4Ye5Wt6dsz4TCyW{HR2 zZO~ATynNhG+wAr5^Xj=IcXBoP+U9@wv1|D=?t%t7Uiwad2{+T;iQ9N0`FuJT>A&^P zq5Xd9%$+QYee}niU`#ZRWq|6C?Hw@6|kIU5wTDLs19-G;S0C2o-=8z zpkv?e{`kqz$m%5F_44i4uea^R7KH8UaJGP=b?Aq8tZ%GyY5jH~hSs_C^~VR%sh7P- zhFLS0*a+Ao6<~;pPM_gRsH=(#`;Sey_Z0%s{~lBi=2cqlWFKuXl+zJuZzS{|C^chP z@+~g?f-;K{^;VCrje&vJpMlHcY-MvrQXuKI`bRqM+=RZ%zwZW#<}>XT4p{ z5h8P8OI3GS#|1*dX+OVoz-%v;43T~=-t@)u4} zz7!i$BQ)m`$7H5PgV}A!ps*jz*W;tg+Nnd9pj>18dgya-$XR+ZAP^TXQHXci&ijf5 z+$fwi5lBeAjs4Wf4sUdRRnx1pl7xIJP3%H&36mV9g)7U$$vhaJ{v4DDfMqHo^1x&> zH>s}scJYJp<^iW8KdGbLfkBEV;iZ2Za(aPBYS|uBaT}GA`NdcyN6PpYPFF8$|4-5l zMjUN`|I1tVWwDLk^`3-EV=<9K8A+N7fvll*La!xiabWEH$pR`m!Qr;_@X5lu#3}B( z#n@mkkDncgF6CvhM-gS($#Gz9aB!c*3V-apPMBc)Rp?9{jE*lK<&yi2P$nI6Q~`_3)6ML?HGh!O-w%#kDUhE-ID+1 z&?Hf)hH+pl6K1CLxi?28!zYp@V*8N|ESA_v%`%#X``Lj!CWQo5u$$$tYD2Z1U8M)5 z1XL-JCn_M7ki~3Pj#UahQ9u?ELpGunM>jNd?LGmG>IoB@)Sg<_&SDH+pTtBfext)P zSZ+qCMVDjKHvEMxRv-vrC$SZNyf~vW5nBQIqO0J| z2EK+?91o9@MQEFmM=(ro+52V#fUkhkQn^~fErl2}$%8f~l!K3U{-3Z(*QBu0j8bfn zom;Q?Cfptbq7B652B*qeWa*+8FkdnqH)Pc?wg9#|(rBcw_(P28OgqrX93W=Jex%yE z&BP=GsvH`k*A&gdx9!{ZV7?{)4|THDc+KyeqwBQ0Ubb*1)VXqRjC1#vvVU@4g1%if-YzPdZxhCIp~HI9nAucO@G zQy>Q;NCT6NQ0`6(j>Y$W8D-VTSPqk-H4xd;N6Y2s6582dOc8|9qYPQL(M`T4Oj{4~ z^8$2`qx>vjE6qa>oSXk>`K`4_0$6zWV}5rui1aRITI>DI_Es6SS%ZpwGrpf*XO)4@ zYvOp|DlC{^C!2YPhaj3GY&KyF3kn|51C_g~RdE(M68Vs}uBPsTERQLmt=weM)87y0H0~7T?W!y_C zHg9{=%G2vwj6GGtUlifXk{?Mw7 zNJ0f7c?jFwz!m%5#zG$$%MErR|7QV&Yvjtb75@AVKy*)NC{?!nWP&#pLpQfr+OC(x zy4tYSHpM&7?diL=LqI7Y%UJh|16*Bt#&*UBOr>IDJOBUaB7GTOexiB@AI(Dz9`8jhl$Wv|R2xAK4VNtm~ohNG}{D>f-7mZmp zlEr;;kOASY(KIPB${R;l=5l=zoc^LP?)z~+GRUfN8{SFB;EjYD=M|DM8)uFBYss?C z3M5A*9yE+b9mHJ&sy8}UokcQBtI1iV4-&1_$5<}sM-$fmC85ZYnD`A zkk-#M@TNQ*Qf~{1Zy<(g+{q;5sKf=*L}Gv$1e{xq0pw16(^O0ozZgc^z3Bk382T*? zM}E=cjcpi&RtjJn;7gV&+KC+?A`la{+YeMzR|iD_^d^11X@}ghk8^9j@;8fvl{MID z!nc&>mC!ZOPhBA=NC5Kt#xy%EIb`R<<6@F-W0FF36yf{PMikWCOlg_uQuZ3cI?lf& zb??2(`rY&yujW+S%`mz9ejy`SHCJI`uxBeL0M-&fp<$M56CCWf?mKqxz=P5Opc8V< zW!g;BvHbJ2`kzu&Gz7reE?(m|{V?G;Z5TiksFBSc1s;t}%NW@e!=7h^iL4YLO#=>+ z*X^!7`(feX;ns+tz>s+Hy2avEiZsqmd03B)exz3E(Yg2uaLCWfsENg%H<9MQu9qUN zy~c@MXx=6Zk{Y_A7pmXKhBd}H1UF-;;U8G0c$TOJetl18r1)75b5J|N!7=H}_SO?N zwK;5NOq+~{hzu59f0O4^Sy^2IV~`d4j}4R(=`%`ldP;?j{gagNaPR7JL%sNyanfQorppbB1(QKBec|+} zPNEt{1ZCTwouJ>C+#Z+sBKl&+kvkqqT|{xiZ8S}pTiX;~gxj@^#7;G=3xU|);ph&&6W?7I(1SedpUv3|WT{uz z{_uGu!yI>A=YI3Dj37kbFqTl+y4oqP!U}{!ro&bK*O<%kp;FuC)+AaBtj9q#BumDc zeiO1j@>s)>%aMYv8SP7zK!JtwoiTc7N@Pi`UbTqC%T#?U5LPU>|H>s@UgT};-YEN{ zZC{|ovj`4#;R}rgm9$sld$jL(4&u5lruA=u9Q~gF^2T0Z&6>SqqL8;??K1N*O~u>P zzQF-JzW)knj_DKD+D7hgHWV~2VB#hkAsCGDOofpuOV@0h{=y`z~*@D!pK4X2q=0k>CUz ziG7sevt&q*RcON2a3JC_p>-{YuP(3{krQVv9<}{);z#hkVCV^2XiAT%>0dc=B&obS zvC+Lg%XMW(?V=2tH4$ms9Z46hKNDz!Qy2;apZYQ(B9;Cq2+g*tQCKh4ZU6_L)l55nY(K76K%_!PO?yFV#K64On8`hy=LaTgI+>ag&e{^E3Lgg(NF^J|;9VuxzG zZ`2g@=kW02BB|0B!f2B?Mtj`A{*dnC;Qdkxa<_@`kym>K{I4u=b&IBeg+mNb;WX5N z=p2F{9ApafIn=cU-y3#4U9N9!Y$zOQJPr$EGE3%%=y?)BP?c)GxO!FfC!U$*g$&w{ z@Ty~xDeAtAo~GJ?{N5h4)>OmwU2``NrCw+hI$?1{3ySUJ;p(O4GM~JOZIf z^A1{DDop{J89;a2H0!|Ez@Urm+!vJ0-=I^kGF27%-}YAB=kjUJ3mpEY-P^;SuQ%Ol z+bb%o<~Jor*Otc9w>r+GD+9V+|9K+*c?KP@wF{P;T(K+FN())#lRO{uAP|4&e+t7!2ENZ864D(%( z54p$%HH8ieSK|$?m|tu$`5=#GIniOu$B&JK>h_7MnYQq7F3eL^C6&3NnHlV@jWiXu zoz*?kakC9J01s@hHvIYqo%X+knc5MNw?97deDQ2shGIIG4=cwtmarbm7ELx7N=abx zyC|baK?(t*;g=I1V?pC+-&8=xwQaL#8G|?GLBaMN9I8z~>hPGmZT>hZ@Moj@%|vfA z9II$!$5-%sISZ@hERek=0rVC>ab6?d-9+Ln*nOcRlaEaVj@YqJv|OrPDGb<32z&o( zVdBCBww#8uY|YF^7#8_#hP$?BkFY7vjw(t_%0glTMM^8t49xr zk!WO`180A8rHC$joQA)69Q7svcW?6nbMDO$z|vJv0*K6F=iDKqnJT2JF8Sd7v4U^-fFQY2MJY zvex1&SSl2Cvrig+ozL)AoWC}3^54x}7CRr~`wwbV|1&_bt$zL&m(wC zF(7`*01Yht#Kzd*o~_?HVA(rv+*;mKmPj$&-NOb`dMR&X8ne#=qMmy;-{mflc z$X*3E=CgGBqLR7fLBf=MA*1(PIZJX=v)|hl3E3BbL1em zpP_nC7~kN&!;;#^(Yg0^6y>!o?AbL8l==?Go*O)12y+jpnRuvz-H|KhLvt2qY?0M4A|sN@;4hxz9xP{I&OKjc(To@bMOmAt$TTT) zD}zJ$?*ZJNhNy0&w1j1Z|5<5CQdhs}XP>{iZk9dcx4zHRAk&SBNyN!q{s$m-lxgiq zo6<1r^lX&N)5{BcPt3G59pf z(54Rr3XosNDJga9a^AE|cz5qJ8q%G)KmG7_{wuZn{i2=UUZL*~R-JT1I4=GJ3<0J6 zR6Ziib^cM^kz4T(&w}ux>CU~*65C7Q>NnD;_QFm^(-!zO1h-mkE}CT0R_`jT_u9^s zv}F))y|VPOmLo7>xQRJ;{ocOo&)}If_9=0V{g^!W&$TTVeUexyBs&GJJV~EI_Gy)1n*p5) zv)-|FtfK|LJrW&6VfrlAQ(zBTD-!>#7m=to4Jw74$#A(%;a}IrOaCbLY(CF|8Y%P+ zF{#BWE0fV_HHcqp#MK{SxXToY{ObxGGY`H0;CEQ(8*>Uj*gf}O8Pq(hpYO2#(8kcB zi%uyl7e3B!Xq&DjRnv4<89VMCioUz9Kq9rGHwG#=bZCr7*C zRL0$A+3F;Fl}A!M+H%7mt%YxH$o#{Mp)`?JKdzCT6maY7mOj5;mIj9lM6q*ihbVlc zs8E$UTNz?8rw#&~=ImTr(WgII@uWARB_1qBB(4Yz)g<1iLmlg1gqNDlnsoTMME%$` zRLe46;hw(Ml4T`O88J})%xMXXIbOj+L!b0);QI`EtPwfeM}0y2uFFX)#Dq4Vc#!Mk zr33-;K~o3SkhW)RuQ)VmuPE@J%-UTIcZ3P&Urtf^e~KeXT6y&d`>Vi$bH1HmGF@Hx zO3q@2Vexx^O&!Kb@q2b)BewnYXhmTz*77S|Chn!jK1ZCGx7y757aS52&L_3@+4sDSK~xpfh3eCiqx&TMT+f2 zQ->!VpHKPvL8l~(MK9O985~O+N7nP#>ty2KK1*oLFP-}7L>_Foc_3ul#GXID0xD+_ zCNkSg)45{=uBtYS_vel52X{T(5!UfUy6@+4@PjGdGDP;iAgjY{thKbzKlldvwzL^M zxwwcS5sXl!5`)PYI(9v^JQo6KPiH^lxk|<(QoG;MBlrE9~#E`!#o}77!16ebtJLk6#d4?hZpRpds zsk=FNAHsOxvEmmj3>?h}`s1#>xjOp4MvXwaO^T$I;p406?? zRpUHggBU9Y?{dmW*DtIec(MrE2oKg=Ib>ja)z z`zD~&;Pa@UHPxA4K6Qj?({iB~Nx@C>D8n_uis2o{LFBa2bRNm89AcxIiHcO+vlaQK5(!scZ?k^?Hn}-$( z0rS$WNAVi>oOl)?{{&AIvI_%cQHvJV+NdoV*m_Cl$LqED_P?v zG9I|!Y^hM`AB4nj7knTySx#9@aGFo5Is{%75y2%`bHjfoL>r7)GCwtf@~sjf4$7v1 zwH#A&TQ9xpPQvK&LQZbJ$vZ`kv&UC6O|mh~r?zxOjRiBzNC^?6g2-X>+NQ^ z7$3o5zyDrlCb_Ekv#(Qa+*_m_>w z2x>_lrynzn{|LHhx{Lqn72~c4*`AB5Bhd8tF@*$SPUotu=9G^$KAN3PQeIA$80;G z4o1BBzzm+)7E`Y1Bo$W))3{2&k{Rmq@3(9#XHD|ky%H!2d?Ka}a!c^#C687=36Z$+ zePVO{3EJ30xcJ>O`x;W(M>R;*#BVpgj!Yt4C|{hK@#!QiZ|?bWt=i|>Z9{?fx1CAO z|2r(YtL3N_55XC{#`=WQYja~+WGkNi`6lJZUZc%K?{2M4EBKoriMRAe+;A1TZCrsh z(ZSC{w-#Lvh3Cx{<(wQ4O<)@XOz(?}ulBZ2x>l{(4`U?}>D#K?ZhgPfjp3m_TQTHU zxB*%5fWIe|+>l0Yex{j{M|Pxf^SwX1!CgpCpp0o9_$+&=jmKQS z+*+`z%ffEI7FIY>iDo67TWTXGl6BY_6-gd>5)sp~_ z2Bu0KVt>i!$}0=vu9(KIW#Pov9wLbo@~^yK#~OSV*5w4~hQSL>W}|!GoQ&>Vw@(x_ zGkzbLhaftOPygbC4!d?|tIhn9*!4&|J!L*O|Ioyc|LP!B4yju_sHe+;rG`Sl{!*n8xp2dWGLO`x*~glvHP%bhxJjN{6lIAh*I)X+4RK zcOsCsP}EYvt|U2r8fkNeQ2#b$vA?JGea_a_UB&bvJ)PhmEHt_v@%19gz#;O@bYWMh z`4uRBAk3Wt;$D@}|A9Fw%Znb{DE@D}4^0z&ooj<&aeV#EIZwj7J{Anh5rtGyDRfQ@=r?3x8x6#&!dRb zU2?=p$ubh&B`wkU5c%n}gv7+W_}tB{4Hqd9^U$Y2#D~SFe_($+gF5|>Xgivkm4Q-S zE0rQ1ptl*{!G=tm;p)V!$>W5hHUnLxi!R7AmF_a1$O9`_HlH;I0RgAmP0XJl=%xbw zWr5hr3xUauXr13_6(6*OAmt8^xSC7Mj?K3Fl=%%ZHAqDA)k^qu&BSp|y1H25%CkYK zu2(W#7kk1}U5t}Leh~?jVvw|Uj0MF_#(TNV4PK`<_eUl`g(`D+71djo2^~{zgema< zp`^Xgd5uJ#6t43;s@1>Vcnbeg69Iu-j+FkiS1r;tFt%_R|K+q6a9(_i z^ZYdFzm-@#2FE-56`CY~;Lm1bbmxN_A#HuKH|G7d$WiDTw0 zX=`J@JMG4B)$z|QD4Vl|80;aNE;G^?3Ubk5LUc2+WRP1EJ-8=o%RN*`|LF!0|D&cp zUqysu7XxF7`oU_bA4|z0(Crx@D{-+M**_sz2dxLzg0wVMv!76JVTGP_6)KcraPeQ4 z!EvMM|4O}V2_wg2M;Rwt``t9R$>ilz>1LxWdi>#H$?zx-z^4LaV*_b!UHAi$4iO-S zWxY7r@}VpIl_|1tR4K>Mxz4@Lxc5_2@AB%=a`JE`#}KkruaRF<8&F| zED*@_QncuC{k0~KRSz1FPljJ{A6}Ldu}309!W;&ZyTp_%ns)(S@%btFd067ZH~*+JH)z399n00GZPH zugf#Xs#b(z&f*90vx`tNGHrz6- zI!b@uTXvw6gPDjTN?DZRU?tUmbFVGxR2{U&6q%|r*fZjA1d)0>eVgk(LM7nbN|@7z zw1GonlQEi>(`+2rDPu;?w)XkN2%r^&Ni;f_%QWGW*yL1uK{{Q#-c_C!l!q>IJ&Ir>-tF1hUG()#P0>zt^i$;?(de=JxfUS( zPKWl}<$_r0tQQP1jCC2K<&~u=Xvq4p9KKXDZsDSbFf^gg4X+7v@xC!{hmV@;HoXJD zkJjS0fxGtuogFwucX;RU?*+;uwe9tAPwb2pm^-~dmsWz0f@U#6x;(g9UzTL2OX-T7 zsJeVN0dU`&1)cf>IPKu+BjplzjamIjOwWd#;GQVD`R4mz*=1)pwf9?0Qk<0r>s{0+ zM)*zI>}%agh>#B zNKVFc^2$l|dla>v?N1rw70kS)E=-N@D3(=mG_`f}VEB>J%ltsOm2z}&%g?2R@0nGM zvHFeuxC_TV5gUdajQKZ9Qvxgdq|uUC+L3b%?d~eKwbO-M@o8dQGk;V{t1~ncCIF1a zOCt`GJL1I_P2)J-Bn?@@D{TeqN;PRROuvq-K{?I28 zgtI4g_}UR>GC5iO>tw2URXFMBJCNqh*b~&#WSCVRMtc7!a?b>W)pNTN>_ZH&%Ls0i zu|93=gbo_SmF6{7Gpn21+MLIwm3dkm)vClwY0d*8bxtUnVgt8l^eHRlQ^@acb6Nnr zEq^530H~jJCdj$ zHtrt=%pVwm(O7@89o}oOz0J(bjJ#o_Yf0`%{)xcyq<0d}ex}8}Oon#WDs@ZA*D=j9 z8h5zSe()^KS|7)X5 zM!B9HwFtem-E>trn$jDRnLNsw&%YK%BRM_~JpV&HaRT*FLjHTJ#f^&yDuPPWr}Mu! znL37SvGJ&uxXa_tQXxm9#YY)t?e(Ybml-D-730@i*8s5w*mv_kbPbpX9~#4Mck`Qe zb`MNDW8}uC@_{y3v>_ZI#CyO)ggPrjtBQeZi0v)}nEHN(041v=Xs7-!LCiIgSqPbS z=uahuYlH2u!!jb5j%=3g5cPFLq)L&n=zk;*5LxuZ{B)E2l+(z9$}5=6u8u7NBjRQ0AMocIX&fpU;20#C+Bec@OT6Tjdk8%8`kN?wzy4!siKQq3tqVD zDlKt)rb>aqZ#t%8cNmp)w)!dX#q|_*^)L$Fk)AQ_gqu`?DaV+1!aS2!Lx#%alYyw* z;{7@@`ss`RGEc-dPfl0N+0WTYxw&0Di&SI^MV^7x0m;=X)v=t$a# z3?#8+kO$bW!MHpvr#{ztw~#K?zYWMvSUhhB`;|3wn$_EkXvQWTAHENt947HIC%h{t z4rtuWV4rMz{HaPD&$xvi+#PMd^X&_k9(kC4hCjTejK~{Iy1j;$Kuj7uGc5HJ$}&bD zN+ZVT#ki%=pXf}o0X$wl@D6Nu;ZRQ*0t<4-LC4alrHPjYEN553CAsRzg#~&Xc z6(h0+ArsQjCqk2-->b@-!UoE=gPYRlf2NbPDWKqiw3cI{>)6YeFaLE&Y)L%cK98R% zbCPHak_$^VD75gDDT;;2Iw@l{-eS>iW-i!8gh>oM<#-Jfek=Ff%$x?XBh% z!lp%giox7W3yFE}Hfa?uGEWp5mE)^z8%&fKcCe=6Sg;J4u3l5kfLS5~Wq)Qu@>a74^-(Z%` z{GX`hN6%Hyi z?L`7M))nLXzkVdkCxOM=Q6)yoAi2r8f(hkaxOcTfI8&c$%V9P$l6YwW+0 z`ll0|=alZ}=+O7)Gx6h3@{@ zxk{X~uip#`0WtLLgk6OM72#LN*a;-86<>yTr-95`=^xx^+f*lOGyIh0(sgv0coI^7 zCGn6wQNFPR@f+2kEdAbAlJp<6=9?^aAp4KHIe;$`$^3o{O9e?*GBX2;h(4Xup@$lI zoF>a@?l{;(wJ0NE=AP(#SVgr8>vshnH3Kfo8r)&m^!H0vjnXlZgZAkeeBzpaS64cZ z?miv+3<3U{gZ8Js?tA%@GS0R6E%mt#x!R&>nK7*3%SVM7C5G#aBHFlk7Bqx~ysj?0 zKHHht(7l8C!3>gu7Cy|^k9sPP(1Ha*l(D-#g;B|jb2H)llm)c92oyvU&iXUToqR&W zp*lVJgVB91ia#!_xVO4*rw&!XhQ8A&-o>-{gLjXp^s z{-5)Iqh`f4n@#0`{^}n+ws~C$j9zU*RG|;6KS7bcX9;pp?TeGWP}IE+R?Nvhdrj2A z1*oSSP~_6tSw#YWLlYD~E0T$9Z}z?p0G&|BOvpa3No|oQ)z{sIe|Vw-4F1F)x;f_B zE8C2M{Dxs}K@Ty$>*5u|*fuW0?cWED#(^!qYKL+bZNnEkf6hc~Le(bbN#$muiM$rl z2~^7zy3DCpiB%J`(3Zb(*)9?-r^|GG%w(9Qwnt*gR2M9s)W)D3oOD4vSH82A9x18y z086Zd$B#{>Pw6RZGk{G$tbQS2A3E`$`dtF)LsaBMo}qz%;ENGjpFR z<{iTLaB{Eko=~y|Dlnh}R8&l?te%Wp0bCDKtF>`4>!Ax$7KUB3ian&W_ zKu)q`b;TigUIIFUD}d~{ zzWo^x+utuf%6N}7!(dKJ#a_zdyZVp8uQwzkh0tl+t%ug(*9b`^#Dcqq0A5Pi6@;JE z$X0d zZ>6dWI;qPDaF(t|{|p&^?k&koE9dSWh|#+z2F{BWo-_Yl;k<+6h+}+GY@3+=a!&$C zz<~HR-3)HkMrB#dsuG*Efw-9i4EDuZ1~Wh4eFzXriTf$w|M*&K9S8(#Bxlruzk>tL zluef=)U_t5TL+{gf^sdY!x;N1lWBa^#aP6aM;DL<*zv3>Eqc8PNkt?|(bL=V>G#P?Ds;WF zjXxA&noZ+L_VVwBP7b!k3x5$}R&deN#$&`B-w6pM?YuWLO%3$5V3qiVb4!A4;|_x2vU zysTUP;w*PVmWCj7B|l)TT~&#t8S;3mW5=?m+4 zjDi&@|JMd=`*nf8kKo8fMl?Y$A4fKn*e`6}znf9?b~$!u?WA`BUQD1ZSR@&EEyY%M3q5izdf5K&CER{wzwOehw+ zH4{!HgeZb!hXpeiI46BlKq#jAgIgTBGnCl8zLV|B?vjit7pcJ|AjJs7{0G|LEErv3f&1Iu^6r^ z0ckG$hFSb-l^lglH++DSOh;YvK4Wt9lZ;rxWH<(-L1>uZvlP>9$N<{i3q$d_idpse zZW6fDb0Yi|OaM-7Fd(nhia1~9U+LKgi>5LR^csTXoi8F~ywm9ZG;W_UFjqypWBXMwI=y#o>M$kR|VHA zd^WR?;d{WRxT_eScolqNX;bjnap#CWz8ec7hxIjr+G{J&s8Kb^!PGw-5f784?VAAp z#1}qCl%|FDU$^Al=^D=9?`>tP)nBK9XjBTIt_ky zRxz814u-zHK=-qt|LERscE|xyemS#~`s)(9DXN=>T5~IH-t-{;C2fw0GXicmr z;pAbF6tIwJdhR~|{BOB3iTPhp*ygivLMTp&`Lq8=_c2PinCAcT+}7Laze5f+<@9;? zNyMSZ1Y9MkDPWEU6}pd;#X8EejgzYpY%5IPr@-1c;yq=Bt#x$OmB{&G)(FP=r*}61 zyO{d3>jo=&FzTi2J3{*l***z4Q4Po3Tb~in>~JN|lUt2MXX##DW3Uj%##}>2D09}L z`4@wq$}%pkgtQEvJ1Y@Cy*_s}ZknoW$+ZQFz4}SB?&TkPzLOv6Ist$H^l|{s=T&;x=JP;%k2rGBX&EmDcn3DrSRfW45F;=YgzFr7T28RSY1-75( z&G}R3HlPk|)8FGn=G6-!(>k9vkd{zQWFO|I$^lj#|k7 z7rcC#l_r%z;ouuWd*Vql$0`DxL)u^S-N31YkqXiClFKyY4A;icsbUj3?XMtwo3VPc z?le>>ja0x4X)o-P#f1N$jgUwn34xe&3ebeYdb?1;R6)Gy}wQJ9uKjLI8}D0PpdV9?G-y*K`mW;rY2Dfj1C#_$P~;@D^)8>)?(4hv`p`LdAF%|~>Cq6WlPtl1w=LcM0gr6Z?auo%_85b7 znzLll#xL!ZT~io^wqlK`3rVwyD@hZ0hX#!MF-HW{r5Hyo89^pg;XD<}=CMBz1Z-xB zo}8xLTL#19Zl4k+QW0~Pm49=u5-{SmCHfQ0tSW!y4i66pS_}0@e=YbQtekq(6K1?q z)0zHqxniFjr6v~NA|sR1aY^nSm~_rH|KE}#{Fp4fr5kX|;(urGY~zaeb0}Rz-&}hP zMoj_oUFpu{hfL2*Gf<|p4;(x9=dVfzK)6vbx|x?G^!6l6675?k{j`aa!$3vCrq{y( zWj`NHMWgEx*qB{=0d~()o~J?Oslc**rLYkY0oQ8DUWHsUdh>}=H#OblTzp+^rkqBu z0m73FSlNqkp2Xh^NAqe&Y-6F?X@l@?UV280q(T`+@$FNn^0~w68CXH=8_~E3kqBqc zaW&_oS?*u2v`qi{Js|Txr0go{oX32vs4YZL+*6RzW`8)?>$G3;uqs*MMMWq7{P*U@ zwHfsJcKY5lR(k%V)ny+|9IE!`R~+|Fwj8-w4>*G!(nbe6e0a!6k)28TSh~;sGB={; zWc~E~(j@~g8#zTf?DYD~xXm?gE9;E$HLfQ?*ej7z!rB0ohRjI!e5h<|?vNqVAYK>) z&!k(YweYaHlASmHIVb8{5)tN#ori6+E>68aGfu!$*&F`brSL1^{WL#sC`DQ%5-*%V9n?JY_2&d7 z`DS)-BlnD?6-U{Bl&q1-&f!zSoK6^>Iq|>KDZS3K%ndvyuqkk?gg-2saE8=Ls_U*w zH<-CL>PM^K+0++*`}J@-h?H-g6({r`ab{9iTElHh{&;lWF;K0`3ustgqCH3gDr(PM zh}DEmx`_$kmH*xV-3*BkR(>J9xqE8*_$fZY^-+WF1%i@d(Z6s}swvRqTR5s-^Ty{e zgp}_V2c_E+=E~Qquv8W~8;Oo`%K62H*vrX(G)hh786-Qr&W zHIDwoMmWTJN!;j2Y$9TF_{qlZqn4-4s8MXOrUW{rcgzw>k?lnZRoP)%u9Ks>n}W6l zuq@cMe{hCeA#n2DJ%_bGUl{{lPP=!>U$=12{2f89&@>R15_dB##OqTI54zV^46*4= zQi+@%Z|6>_8pz50yVYlGAP2-CLMoIYT`v76lbv7aI?7pCYoF2G_-$MR(;0)P2A!sWvT|0Rcy5>4ROPm@E)$~z0@ z4H>k^A1NaY8;>Pl_u2!7^Ii|T_knpal~eXtq}vm(Z1CG%!Svfhw$yff{<1yqf5$k5 zv|B6@Y4q`Nt0|OARd1%^x@O+Owe9Pb`$9H|qf_Rbq<4f)lu!X>E>g`Yv^>%v3TqYU z6~LH^bp8@Y4U{@neXT5W^z(m2pq%3>e1|EhNrV&$kqhi3*#15!VcppEP-UK|^zvdh zIeIPhxhz%qwMcmTKH&VGACM8&tQ2iUZwYpDHS$y~9jEAAc=Fn+a{nv5+@hM;>5q>) z2O?$vWd&S2Jj}-X5A5z+y{@R$?aT7p(q7#@ymFue`_y8!uG!b(onltMT!yP1zPS_P zwyvyhx^Wj6=Cgam`b-&=pC0Pw-)n5tqnM7L<0c<1SkVCE+LFhOKK+I#E%AabZ{Ekh zfosjA6igui%RT)Kj{JQF2bbOW$|L_$T$~m;2?k{^#Rnbcv3l8eBaLyy+?DQo2x|<3 z7^Vt3rZ++@SOcj%_>>V=cC2WN(hzXs{)xNK85A4+7GF{mf49dK*o;Wr0H|X5!-MROIPBwb$3(4vjmxUYd4@U>$q+r%PB~#(?#<9*x zfMxWr*Oa#(f>j&fZSk&}9I*|PLDZs@z#GRszD+Dg!IVB+ISi`@JWY!2cBp3JBpz8rmnAZU;faxtQKQQSM{aBE(bb&aOWd>KXfP2&f(79?Rq0G<=PT(^hmt-^B~e{ z`sM=N)$}Az5{?Z45mt->QHJ)1di1yH&*%Ql9vS4h_t_IwOfWji=9sr474@GSHhj)n z&+mk$Jt{taTp<^8SRNMW2$NDEi6*epmcWU|yk7aUkNCjPz{(Q!QhLmmWZs&!W01H- zD&>=o+7oO?wYL-IFHp=&fFY&&@>@Z?ZiWCGUZxCGpF}eN6*kluKytawsM`vkmRt=EJ(TdFb=suG@^l&-KH4) zq%^=)k;V_ME7Z%WNas4AKSzxyN?}h|#Q`#LwzkCjg!^f^n)Zfb_5GhtraiAl$(^_U zne*_vxA(VE9c)HU_4Ay&{#GQ8AJPoKQs@deh zMroLn&==S3|J9kTAE}SwniQyHvWX$NDU?S0hPJJRlK}pM4yl(+%iMDAR%2N*czN0@ z-ltIiR}LnLHgwE70pWcdY7`SeED&aBfv1AkqID^&Ja{fJ&ao0o$qn~CoWp&6%*)$+ zx<%#ZB4r4HIvWWhnLFJHmlZOkPg-pEU?GWBK$t|Lbd53Uo14QLoQ)(R_r#8CF~TTc zx-JHH9p{fwv=BhCj56~$)yx(Rgeoam@cHVJ>s+BzLG}91xI-LMNLMt|E@Az~X#*tR zYrkge4()KXS!)>>ocX&e+r1MCE_ea@95-N(7Bl7wLyF(w)z`O;0ZomFxdB@rqY6dZ zi!NWGvIr*SQmOl5x}IrhRn4>6g7*T@hqtcYN0f`b8aWONK@F49~w&BnD^fgU4xr2Me{on z3TeKws16Z09hD!49m)@-EXFQ{eA~Etw(-d~Cip~xB0v=C(tO<0;&if{sr0TS603QY zG*RTGIx#pvknXx;cy*O%TKHb*KR&M2-BgXi*ZJN)xRJcGYJ@-Nb$Z_=sxbd!cVv!H zX!QH+hLqjl-CgrWYb_8Gq+ZNMyn6|sc!(k&;(Ix&U8u}}xd<03nJ}hVqem5oy%<+r z?{14~1KhJk#Nrqd3@H&p&pN?FNu`IFj3@2ly2bWYWoJX#dY=aim-~svJ%x@yw*Ot| zE>OQRL>-6`r?_LA?PPR#7i&V9E{j!OxY|>32+zwar3Z6E(I)Z3IAQHHt zWH#T#b*G%U-{h%!l>X`~6XLOAxO{lDOYYfG*Of*gBqFT3V<~5P>R~oia)Sg*i&q-9t*A4!dqt zPA#)^YxTEH$J45||J|SKe>W@p%d4xRyk~TnO+}?t(cwi%vI7LeMzsrA2B%0zDmop_raVnmvvP%A6Anui5Q2IH5>*l(hK)LjWOQ)z9BSoD?tZK+ zrj50AWK$FOrPEI+f9U->LU{RU3tnh7ZP{=qIV%(CNjamksOzttC%Oa1lFgv`vd4Mh z7d8KzB66pfRqi*C$GksVCcTMDwirSFM7tUTU=7{(zatU$-^-xpWZjVE+t-@FKe2U? z(i)r70Now^~vcd$D_##315{{kx;*<%8# zJxC3KMztX+D1`?2bms0qf0I>;j-V=#AoUe?!g<_pa1K+RiXM8e_{c5(Y*N6+%z%dT zM^B2dhb-_Lxk%!7Q3~)#sj-J;8or@#;-T4<%}bR(sd0{}i%azNKHJ4bKYB8qSR}{L zSPq}End4Masmy*odxm`HV)Qi;a)DIDfw*+&;wmPrmxe|YzD=`!kZAs$f!5mUhvwdM z+q|g41fA&irK_<|+Nn>$VauQ3;FWLm>D8zwUzjFFLX$#a*f|$G@Jk$b{+tEVJ2nkcg;11ku#v_Y5p0l~ zuO7pVQO0jf^Z_&P^~TckaY8%S`-)ymPBXBz!C0gz;wHhw-s^Uk8T6B;<1K`rUaJI;J{6H#Y8ywJtd7AwGC33!?Z{eOL8`#Akhs!l@rkh7Eezg~F*K6X145fUk-_FVIa1YnWOJt`b^IhaDY z8|~2l(*l5yuRt`)eM=~Pu%@`?Dn;c4XsHA@EZ5q-Rsy?EO$s%7U41|VcfSjp(DZMc zrE*AfWmAB%rYB^;=Oyu95}*{_GLlXz#xb8Xs--#cI!8CaK^DRE6de|!-4v_BSL#xb}r;=L*+iepG-`Ra(fp$3>3>|2$}LcuiJW|Vq}xF>#z~~%kfmLB!z=0&>;z$d#{^#XLtX^3Dg(y} zT5O<$suKHePVX}w& zR;WW9pMHo(!VTWt;Q>q~QTM%1-qynJIas{GSM=I+4l~>mF4)~R7hn;DFSVw`MxgZ>_!4Pk*}#s$VkC29LP}+$ipb7@D};o7WvA0)&{piT*$0| z*~Gp}_Y`9al8kBw+hE}U!K`d#VFJzQ=ymf4Lm+wADt|Yl%tgnV!b^1TD@glK<6nMV z)HoVNEA<`g0@awL7mWl7mLy7);(e-yOSChg*H}o)^977nkvGXl&iC zsW~~LIqNRCEftOu3vT_tnb)dvWBJ{$`m1WlitP7nYtAo*zN7Y0(2hGNfd-i5OtG#E zB&E^y0)L~*M#3#@oTXpp9^h9X!2a5xYQqgTuMXh+@4B!XJ}nIgS=8slf-F$oFp~zy zC14J|#mu5vGDGGZk6w3fo(Zi}YwPR#>Y8>4J~U`ECUi*}xf>U^fmvm+;QC>lvXybT zueBrXK{6=?>VNq!MJ=NbeGBQ8zsN=1E2HUF_J;9&3Kf-BQ6F%62h5a0JwAEt+Pb>I z+hbzDux69}*(deCmm5B-L2(sRqf_|V$g9e2xgzOqK{1tqsr+uT@$ZUQ3Lg7c>*4Xd62R((d2)FSxxCPkSvLzvT1m(>$TM1`l490NOin z10kDcqac||eUqC+=Xicmw1;3G_o@l+NPnIyhgxDA_qn=Bo=y$8eHMup7vm6B5reL6 zsGl2nG9e&A<9P}c2`NhDfjXS*N=P3OH3Pj3IGd$8N4gOQmDXAvmJD+dZ|S{Gkp7{F zV78QP$7c-Zd7D5-@ggts(`x`p9FzqCAOzO;d_^SJnMhD#9&z zsBx+wn7|IyJR544Cp;#8sxXXc>(1zjT>XP@!S>Ehm*y zj*rXn!OdCU1UvNi1_H})=?5oks_*~%-|dZ*FLH+g29TpkjRuNcm`&Bi>N%1 zW@>cq%lkS71dU)9O387UbppA+1^No6>GGEHL!6m9oJ6r+hb5f<^@-cs*}J?9p+*(n zeC395Xci#(jij%ynNyzK6t`llhy{-VCw?Vl%9A(t>>LF;KJD)dPAE*@$|lii!el|U z^DegDd+aEH#?Vq?DWI(3#?U1r$|SLqpuzse7zVi_Mz9pry@zrQAp|_|ECr~aS^HeP zizo^NDL78CXT+?muhCxK>4+j+56TZD2$e`k1gS3_wv_ZF74cEUQF(h!nepsYkpBxT z%X_7~_Ck~n5@iHh+pfY5mvF;|k`Wb({PvlRe0;Ln=0GgfNu`W7m6Z&6BI%`)ygH!k z_e-=P&sqf^s31s5e8$#Zkqqj^*5u!-D{giTY zaufU4)G)E=2{w6bfFF5lmi3Br-YpiKk8WC94Ymc5TA6~{Ahn8n>N7bpxGFfF4Bmk} ztI;8+q%z^LhImLLS204}itgth z_T@2ku}N-KqFKA8LK4@b$m{07%N`;MnI$^?m^k~+ZFsyT_F^SVQknEL;@i?K1O$%Y zR?x-)mwi^lp#>UAVHQ0^s+$(x>C7MRih1=EQDuWG^2OD-+{BAq=}m~jDMb?si04n4 zF+D@|7g~Rk6el-L3LM-01CEknxSY@SQVq(^23nv!y>9;yuu6wi-f}wu+P9+F?-sz* zz^eC4L_ZBU<&<%d<*D&XuLps=W|{3@&wBHq<0>+Z=$$*uDs~iL*7|o(UtQJXuc^Pj z3dA(2gJ%yBet;wE!m6uT3_Q0!8azBAXLma$-rn9S;+)%cgZB|WI1yuhCR5Ph{@Yn4 z-u=;dlax)?a2cyOe_3+8YtDV8Qk8@us&IRst!rdn*t@Oau$@?(F16!8V*50wRmr>TkwhBQYK$y zTc`?eSlc33tYiV7+ZB!%9XPB=Y}ST|P+TS5@o3>g%5IE_G2)Njm2@slQp9}{m4tla zh@AwQII9^DB$z#70F!OHoO#k#C&?1C(>J1tJ!_iaRAG5#qn{xB5@pq6gv{I0iNQfF zBB5K3@NtQ^A}YO7v&QM}H)@nud;K?c8q2xZ+hB@>pA95u?EydL(X%CN`fW|D zzS%!hlK*J<_fzn!YyP(ilH2^!`rgJXBEjim$IUkX(G$)%vrVv>GEp_Dn?Cj+rJEWj zS$suSSo~+-Y;?Z^@ler$Seqrl<8ry2pHd2y8&nc*(j!OIdI1NvlTwZ zcSNsLP6CkJTqP++ep7#D8+Ky0lAHxVTq&VI-MCsIZS>QCSBG_!Bvu=rb}^ zX-KHdU2^C2Bg=uag}kCn04@;(0ETATvI`NoDa&G!zm$Q9bzF6FDo4`M!I4(K(Rl`> z!&{en5G?|(S1Q&Z<)ZZ*AEgY{9G|4mn|q1tuuWXyh)+kfO<+^%?j{D zKCS|{@}9@9uF;u}h17Fg06CgpEJp<9SkyKNcsE%e158*p3lA==u~ho5*1BHcZ~$p& zM?zaQFVKq;gwAKrZ$}!cTs~T%zbrCC1SsO}au`owdRta(hjuG9J+~w2AF>?$q9?1X z1#jJJ0Mr55Uc4CPk^}TK8W=si+ z_)*(oIVQ_3*lR#lQ@IB}p`)a*-~Hp5NRy2N#UE>7VUj{zr`s`Hi-r|U=%hpJoi~f) zYv0G$J6$N%xLS>2H>G72L~JQ`f9l+ZZ4)!s2`Aa2g3D61t~k0dqSvvfBDsu!6_xPj z*#tG$va8k_$dFudm0jmLu|2PCZ5lm0W6jFicK(zZW7T-clh#2^>lL2t#y%dglU{o1 zjiK`g?3mwls3)g`icpa?veF3Nv$6}_z?9H~{u;to66$5PG$;U_`}RJbhTgtq0HeXy zmOU@}pjCX9>%E#L6HVL)NL%SF9=4#vQaWa6FN~bEK(A3ovKZqhNo+8tSFW+pw*mfw z2|RNd0tW9FJ=T#RBv|7=#~=T;Zr^+;JG^yvcb6fmM&JAv$6$BMz0dKoF$0pC+Nc1zp;O< zigU!~vA3jQ-4Gj&$wgSUC@b*v*Evg=* z%J@w@_S2wMTTANU0%%tO3h(^ z1}a5~7WW-57Q^TET4?%(4^`WfC8}f#tJ5H_4I=uZt6>j;sz6S{X|AHe#&mgoeFt9n z0Y*6BlGcK=0?Pw8>FSvA^%F8MhLaFjNCG;8C099GJv5L~2=17mhn5dm)M zTa!ZV+@t|a4Gnik{fp5=tW5DFI@mFlV=N+td4?GGg|=P-Wv07A$J90u7)p%-)?ecn zD>=A#R?Ov_8pWoU-Y(y&Dn<$Tw)^bqj+^ix}3A8f-?!xpI(zpJhIa|t`EyTts- zPo{ZZec*1}UC65iF}nyPcXuawa&lJN(mvjLz3&CA!^_KmiZ9bAi>_V+N|d9Pr^#(l z;O@S^tUB6Jiguy=4TWX6KxEyl;L6MsdImus&pdwlzrZPI8vXjH^pra&=*3-v)85`r zRZbL@)Wrk0o?*hfCzrGA4DlIt;luH=ZZ|kP3LNKdBLJ zMIA5IT%I}z$8k`tmUl|Z9UINJk~~6Bbi}`$BYyxNe8ehI;iQN&cR=oNv?|Nks^DBC zvS{GHUXjO$|7t+}u8(1WOyu2}np}X>mfqd+3p`gW<)q0r&E>WajCJakGR_8K3FKR* zBoToMllsutyt2WJ=E1VInpp)vllgi2zj3~ES!_e`$2l#&_F$)%0gtu_fW$Cm)d?g? z*Ad^p|EN&C>zl>+)k`9M{8pVYZ){94{0irh*ciF0`@Yb*LO z2RKA&i)D~PMMmWV5saJPD9ShyJb~x)+OQzY7wd zEV{in2iMl?UpiNNtco+_EpqM%YTS6$;HB$wr;d7+cHbX4pG~*6bltHTC;-Fpa6Rrn zNR#MnFMv!hS3HP~H}le6fgftjuKvg4i7#XWsZ^F-L1s~ounOhf@Z%Dj@(%}gy|DRn z^10)mwe9UvYP<$!v=nhAQJ)V`gCg1-8PbyI2#pC$vm>TPV5sO2RHIZEDu;`bR?Nbq z@qaqsj&V0P^Rzmd=1fkq&?Pf!*v&lcMRvz)At;}Ov^eCl9`$H;&0H}nWZrw4Jj!+h z*i$Auk|PqpqK0zBrlN{}bi7@~ZEo@4bNa^@Vw@)_t}nQ)m00q*kU_NKoehab16z_s z%v}U5^4N9?wIY>wClIPW6MN^Y$EdY=%W)@&@to$f(0m(WP96Sd{Ly!oG_{DGD#|oi zB+7UQnO_H;nOiY*KP5Tz6WaSmyscsg#OtVn{9@AK6_vLJdH`GTm%{JY#Z{%=;TpNN z!#%IVODCizV;X1a@})$`dUqmkcIVl)w|@fN(h~j@-aSM?Uv9gk{FuKoI_tlyx9B`e8 zjd-d|njpde-6ut?3mIF05qa_(tH(KLRlVtO%i$>OR`t#VpLW|P;;}3d(jUNHJlB;; zYne*K)YQ~UVoPH3?N%p#-lf8!v^M>VjRVt%p*};qeD#OL!O&ohXuO1%^>U2avSitB z012i0$fm_+r0`+4_AP7Ns(K=&8BJdfrk=sZU23Prt>iSbw8=|zFX_V(D9+6GxjSBjD*V5{^Mdfx)x8#M7L(m3Wm-tti*C1Ok zDq<&eXdbg^Hq=-wA0(5+K>&)B_SVPpJgTRF62)MWR+M5gN#yQ#1=^wGWSX4~I!Ta( z8Aj^Cf3Ha+a$ zmW!|!+dZU%DfRxWR9$9?Ph8D?@}X9N*;xIv=L7Xg(lqq@0Z-;hp7*q_blr8|od<9d z-NHtEdIe1cjbEcH-%A%M^OA)zmP`y;#kdt$gy(wQIvjrG*RJuEl6`2F;yqO1^&4ob zv$d^i=<1q!DLbcDNQ%=%F*z2CgOe*Z@yKzQ zvrDjFd)SqUh+Y(%yTJcq#urAXm6TRiI9= z9gN}l#cP?3n*TuHwq8<>dh6^2$d<72FLPl=!i{wTmTCZA?xl|Z60BOEOY0OP`!Q#Z zOQ8&%av@Wi%tK_UyvByrz&B+0?e!dJw!zn$sPxz+I#+L8s&2C2BH}OvS=|-|i+CaB z#5}Vjlsu%}BdBlQ`kT}XS*lDhR`R>qDMQ@a>-q3q?^1zbvokV>Q^XAjEx2T~^>zFC z$33|ui5Z_N-X&>af*t=!WxqA4&Kj~ zF_lR;O+Chzh(VNsL0P_BDzXUK=QyX4Jk zP@q%*QPcVzNKU2!Cb^;rv|mV-Lj$t-=(f(D$MW!c4-^#wHJ!_=Bn=(^knpDrw_Wm{ z-*>;QOxJgGND?%{3&Z%+T1&Ri5KP0Qs*e^n(ulZeJWCAslO?AJonyjcYS3P2ub;TM z#UGK{sq{@ndQLw*!R=4w?6;6V+GVx@`hwZdan!TbgIz(|NEok zl+z-Li;V>j2R?e=;q~>;%`NUdwCSdh3vMs>HUQ$me1!7 zSS(uNaqRX^!#)tL|A!_hmgyT;K)#JZ0F;oi!}A4gZB8~@E4z8r=O7soe!-2!9#?^y zh{;J_<2M8D%(zPqXRHFjxfxHVPO|Y||Bxs9DcHE61qe;suX1GVuEFLf=72 z`@n=%G1I<(HHlaKHjBr{*w?iEV-cS^ndZz&Tnx8|Ue>iQ{#0k?kfhM_Z=%%0HvSu} zPtj+9rUj+0-fxdc>_qqO%tl=APhTHS`({3$3-8XRnfzS$@0#KtgV1~exGG0-l(T^X z%Q}YTm6g4*T_87V(2hvn!>}=v^Hj)oscjYiWR8oL5Uu=}Xa~$U4x#!)=|qxLO7-rW zJb2yeCp-=&*g1E!9%Uamv+|F_O0kXO5&LG;EGz&hF9%ZNwHX8-vHcF0X&YxT?w-%R z16MVL`HSpfvmqXoONW94Ehcq4^jpBYO(I8QO_4*r7<+-Y_3BxHyUriK*7g?!BQYMj z&u+l~_J&~8L$0vZMrmqfhpo7VbOvp|Iv#Gj0jf1xo<-t4Y^!hS3Phzl{7xy(|CXJ` zAVxxj(YFcTA3KbA^w|kDW87^FKD&I+6AnnU^uIw;db{w-eY|u5FlR#6?Dnz6p@_qd zLJ;vGQG@Ws+TiBosoKUEPAez#2wStJ>bkDf&Eg z57-tmX#hT{16%OqglVVvn#|#`zdFY}vlM_LUAFqZE5@7}#co+hh^LwZkFI!Cb#W-Ofn)A8-a!3- zS^!avuj>}49UD>_nklMl{8pp4tuVV6t^wyx*TT zt-$J!ZP7Y&hv*1>0dXx(6CCbZcDUoRGcga3EJbsF!|2S;k?!krf65g zy3=Jf?(q->p9nvu;4lTs$TeA;HJ`^BNzv>P6ZyE_3cAD_zu=FzxxxIB8D?AAGEn+u2UFPt9`O;>cu22Tt!B^u_vYPQ~tv| zu&C@KYp`>ObtDobIuzMo$3=2ABTjB)->9B4Qn$g_iiB;FIOD1pDkJ7K@Xco%{EX*Ly)+Bv$>fsJ`piQU#}e%v zp0j>@a%Oi47=K*ueqWPC1z2MAv}DI_j18CE4NGYLgYSkSxxM>%wrA|f)f{dp*Rf_f z1WoxBZuZbL7_{PQv_M+*j&;xRtcjS9OGkW{@!y;ia)sN-(Cx>c1DX*<32OJZBk+tt zM@4Quo?NCjE)!Oky?algvBW+xcYqXGT!a5;FW=D))>CO8&75-S@E`argw6r}gLm}# zW5m$j9v!e@@1*<=2Koo(I?iA2XV1}R6cpAMv@H33?liid*UugAP9)1vY*4x27MFM* zfj8TBN!pa--bCfUE%H}N0P~d6($>F(*W7wxjpASAPt{DVRZy_`PMeiMi(qnPE0TMw z4U)ZcAJXe`jpg0=HDSvXtm`pvQbOEcGOoD-06jtQWE62M)=cioK}cFUV*<}(5G(>m zg0PLsHMiQ``7)*J3MIcN%e)sxd^UrF89$H?dY1l<$%4LL+nZV+PQ!AE?&@aupdYpP%RL^k_Ny z*}HeP=pOS>DPxI_Yex|kJiRv8NM6>i-tGtdJnB(SSB-fqh0Po+F(pbw!ORF6q@#D< z=H}+#rO5qK!pXcN0I9rB#O3SD*cHj=-&~R|CZ`034f~vlc|{q7r*1BjyANXfi6NyzPr{d_$XFRAG);&G7c)dn22@B(F#_>1gN6_L zHEj~Cmxv0v=43J|9E9;IqCInSNqjmf8cU)qNOg)aJLx4QwA)&tt3>Obt3RYH27Kd( zDIN5RR8CZiRPSU@sTr%A05xnqHqL zinq{95&5Ok+k+CoWRP3wt!qC7JRbr1aKQT`YOoOdBiiS={F=%d_9Go)U7NZeaOQ|O zG%~U{->sLhZ^RSSvMa9ub_EIqKL3Be@`?7Sd0Ot%<$2SmhUABE zcShluFcMW<@U8U!kiZMC*iQ;K{%(6QY1*E_KbK%UE+1OIN9^)&`uShZNS0KdO?P#z z4oB8Of4s}Px;M!{4w$8yGb}i~Kk%(@K^iskWhb`lMwUJkPfv73>x6?OM@X(N%$(9) z%_nOL8$!?bkE3TZ_vl~lc&Q;s1_3Wj4Ucd6v$%j!$#5%#+!w=?bq|5q*Jx%v?QmHm(#CdL$Al zl71O1wm6Y7Ez4@y)<^81w>3(iw=E^?Eh+Kk{I7yHvC2}WTpBANuyp%!TLHbbdJk6P zwE~!BIZYm(1p4@K$o?SGIY*AOP$k+S7){-9w06I1dXwzn&` z$8G#>27Nsn&w~Q^$er}==l1R*hz@d1krO?AGGV)*Pv8MNK|?+y^eDf{5nmTpoSCMq zqtNef^vB={M_4!tN(2f;u|~Y{YKr;+CrO;Jxfgj|ISnBpfk1j}Fd8{B6dij;cjzCEx?h+6{Wg5TMGrVJPNLa5 z`dvvQGsfF>i3&ttITgDj7jb9l>ek$Z)bVgFWFBz#vsYZyK`C;>RB_#=T#ItW0e7O zu(9y;;MI)4K8xM%!x9&j?b4Sien5qR?*&j+#g#s6aq5kh+;+v*&ZkEjV^_&EG7F~_ zZW{lOsILsGa_hRLyFux2QyW3L1f)Zd#-KZ;rKKC`l5Rv=y1S7M>28qjhHr7sdEW23 z5d5+C9c#@sbIdW>6J-%-iMmv&yKeNAaJpXHFM0{|&&)1v&X{=iU8Xxn@9(E3Ywrl_ zeQZ0%@45Q>siiOdex_x$^BcaQT00oc-P1c3+j5=UV#G;eNAPj_c^!B9uv&|)@3kBA zteCGi0jB++y?S|hxhR>GDkCSuoM|X-rWIu3MN2HEVxQx#NeEGayHaWVR+ilwnZtr^ z*o$yPi9eiNYsXkIGnjN04#8g}$NgyXA@ATfF`E}Wb3e(SPda1p2%RZDd>JRIMGt{q zt}cXtMCc{^g;E2uZq5stMEFyUSW}Hk5ghS9IV*h|xWWCJUky@$W{_-nR_|Sp`@WOK zb;jMVeGq-N?PP7xE1kdTsHr~_;BZDZZ_ZIYPX}9!>x|T z{!nmDdoXv`YG7%)HmFoP{nX_3j5>5bl&0{w%AR*xVxVwdhLulmP9M)bG;nbE&M9|C za$uU2R1PH$quVS5s99^$=VsrPDcZ|~@8Eux~Xj)4!x)=v}w zc0#Uwn1)qJuy)Rofw{rA7_{Isa(DObk7u#l93WnFUQ@no?5-Cp72G1Dmrq&V)p_L8 zxt*P=eSElu1^~2y}_w}jCYo`NDyFUNMe{p)9&~9$I7)&N=fhUbBmLU%AIa#voq9C=+m~Mr z)w*?g_n=1IssIF)JxamSbjA*P639$+T*CnJfr=G{6}gg_Y-HH=?(p$q>4frO&2v4Q z(v?UnWMWMWTd*_z@jBgZfA&N3dGV8|HEmt4ZbqIo@>YgQx&k;oc&)H0mYs7~8fl0; zbJ`a$pE~BZ-;RCoB&Kg$-mi)y(wwJ z@q_Ju*RO3LF@j95kYecKlF0a}42U6cqtSuR{4i?u=0!Ux6w3M@5@w&yKvRgIJU7>)ztI=U1V5XuQ?BxKlk@QjwnF0 z>FamE-AXEOy{>Y3Mij0aRGT)?53m%?*MrvO&9=TOx$W-$S*id_C=Z9Z$2M!Jz3}wStVp5 zV3tP&@N0iz*{#nqtK^+ne+{_$d8(%EdvG1J%9<*~)R^9$`b|u%(GbUOq~x@ra$Eno zGrU4t`%N*g5AT_)uT!%2>Tz+w$*_s=Pzh8w@9Ni{YmSPP{*n-Iw0`gpsHV!Ox+{IT zaz5pV^=3k&5PPvt(sA$B#wp@8^!_Fpuehi3kr9RJJu3y5+ty+FYWKO8;O)%P7CiQw zPD4JUweC>fXjjjpir0C~OMh|2(UQ8$o4EANcj?7(fh_j*v16xrc;#%+#>K_?{aBT# zDJU>BHxB_Lux}5%Bc~27ekv*bxjVf~TLxwi5-OU-`giaAYZHZZihXGp6-L7`KS>zy zKu;l-w>Ko2EWb3i1ekdBfjkI3tO;#0>Rh8SGBV228LUpLK<&F`k0~f0*QH*2KEPr5 zX80W3C8Wul0Fg{3%4UnxE}2q-Amj*d#p)K}rs9~hDfwA_ri)>#n2DX3`0(-L$KR*( zbLOb_6(7(`c9UqN+jv3G5O85=e2HVGVOHWLP?dZ__xGu$KgxW#(R?GRFl~n%yZk@CXTw13 zVkJ65^@-fye+9Y(w$R}xi9o7jkUcz|x2>-at)%&Z1UT%5)|V}|cb&)gJR=YaY*SMN z?dcD{dcF=~?hX^2)RtxoBar{ioZ4k0!UAq76AO#oYA5RC`ALO2R3s*ug4F0xEu+591$4wV&9{(vJ|oUMYx zP*&E-1J`isDolp1TnaUM96U+UUd;Ywj%z`S)04}Yz0{IEu`wRn~r1OVapCNnZvFWXbsZpRl<+HKRma6C<)IjFFD~wd5oE$Nb z|9Fuc14lkh8E(p4!tS8^b|KB6$a|JZ(+H;S8`ktpA`bMhfh_M}Xg}nS!|S0;(?gYL znxdkjqZ}6ZZzb;g=D^swgK~S8Y5FHJ;56uUTML~id7KOkC#Jl5yeHplxWCCds!fphcg@GZkdA8^+K*QF^h>Xv+Z~U|RC8*bkM)7!GTm`` zH`VtVR})hl$}D_5v-IfxF#LFyezgs}+J3G-A5e#mqaxh=u-%s7HJiTpm$TOh95jVr zWC$e7Ck`5q{SJdfdP7<|4&p6?)vm*^(9G-W=RaXNzHW8v~GHw9e8|x(X5zEP} z_v?r@8=jIHB!$weu?9WizCHy^T@)&aykxcn4sbEuL;$tO&wne2&?92I`7uLjpFVV9 zZCwbmH~XN0pg1Rry1dUOfm1$pSkYJs0IVd3o=5j?t80{79h48Oln=d9+vi_FnlEfW zm3$1kFK&O>HHwWK+JoM`CUN=3402ua%rA4w&J^qMU>~hk4@O<1`Kn839XJ5*DBO-< zeH2~vR017Ho-su(HDR8A5E_dja&h~|q09OG@`!3o zlsIl?U+D!;e4HE0fduKLeB7Z~qbsMM&J{+Bg+t5r)~k!5$GzpcQ+aEGmLXHdtFQpc zsrRJG+9+>d;s!3PTNB<2J*$BkcH+K87DqR2D6t>)I6U&7|B=|C<+k+QGrgI1%!OJ5 zSC|ut^^5g)KIgY_a0v695A~~I8!_p3G&+wrQ`ZYi_X}8)3pW$U-lEAtpcm!-GJTVp z@){+crI;frMstp*Wqx;?KhAJc%wK#wL^Iuj_i+1(kxJmm+(cLJ9c=}K`m zpYl9eK#NN^J4x8|;$mijc@`8Dn|HRkF}u~HX(aN6^EAQ?wevx@gG)_ecPQAT5Z8ob_KUXS8%LP@)hoxDep!C zop(0k%es|!la_mh>khlxY8QC7zY-&RY+nRo4S84l;9$h2c+Nunl*^?kExr(;gJvy* zyA7QO#0bF)sCzoAMy~B7ZU9IpY;VAO?A8c&XM-gvG|};`--P;A#dW!fsC|FXG;X}A zq#I<I$ZF=nSCx_z4V;sDpq>v}B5!s`gyKndP9%8e|LE=h45_yz8kC$*wxYgx_%^ zUK=s?{O!r{zHMI%SIa4*V!??9&M@XPIuV4d(i1ZvPS$lALT+{+lu8Tx%X%^`o%B*%i^NhtZDL1Mpj+OGSP(J~b5l9-UIB^hXmefR)5 zELd+f2z|l+bM5o+rrjzOB8@M~!b&U!R~E5W3%@x(a8ys|q>%@Y*HVLAwUiQ05lV$J zGFdJ_bh5$XnJ9EyAw<1aj$76hE~mC^R{^Zz$7>*G<2Y#XyBy`A}4|3hYaQn8U)0w5ij99qXpxy zwSNhlwjXOfWp!fIYG#Wx=B@H|5Vqmi5iDPQ&vWU0O&u5%x+5#N??8F6Ja>=5W@y-5 z>0w5U+hZbxYjV4|^f0L|8w4>>8rJt0;1Si)hT2lFywSol5vtn?Tgh!@16@0*c~2Y4CM)a#y8KihyxlX@4pgKGpMnUb`rjC$RMKj- zNq08-r{fB*R>6E6YQU@Y30DHm*Ml;NtJ0W2BHXIR*RjFBoXO_l&c*FGM5i;t^9cNtOV^#b`IAjhv6h=9SAQM`ZTFYwH(b#L5@clQ7 zQk=gNf>9y^qx(ub+VkhK+XgL5QfbMhp}gUvJN9AItm-ffVO?Dk7S@fUO+l<*_CoHB6XwX+q4zEOIOQ}6kO>h(`Lfde3qZ}jR~~=9K4UEO;+7_-6Qx4 zXJUy|^34H0Ot}v|(|4(Rw6a4OxdoX`l^FxJap?Wg+-nn&i_>=H<08FwjW3&98(L{%QuCIdo zUlr$zzGr|53Etu{AmW#1V%-?iyJKBT%=-r{_2TE{WCE-j6+X@9+_cK41b#$>A3q+=+YM*V?Y;<> z&05WVeo-|!J+re0hU$QTdfTy{+q(eunkXD8XzlsOI2JuKGvc*l7zs7eUAT^_#Z%f_?2-|2U#g2&$!GN5Q0 z+`oj?;({RUtE&*0FeXY-^X`)q7lGTo3RS@tjAslgaeq-plPtA^x!kT+kZbLC%$`O( zg1^y&&g6P}dZYHLC-cS!$kDt=4F03e7IKPO4YF%qIuwewNyk+Mt2FdZ_*Ur{SIpwIiUibIfEl1ZZ%R+azk6y!`4|Y{bA&IEP zB)pR(5^6p{M*}(UoDb@@Mr4W@bD`m~8DD(Ym`Nz1}#j72)v?pmBU}s1^_wh8ZeF z%^zhLU56DYwl3jKV+N4#v3yc-0AT8b(*=_!u13^jNYqw*GTkzb)DA-CHy6fJtIL^3%qMMz8$!D8`h};)XZ~E7NVPGqc0#LJ=Sh6>;>p zummEF7b;12C&J(_pSzC?Z=|McW{c{(@?4c;C|xKJop6XcIPc@Lb*PQxrbkNz0K?Nr z`|!#~{$wa;>WF^DqKbJjm_a>^p3c@hZbmJq5-Z`2M;s)>F;_s)S6&H$kED48eU{y8 zS8BYshk-L=Xl)ytF}{7!8s*S{YJnf#8`EDYz%J^Pyrw9^E8KTY@CL{ z+PH^nn(se`gUI}7&u%zt!3rMcWOcibl06wO#mx9+p2gwbvOkT8S^uU_ubW2@D;XwW z4Hi>*1?7INR2jAuo<&sr!~bJp*jw75_i!qXvjc{bQKE541q4OJ_o=w$Ok(qtnNu7_ zZfm@3My%-6q2=zwpNXxpTUU&nliV#<_A+8^oqlPmwo%@-Q7*sJxxsr*NEr2#x|3Vl ztRXdHB3$V?@r|z~Mj*rUuOUGMD|gqY&VTcBae7cA>l-noZaIIK6JSqK>hqG!L=e9S z*SBv#UtYnAo{s%0f6Ktl!&6$-(RCz){ z-{4|>gTDFaZz~8~(q>asek#k-9<1dpK}4L3f_je-Y-Rqg35~Wny~&SyuJCzGg3jsX z#f|pfHA|WteUBNOOqcUcPW(}^wFiExBf7ddQ@RG6E;5h!+i0SCCZ*@Eqj^P2=EOX6 zBR&EuvvCD#1#o?hQi36IXk<0DOo%LU*$N}CptTOAc+guPj-!vmnf zk(IN(A?+r45!rD&S8TrT5praw#SQ%ta`Zc;;V3X7E7euiH7kATRAv=%Gr_T$+s~ZS zI;vo4H*0$aU9>EGX-vsFL8E=sPI$B+bSDP#6Z94VwVU!2$my}7(KOvI1X8u7Utkxk z2sP&B0v)Jt=`7;LIJ^EXG^lE6b(sjB5^D&4@~$GLsm1b4Goq5%uH@GyQ2a3>Gq4q^ z>o;gsVnT+aPAcMql4^y)8o7n3)g@>#Wj^||BFt>v*F!F&Eu<`%Q~ zZ+aKE_N?oAS2QTH(YFD2@e89EG;BxyEN&DfO&1?D>%I`%l6ePNT47;Az|3AEjN4kL zM`pzbI5N+^LkIqCu__1K61VF9eB zKK-)sX_Dqv%Yc?st&36zt50HVsxMZYz2ypO_agN9-XF9ddFeUXDR-&T{lIPFy}W4WsmA>Pl__- zp3TG8{a@xAoG8tAv28bIOh!LfMODtipipZpme1`&q-EV|^GWo?VR3M}P=yT*`sN&2 z4P%Mv;$vk*@=>HMQgGjG4P}n+P=!Mb)r^f5Ey-er7zqJW`-+L%RJ+%=Rf7;ol zn}ww=#*#+;cH{u+N_FY|gcYUl%0a6{^-vrM+~>~;M|K>QwoiH?VI9BQ1cu9+s{?@c z3GFekkBf(eV5s1CXY2{D=I5p(P1721p?bx1qClAs)XnjcGBrYvP5qb#2SRg{tJiaJ zQC;8Q2NN*h7vyjyi$`&v`4kJUl+*h$uy1kPU)IenR!mX0GzZyCkHqS5C_KEL4#rQv z>U`#Qn!MC%B6Lfvf+(GQL{a>Xb$XqKDs0p=XhkqJf}3Ka``Wi-kGVXK>cW{MQs^C<1^88LAr5u9*>_%?yc z$CofIm$xkuJk9r(_6=GZ!Af)WQ)R?V@X#vmvapxcE+_*q>I|aM;)VKnz>-DRpF@J&RW|ccJ?Cs!XJ^$L0^F-;;ZjR3-~Fh{~bL4C%E| z#6;KSaco}~5M%a~;;DjV9W5_QJv}1kdK9k#Sj(I8rI(jz!?dK)a2|D%&{7RtdG zZxFeBNx+LG!Jed8UR-F{KHSTw8R3v-)br~YHTC0^EBm5{D2FC|8j`@9p&r zv)N7r0*&_(QE-X<*4itEwc7w3A%HQSC41@6fRh9gOaQ#}w@Qj!GQc>ro}5Ue&z~JM zH4tG01Q}#3e01O+#a$7NlFVj1SZIj=DXz*nOW>eaJ8>(;G5{gA@i3YR3U>+cXt5T0 z^pnpGVd{tFc8OM(_L1*d_xxNl2iz+J+{mBAz;2IzCLU!cM~2Yi5)foTCGnCqWs6XM zWp%P~a-+9rLLk0WC{mK+O^QBce>7S_avQ$!?4&LbO-cm;I?PUNUvsV_l^lv^I@Vw1*KqV+?C#MusV& zJ)5+G#3L+#tZ^5AecG}RoSU1IXCiQ%!0>4Z$&&oLK1n4lJ_q}~%& z4sSF`GK*&1lYb~6-=o@OhfGR}ZF{^b0nLfrsw?y9VRLkJj12EFt(r4^Rjo)sm)ul?a;Vw*xGJqtXIHQQiYKJE>s3bNKH zb98)+KzM;#PfD{SIGB@p-YdR+tRWC!dJ(OS;s7dv-V-vlBwt~=v%+z2Wxv!^avJGS z;J{@;L9!|?o|;A)9b66o#@1FpI}Hdb`5mj?vKBJ+TYeA6lhDIc=7Uwik>`N0qxjeu zs_wMXXTW<+dF*n5td*yqc1=h*w6wNTw_3ki-7+V|I|jC5G_oYF}U zBYtTMiyuLan!0xPHkWX z#zje2fI_1NH(B%L08vZzR8(6r0(4l|RQ2?1VT(WZ zTgMv_ckiIU_@Y!#RwgDYL?fW1hX92^6ru8@uT#PzUd(N6SBFQM%wY#UzLGl= zM>@r8=I;KpF%-h0`oU!#1)I`Jmy?1z9O9*iG$th*M-sJ?jV;t3uXE3ShL=6SzAj;f zS5`H*Jzf4?E$)SM%A1tW*Yqh3zE#p*@gOb4pb=L!7CR6sUla1&3mgbWfdo*qQSys> zmBWA{^)18{l#N??TF&{juU8p^))2Bsf%>JEm&HoY;`E$CM@NVDlc|%^%WLDP7jw@R zGlL@znP?xnKrza+AzP)Lm*SD*00jh09_z(c;Enm^*lk7rl;j19thXOS29|Ma_}0Xn zAkooAK%R{RGfah=NAyLMXw962Qo@C3g=E8m=nn#Wvrdm385LW4_5 z$IfoC))7y6D0-!@v}UYojsVt9>nWWk+({G>Xj+8_{cwI30^uRIXI~6t;`lcA z{JHD4LwsokN9*JyH_-cmp!OLmCDwANgq7BDx>!Yf&dtqZd)$s_y=D(jP+aBYP&*U3 zqU*&ecf8z2@^yiEsH7-kYWGE1&V?!m0mGVZkgRkQCzmf3Bayb&-h~bV5~7G6qA>6y859C% zG*&)@h%cn|8B_H|duLHZq%kqyK6m}XuUA}iaJYJzHC|vos*M5seH)A}fMINn?%QW1 zmQ__5r9h+-w6u8(iD0RMy}PviKDFE>1C+T=+t?UfUsk8hG%~IpCy9%OP*i(Y2w@~4 zak;%86J>VgGXlx%7tzkRMyM8+2UO%HtZflwcS3wbt-7qOsr)r;L}#`K^I6jG-;u0w zN+=m_JI;+1MbV%}e?-^XT_wT_MQH&ZH&`ymhOiJq<}9$H@4`r5gyX01J7K9;NEZx{ z7%4$2UZ~Nz82f!>6jA$3kl5Wy@hK!kgm)vTmkOmlYSkEB$L(0c?RIXdeXLI6?3N$; zE^NgVxf$}@3#!ap)A;0YHB}km3&Z79y|lKn(jPfoyC|Mh1w)%9UTbOczD$O645ZCB zJCJs)z%*$O^KlJy5x96~6&ZVGzrA%s18b$A)ffb3k0H5(Ry0Zq$U)kYNPUST++pJ@TbL)%2*C%K@Ac6H zI}%2I2>|25(SoT&eQgSGLGOkiwLihiwSqw4)2Kn`3A!{0dj+;cS~iru<&_=6CnZL3 za3q2ZG@@Sclzq1)Gcb20O(rGXypnP-;Fvx*T-I51!w3DCtvT<8rw7g*y8}`7KlZPu z!vH5nH$m|m`hJ`O(WQ+$`P|%`s*X<2phk@i;Ch^I@ua*Lq3meG0SD4ZO>|~@ghtv1 zXbnxbzYy)ay^t%O^lJ>k>)(XJRG|UXDvz4H_)rMGI&l8d@0JygZb5)mdv-Ps?Hjvm znsI|S*45KwPL)S)vLY-Vg0olB7oY&A&R2N*6_c=e7#IusGzOi{v4IJ9Xq58Y#72N( zZNX`=>+yQavwMT_(}-2g=TfNaZUir*e z%r)wET~^lL*bGPnnM}kIzoBTDHUSe2PPQiJSTFTfu0qp#c8DlF&u|xAwk*${*Q=I3 z5=Y5>QPbrm)8NUJRQnv8V+KENZ=r>bXtmfF1~QkL4UXi=q{*rpIz1PY+>+U4n3;V_ zaU`}Wx_X&`pk$Jp_7x4#OSZjZX$l?o*p$eFb}zVIDz5vZV6^8;{S!0AIx@EHmUzNj zHy&Gx_r+T7IX1kFMlj>MNc;sWY;G_b4)Le?7Pw_EULG{mJ}!Ij zR8m%?x#CGU?-th_UCddpEj8b~)WsjFQ$#f>GYn9``EJC}y+!~`UFZ4k)X{~8^%`I= zpPpUpZUS$8xcJ7x$}-C^0PeHVRS=2{?n!xPoxg;p%LN*s!sI#n&f-bTNXWeGi!IdY zED$F$MC@%@JhpTxu!LMdFSAP1aO0cf1XpSWIZ25-g>c;h09u06*EV5zH&P|{&06wT*FHCEGS%W zI90gT>5yX4^U)pHib@U37G^o%FDBfQhe_7DZ(&(2)%(vm9q3wGVp6(ay*LnZ_L{2+ zYt49@X=qt!Mal_NDkyvz>tqtr9&8QqQy=z9 zwMn6U=O;ok%?}{Yt6jOQ6b)a3>9Bo&cSVZqj`?8#`%yE?>tr<(baP+kDfx<}L^pG) zEsXSVRp((t2Y{&{$|Dvpi^%A!xod2k&M7L5E29zAlE6R^Ms^zDi3>O%;eg4E-GBeS z1cQgH5xV$mY>&1H)6p)9vm3G=VQ}S3X_8?Pwgg)WcpVT40 zFD?WzHabp%x9eY5f?Zx^zTN>n+ZIg%R~7y~6?T;u*d39Q8m|=_QzU_6?&u#zCL}6n zw_K)F;2nGzBYXkXW>@Q_I@Yas}YC zUwho|>KO2neI*owB2&;d`xP0x-HrBK0dikkP$C)+_`%Ru-U>KU1@E@S+>Y7;YaLFx z0eIZ|7Mns>fCRup6J-d-zsRyqZCZSwx@6<*eHbsw%m6j5qXXAN#3{+P%QY@2zC5A0 zuqf^qEy%DP=x$m)Sa7{M%M~&zWJnP2bJ4!~5pKC~G7(nEtq0Sz=rj0+-A9>?LU6vb z#N(W?5kP|JzOm&ST;~c@Tj5~C8yX&P<9VaO)EWG+HbO(>*3tKMG1MX{cc<^Sv(Elcfjhj!ERv+c99d>~yB2CU(eYAF9h6xIza$lGzUq;s|gbP--PvX(euUghxe zfpi=0QJDIDlqX{KBSOz(!mB&NX0LK6sOhQiKVN;DImhvvYMj$|lFrpNE{954G5D#G z@~Io6!}CVr5qr_)k{Sx5WU&e{8Db_=>hJNww!7;5ZenY7O$JbtfUtv=3?nk$|C&sl zk(=X|<}34(j~!d>nm>xJ3;gEb?j-%Ta`(v_883A$&nb`2!axjT3>2h1luSA)qlt z2Y_?nVCvf1j?x$__hIzC3lrcu9$W=%iIpgKz2y_!t@j~v^YC&d>;~S5Nq^pKh^R{FCPY^Zgl0iUfvQLe2LZ3maei)TqZ2ej^1?J>Wq@*w}8d0lh%F1S#IUr2p1J+Z#a6%NX2Y8wG)FU2KV9C^Va%m?(w>&*72UtC(p-VuyJ_i4(qLZxY5VZZIK7`p5%_Z zH+2R%E&;b75}2(rudS#L{urGxzMnDi*_#*y5i}g*p~d@_Ak#uF^l%(;y!5zfeI)dF zVSPNQVGY97Im#)cPeLA^Diid^vkv?n6 z0o__a@!+ex-gO<|V0BDxK9|vuqa!!j#tx`$s1NqM*G3wmE^w_Oh zY6tuSHK47)2c+Jg%&$2%#WG;T{>PmdYBAJFIg;-{tl6If&56VFS|ML2leix1S)Kh+ zH!OL`$;<+2E`z2ho)DUk&3bJhc84m-A#nTvNHO=P@wu~KZn_7FMzTh&Wxurh-Eq%T z9x+2mJaf0_^+AfCIl&Op14j=DNLpV3aNRf1hQ00*4xWn`n^N#8ys&+Ic++@#0iI?p z0tD)aIgrRNIe&fXg2zV~KU1ZDT6r%*$Mm-B8@(Y?IYzCgHG0QN2`fd;+n(2H?iXqp zQeq&O!hG^b_zQoAo?JLm)^px+ap0x2M+nL9YN58r2&Z6(Vk(jNXM3TT{v@Sh|GhK2 z|2&rcJ8f{euo?l~ZNp@9Wvtk!?O+>K3X*(sOeUYB!#c0@$gTsZ=qkq124{(zG0Ln< zz#4Pci6kgwsOa&?r!g+mXrD?q&c*Z&{G}DA(oc3pn0V{u^${;~!HoJFJ}bwGfK=KR zv>#7BdB1Qy=s;lbG|m3v^b?(YKocZ=p~)6hra=Ck3uv-%pZsf#OD`0(_Lz11U<_o& zAKUFTNrzp(Jv>qxVQ-}yH)EV@=%eb=E#R>3Zj<#j`LtT_afvCA|JfL6!$=!c5gY^& z9IOFbg)2G~QFx?HPyMapx#HezDioimG%<<~l>i+3G>+eRuazh$;Iv!(lWOcN*GTa9 zQ$cB8YwJ`G@ZUuc;e9jkpgGRW6)msx>p8gM|Go__fDoh#zS#Eqdcy+opWA^uf-gLO z@;m>z!_)8fOsW+?4+H+YM~c=gFQ#Ad^w|{)P&Q)q_uWK`$IAKNr`y#)?JpC_TU;MU z9{B{B;lGP0kl_*!!TiZP?Fp;*ShfA`i`NQA0r2V&ucmRhy*XGty*UFd9eIFFm z|GR+#5_nTi$1@=#0(njy>!PzDo5G(-IPZ&fE2{f`3)^)CCf1SuRC_KyPw>xD|F_yU zAM-IMiGL9p(!Q!SI@q$&sSzSR_iEpV!_BPTjb)E%h@C7-EpcT1RZNik-@T3u+1yR^ zSl(LS7Xd6JgP|iLG7<`vMBX>Fm94a&*mK{YICQl)3dZ!1@~w@pi-?!mRunOcPJDk# zTuz9-3YHC!pVIk-!$7No*I_&Zt$W1WfpFuocf-G^?&Q>M=dUn1UoLd*!&3GGs9ytRHGU7* z)mZ&u9p&FAS$U7!P0tC5qc!6!HtxD0eHMiu5`>W(`BQOOrva%4yPa74yD+Pk zt|`rz6vt);7jn9i-{d7XGP2E?YXNE^b5ljTe4U-gsJi6qAyl7vi5;y+e?BYXElwHO z7WwxsdK4Q3PeB`*io^_%Aybs4*HyHSE9tw zTxo_`aKXSKCX&`}YEt3gt8!F2Fw}m~e1wzk{O=T{7+)k6MYRW88{YPY1+Dqg*IO8C z;`_~jLn&+2o~o!J&GpPas|;e&Qy?BZTW_3z)UX{?z-CB_B{Dgfkw`y}Rh5GEpOn~0 zW=g_wq0wFWz-fdT-uj6)oHHTZORpI;!vJd3yzx%Y#zSXT@ zG|(8OKkS+{3UQP^3x4guV(wB>u8 zRCJZdFA441AnJ22Qe3+&I@fmhe|N3Bfa85CJVV~-B9hpLSHzg9Cn}WZFUR>sQh(L| ze;dz4H{TbSPNem9pxpx}N`zOy0xG_tTblV?FUUuSa@{8b^;Ih$K4)xs9Z>?TQ3th! z=-YjJ6eCe6pmbk&qlh2W8>oLZmE;@UG-iSJj|7m>M%W=eOpoTBl+HCBZW^-ga>+Jx zObw?IkWkPHA$kk{F#ct7YdzUKwjihh1WdrVAk)n~vH!p{GXS%#*?3N|{$h;Q)zC5p zvzf}@jz_U-F(tq6l~Rs3O~Yxd=6_<=9$tRLdaHr4j&_<+mYnkdkQYf*bE!XZ6@?G1*`eaa zOopaiNw~LN357qt7>u-N;31<;lG6KD>6rN_?{y*4bC+7a)tY%nuYA>zAB^(JrwgNY zR52up@gF;dV`He5t=0E#K>fZGd3KM#;zO_1bWK{9)dmO2{Sm{&p9_cH{M;yyl2Rvd zhWrNx?npKngh$R;Ed&wg=51LMoHXhK{FC%Ww`G^Rm*)cbY~0h0VNue_y)6*u87x(4 z-D=GZBL_!YbBcNOo+0VO;ZAiSX?*kHa$)dZxH#}@*hcEK|4EL0ijdIrTg_wmA1g~m zHo=ixV0KRLx>&cBVh!)CkBFZqV`HUY>B<}E3_DJDG5@k)z5)5SCtIR`7LGAB#Mdi7 zjAhvQt*S^C#ikdz4|O9S#0jZeN#9N?0XZfZE`3swBZh2yCj2l+_0oc@^nXQR1yV?nppZ7jA*x$xPSmgQOlWAJ3 z=3DL99h-SfA7XLU*hk=ExaG?j;}E?pIAKkoiN2WrVL9!$pa57E?yp~_>ah2_aE5GD5)5}~lbmM#9K z57kw96NZ6?a3sPQI;s~0A1||wN?ErQv#OLC1yRL#IyIBnYm<3jy8Gx~k0Ip?7HgY_ zfEh+>{`IU!{-ytd-|X4YM&o4wN#^g0aFq={6aLKV%kUz8DHY~~jLc%c1=A@O!XYFG zdkU?OLUrMfWfERL(T-8A71+_L?oSd3`vsqB=g`0Kn8HM1hSZUfXV#8#DmCK5a{U}mhKLlBg8F19WIt+E zhLOjVy*Z|MzpnVoorbI3|FbNA+oI)%^{G#oJh&od$JuNg2|jpnR#KN0&&XS=X@1TL z&nTkRxUAth-*ln>yX_`EGgjV-{jwNGzuxp-Yj6hMsNDQ;-Ol3st?SeV!ML0*A=gV| zRO1~4n~#)7`!&CgxS+N`#*~V(UDp)rjCdtu@f#N1VVgav-|r|G&w*LprB{jqYE|BP zk`IDjgDEv?6yJVwPLAVA;K6)+zhN~31F;mPo8PY`!;-hLoy%PLta}xP75jEeiDuiy zA^7rWneWIfVqnb?k5U0L_4JSteQbaleR6o($mk8E=DKKpiR^l%v_^6H%_xRO-uWjN zQ7_YA><_A4v5FhrS>N1~dr}>K54vGA@Fw^&ndRNw9@LwbE6mm+kJn&}z237;cJDi~ z&U`J8BToZ1>-FC(Z_;Uw0vU`Tf|PMDHVJD}MavGVyqP%|m~rtAkx4V%XA!z@Zn<+G z6hCwRRAFWgm9qN^6Z1?lMK{wch#*162DbJcJ>yI0r@!T6JOi%!@x&ro$`Thz;ywP> z0@c4d)_9L_t1o&=L%0ZCSf6x%U|X5{!<#;Txjom&+)cd}j?-V>KZ9Pl&Zm>?ZWUnI z4UOWk(8-~Pna@ukjFK%6&!qW=UsV#V=7+B@KeOXqHKlMH;u@->TM?&%bP(%{a@qSD zmAq7+@*WF<7FgOfCepFiZq(Dk-Okx!a^vbJ@0T%!nYX>~{88|S7G9mB6%E5@a`x>r zO<<~=T(Xn;%Kla8Ho+N5RwD!1tSinr#*YrO&E4AX7BR`{V^-{n26uf5mRsi=f4#X- z$gOj{G_t`Ydm*(I`Yi2f4IP}zqej%{a1ewf4#ki+XG+4EULAEhAYyVL3-O2wxGj>> zb(YnMd_0<3Y3CYA$`9F?nn8Uwq>?t}s5TsAH-vw=1eL;*?sQdzgIa zE?HXlz5jQ0O=^e7IVW9XUs!qkek*k*E$^FeUyiHhf0S zwpKJwqLEZ?B`AQ~m}9;xXQ6I$uSuI}yMm>({^x^mJjUQebsriuiFl;?WgtGpB0ImA zp^YHrC-Lb=L`Tkk;(c<4)PDk`_G3eBkAkm5C13X*V29q%#j6c3+Q)b7c?`M=#3}^n zE+8MUzk0a`!RH}E$h2$okh)q(g|FHeoI2q9Tl2mSXSz$8e+qGYPmtl$o429UFyGyze z=|;Lkq`SL21f-X4cIoc!P#OVg5ctsd_x%UYojWt4CE*DLi(!<#W@_sdmXd;70&HWz*tvrQ*US9z0ep-Rd0Q@lym%7`rf;4SY4-iQx|FEC$>6 z%v@(i3-u$ml|4?i3;ILrFSD$xodPdhS`JaiA+XI;y;PA8b50(a+iVw*$Re{xTH)0vM*bVhxo0i-Ai9Q-R>1gbxz zVHfq!pVp2fuQ-)%?=rfwJx43L42qqo-UxICN?#bV^_m?p8!LWt@TU}l=m7btZe=yf ztLp@`%mT;q9)i_i9vO0XOo^bmhDF|-3pe^X>w>V6WEmNEjL&Ekn|ta2S;2lodHxis zmqVE0zoIQ98ElBqAI`s1mv1IK>RslB6YSpP6gl|#$*ARWmC%@ey&2>)KMPb12HG|?4n0O_HyLU9UR5YTxgJ_IwDw)>VEc}RNgl`QI-<_l`ufRaX>i`;f z`rol}pA`ux;9Z3aW21*EcH5R&w~Ri^(!%lf$I2+^U+E~imcVr^Fj^Mu_m*?thZNMP z+{ZpErv{?l`qrKFnf>Y*@J-?4`MmjG+%eGzzPqUl-8c5+=)Ar(7$M8#ZTqow74k6u z-mWT5Fx;_;@(D`F=b%Q=@~q>D--}cVX6@z%C8Gk=T7eg9##r&V^kaRvTnb?eVuAfb zpAms5D~T9$=*3P$`@Wk&MVIC;6#=4$*)k1Vrg6&$d!I>k1gZJ(fj#009C+lF#W$)U z7(56J(Tj~dYrK9t^&6&O=vN))aHRr8Y{XSVe`Z2+r?37`%BSx9tVAtc{$v)8m_+~B zke8+R)@eteqhSeTcXd*ksP;WsQ-zhlAWlTf-ZpMqYTD@~M%3hIS?8I{vlJ7yERKx8_=g-M zddhOL7F1Qir7-~;zgG-Cv9fXX!*|qY$z7F|M2>K=%D__g-BjS!#2*{BqPrQK0w7^5 z#Js`v&r0b}fwr6<-SzBSD=Kyd0gUHDJiEOqyb#h357c>ycl{$yw{Lh|z5^Guk+FMi zik9rVh}NlPjZ|FW++bK=@oX+ybNkfZg|k4s`2EtqV)Kyulb8+cY-lVK_a|YAjL@>t z;%%1H+=0NZQa#6Q?qh_;JHlg}al0H0aS|D3lyj+XajO$q$OQ=d1sJdU=O#uSWJq%o z7J0A}4?)jhh|a&jy}17kBq7N1*U}*U);^&mdud!q~{%n1&Y`<;yWH}cn zDA*H-VrK0YP|Zw%8jg6UzNxGF)3jU){#I}S6=@R$R4jr!u|W_2EFpDPk=-_N3c|^> zExuUR66HR{UK(rfKoSe8Br}nq9RJYh$p${bmibt|-u0d}CBP-W5JFLfoMFWYz{NO+nzeOT}oFAsYf5CI=?x%sSyI|jb z^Fj;{v5nvYIU=S~C6}2jifn(0BngWpZ-BWnV=K??3}bj2Jj5oSLw8HI^5)52(Q9&enSSwy7E*!){}VBTIz7!B?T$PnwHas8I=+#Gt4cb@}GU zkQK5`?WSynao#i`LX=kWq2TYg(?Y)top*yCc4487#3kA+#K%M2ErE$6Y7k67!8{RQ z?XEAxY2JXe5RA1M@LKQRiRe=IES-q=H|IhmWw+R!;-C+HY^n52-jJ`TkcAdO9~T8z z&q`C%Ek# z^iAcV7q(M!DeZ)eN_Z8$hM_x}X_0>r0*uRgc$s-tfrXPiInrx&cC)9h5O9Vp{@{L9C&+9Umnjftl^Wp?S-2UUsdQUv!}jtpNq8HKt?xskTCt z8K3%DXoG#Hqh|{N3rC#ZMB8 zK|ObyN(-{}mQds}+mmL|9Pq)eE%761A3Zth^53~gNOITqTkfJRI4c?@^dUyb}wWb4+^Iph9{wBge;$PZi4Whyg1Px2Sb}Xu*GkH7)F1K z-nU`fa9&nNp4TQkr!T_&FL9ws3EP0=;J7?{KG~@Z>1_A@Ipr;0UyG&7TaLmB@uo zwL7rzwNgu3YtJE?irP-gV+!O1?db?BfR9V)h`;x+Ah_LTj z4sq5}{_lnZA1_HdKD@a`ckrKs#XrgOgG;fvgSw-ZyMxTPEa9avZll~laK#t3NW^B0 zWyAxm(NtTjS2AwbR7|zAXR||N;s81CJ6KGc8bs+je*9B*JCX2xePo0ly@^zL4QxWY zrTL!*7Y1(r2@E*Bk$6qh5jG7+uC_sdx6DTy@m*-=6E8IVT136AT<~LetoA5(fUNGK z%Vb0QrORjG(^A^unB-m>AI)i5tZhLWo5{ZDfi^C3H zi7NXBGNwn@w19hBl53u1Rs9Y@KOymoS&6|YGP#!#$q=?Wk*A>1NQ+Q94z&p*Z92$u z6>!?)KZrqaZ`!OlEXZ1u{OsX}Ji9PuRulAa|81wBmb5g;0d&jb#vgLJ!uoLGD*8*RLR%NNT5 zpTa_)K<`Hy1mzg-`!3ljsM~Ok42(}J1&RYb#NT?dm%YiXgn)DzVRy6&=(qRI;ipt( z3@tS7*zJn#*FR1Nz=&D)jryYTV^A2GMRFACb%>QenIej{kaEDDBMm3^&@4lM0b+;_un$pSjDejL5LX ze8G;P0CgEe=b`}qbaWL>Y=+L{-n_IAI&lab2dbO)kOgnsCEGErN4Rx@KMhk21(}7q zOR&(aT&))3tsz@7#DU_RV&gRU!O!H#lGu}x@J$Z&*VI!aZ-^hCctrgPUWM+vfSf@< zMAevhU?g-n0D57uvG+72#5gQ}Lq-EOBsc<@GqdibEgI#MgJvvKBFAOmsxWZ;R-STr zM6CHT-#6D}Z<9^!9!1ctNtXosKRQYjOPK!5QE7zLPEB{uGqD1=C;13V;2C5pI9w)X zUZ)&nZ1k-Hi7g*Et5vjpyxeqx{8TR$FfpR)%1+1G_k%%=z@$h7B+aI*3uw-_oBYmQ zJim&9XfZNvK|M|hB(RYPb``8UO15o5)en_leztY}((o9z<8zka_ajr{?aCr|Qa9-d z2IikgP64YQoAq&#zaffvOxS5n{0K(psza}(CuuxKRsnr)Gi%S_M<#M7P9LHWOVkN{ z=4SIAAb3-Gx}9w!wE2ZZO1H0FMd8EY0SdrD#tr;Kf!6ttGYk=?k5L>pYDO}h!74XQ zEdp^AaiG8DO^wKj4>8Ek?5sxL|NFX!e`8mYHQ%yBkdXqj&Pb>wz(SWq7?^%`enFm`uOaiuI z3>YH(mqA#MGrZSr@XFNgwxr*4B5fEkJu%w^u;doKNvqaQ`~+IJYD@}N^XF|zOtP*> zX^a}SFSb{(YYOLN@q~w^{30{Q%uJFHFAY5oG={^-kP(Th(v$4al!N#+#U!3yiOknz zZg2kJKktosIuLu5b&4Y-G5k3C6=e1JnFjw?3j@-W$UP`DsP^e+Ny`1(76Bh0Z7u3g z{X^$1H8xkW5Hw8(=&PQUI}RlHje<3Ot1CjuNzid0RF>!NBiDooii#a>SZsUo)7_xE zK~N^~k{M@BV7qnU*Y~V?92^9Nj9YLF&)rh6_O!k>=W1TH*beZnb~*;L*Y1qrFrhs< z=%Xg}Jpg}XOZaqD1Mq>O@qytq7yeO@w@N%Q)+-JVPrpx%qGKc@I zde37kj3G{QDjA&XMeT1wC;rL-vg%)i#(&ohnhczl*CBRdpCXi7kdAtQnf*LeK1Mp< zDr?4S?S)eQxU$uY)0T1C*LHhf?as6emjR3MKI4uS$WlR$zVCiD)$A+$E-z-f?!+5? z8o_oSmH(MU@#|kyj7u0QWmohj8z+1pCY)5)!AXxaf9Kql61+axSb52hR?6i0D`cO0 z{wETn6dj!U4`~F~7$^U*`W=+S+#4!sNI^~|%XdN1ijugRs4Q2lR+kLKew2U$_s>r* zu-R0?UNBs4-_`KS-&?x%iXTRB@5YbJ0Mm-DpPUWsGM+5Y%PJ{0qBvv!LW|kUwlF~k zXG}uDio#@7AQq9-=<;x@O$5KS6hyyP67?gu`xgZr3q79Ib>VMm}4(lvHAumXEyy9t1tjpeC$+**PN) z=3k}`ksZ3h?-Lo7y8p~KgL8`Q=h=qFuoUX8R8sW>Mmtp+n*AYM_pEAt62j&yZ)88q z02eiUSKGO^Xb^+u=`_+70l-Z>cV&>?MA`jqjcMHX?(`iAj1>EyAF2~0U^K#}5`F8N?k3X~Tb=vR&;GZwMGQZsw1#soGPQIg4<*VWoXAL2` za?Haa*Cz}3xig!Xai6KMVe89-SjmxEBdf*wFHUmG)l%c^ZXYW2QDkyd<+YyMSCvA8 zB)m{Gwfq6#_r-LIL4&iAD1pI{tyG$G)e93WkEGF`M$@q*sMhaNaYBOMh^$kH;-|)y zX17$~aeFf&$Fn=?S4F2nY?kJ@v!$-(b2{rkTIQ2KH!#O}9N4_9m*F!43UDmEL_Io? z^a_3G=;^?G@93Op!9oQob6V?1RkjdoWb+B4QdRcED=XOZz5d|9XJ74f^+>`iYRMo% zX#X!Y+Gkp3z@K`5v3L?TRfF^EMn78j$w{fbtqY@*nV?UMalVsoU4tiA$g9$k+;RoF z)(fKz&6kpje5ui&$S9L1zaZLoHtX#@EE<%|LWWKD6rszn6=?w=WDDbz_itJYYZEaA zO<^9CKxJbmXlR^|$x{ugY2RweFr!dk;&Q_86DvP zHT`-xrQD4Ydp|EGmKg^t>!324bcZub&7bG|S&e<5kUg>7Sfxkt-_z@+Efcg*E znsxSvG88{Ug5(L_W<5Y4J!j!Js_g5TNPxASUdQJak(`|wV^b-qh@#j0E>DAa;-w~O z1hLfQJlCFApS-T6bO zSV#0HK_PWTyT>^tP>hlF1H!}*=kFSx6UBm~iacNSM?FMhsRgBJQx4HP`p3AShevES zEXMMdp$XHTNfZJanVNe>EGv2{+UZJ!z23yioS{+aIrJj{JBpLm4N;L$D>4G-DS}JS z$39mQFVs4`g=bKBbv~~C+sR-t?B)AYRCJ&i+XjO#kj13NZ*G|b zI*XNhi?K9jz1zLhyzqFwU7Wp61d`67w?A~x1+eS@rd@aO= zf%vN^24SvgQUoDU>_c#vQGRE$51RJGr3MV5TEe$rM^nlN1S--IUWozlKc5;@qe|cw z!ShAHj(OG@d>;ReQw=X?Vi&et+waEfeEb~QvL`^{YB|t-m&>;n1?4ZON6}YL?#Mge z?C3pvcy!>#1qHrHOAp#t5q+a2ac<@>2pQE9z}c_)vR!byn9=p*!}1(bzbJa5^tytM zxcPyiJZ#~(bS-F1{RtYyfv5o#&IgA#!C7u$05q(LHqmGem!Q0Lq*0KzTUa9pwTSgj z&So?h*)o+LlU}pkn>CEA?hJmBVEpX8g4g7;Xqd{({5(h7Ev$3V0$Tf6x8!w5*3Mr4 zTpU{>D@eAN$f+RdA2Yybu)U5eo$&$`3KgI=x74 zC=6_~5+*$~unB-vjiZuw{F@WrH68&a^y=VBY;Rm~W^0sYEV%#_JZ6FC_A*?T-t2Hc z8TE?#C?(GKEv*}(3S;xMk5cK)O<<$jWcxtWF~80k?~&(-l?(1Xx(pbB>FqB<{>6cX zEXQYfrxex=Z8!9`jH7+fnGD2n<=` z57MGILp6Mzda!J7(T_(3;n@|%ZNf_nhe5XZ1P`vWTd4)XaWFlN*%z6?RadG$_UuHI zZC&0KoVxt%|NPP9>HhQ)NT=+dK-b($hvnRk5dOvzl)Nb(RccfJjsz!k9m z=hpt5#dp+!wVb3N&79p2-KI7O-i}T;1mAv$tL<@J;_YXF(gQJzf0{>$VyUCLtPUot z1+Tox%k+K~zM39wtx_~IWy@g+pU-GQwx#zbf$$j*Lte5Pa!X`{@S*pJg7yMDr0?+3 zD3NuMP(t|vk&xQOqm*mM@J%CwS)a)M`fR=_Y|v(-*w^mx73zQ;a|n)8fLMn;&FV}? zE5(nG<{0g{)IVzafOJmC7hwQ1K2B{%Z_V^yLOm=)aQ!5j?*_z+GK}1LT@ZCpsGb9FkjhGB z5;a&TYS9}rPEDpLQE{F_B^42m8GR6_qsrbP(*#oJp$Pk^s`xbt&}Oktk*y zBHdsaDfZ>CSGqF`tZL6u8KuM0p8n_PX}-C_Sm9iC1!>aDQ#n8)=onoI%xE9@X0&Y? zdSwEMcql$6S24&Kb<-iz*~Kb#!R9Yh*OSRtoyK8XzVTd?%GBP!Q*Yn;{JT@dP6Am#kT&Iagp8jOJY7JH!kfy%89q3ol@$>TTM=vm zJQwEZdgaLQ{9={uy|x4XMRp;1yLo5Y2Kt9?&8L-b6Z)gY?Acfhe{=H7zAxm~8Q{=4 z1m(g;rLWH9Rq$3Ac4*tN(_SQ0>|w@CC17eo>;*{Z;17$&t9wUX+?Ms2%X+t3vCklM z3P!83Kd9UgaV>>o9mrJcZ!9rvC{TFq&UlZoDk3h z^DFJIGn}>Bi_?~tziGf*JMU*H#zi8ogLv%GpOuI0c+sixp6DYWAfkvHDy0ky!4wsg z;}!JfL{96eQ>6H;t&(DQU35?!6v+JSnh`GtC4Qapfc`R?=;Dq}ie13IXvZ0H7>1Mz zpfS&B0MI|Wy?l@19wnzb5i8a&veRJwY~x-cCmhpJN;~BCc=~Ynqs8Wlzuf zkI`A3O7~W$-x2^MDiOAsb&oFzjOscPTsYCB33BAuerZ&EbiCrSe|)o1>GEUo^5f%P zPuDGt#M|Va;=m4=bltsq>wo@ZQU<>ct(N=nQUUeZ+L0q1RITBUU#0w`-_~GnNSw(& zsEga0ZxCk{H5F9?0m134<6vmCfIyjol%;Ps>v!<4$}VI08J3IDj&!rITxrHic3c06(y% z<#bwn4UwW4fF<$444l7&v97nZh#DD`c-`4HWkHF;$>rbQn_to@F67Wc1gL--D`;AQ z<8#KkRjSS_e@%f^yen0NhN&75Akp*t#!1BKyk_d4x22X$#g`xorJGuHU zn5#g0s;p=wcNa3FLftWR&>ET@?lawC%>`wwPM4pF+$|~?EinqwBn?1(4znT*ciGIi zkG#_+Igx64$}Y3}AME&d`HtiF%-L<<7Gum?uiGWD8|dDDA&WfbF+Y0Daw@9m1BO~OOi+B{BI1C8*NoY#EUrYrNzL{wBWRc$ih!AN{_<`O%bJ2)DN-<8_tt0i zuL1NG=6UCRyS~MHO?~j>lN4SiTdhJp5*DNh;ZbtXQwbo{Ncw)8MGu?NBv_gOHgpth;A$|_fyArMd@C5Q&Q z;IB#UGO6#!<$Nid10C>Z2g2nTE&cs@z=h|3fULqP04G3*zo5vKIz`qm6{4Xi-9V#J zJrcjS%RT)6DY+HEeY9)KBOs0 zf#tewbl!Ai#}lo(_Zt`X+P+)*uxk=&25#aG%{ZTuKGiIH7I+4^4TAc_!W_<}5gN_e z$2GDdo0IJZc~>TGrXOBt9+M98YSf3a*?&clD=hN|QnRn820eD!{bi$(-w1%2+Tt8g zy-4h_Nib!LA8Zc|>iwPE)XNW##gfz#u4;)03(VBWHJc0urY|RspPq;M<=j8Vm$ zD?KP3S@5Aeis(Q6z5qZ>}Q{vFu+Y@5wy`Vk?8XN}m zo)b13^}|fzH44^)=fJrK!@NvV4Wm(Uv(_W2LR!4R4lBc3$A5SQSBk`;YQh@Qnn1az znc%oq#vW)uEDpQEN}x%ebiniBs_*JSG9D-#bK*yZ{5y#6xVg?@Via2rXY|0TtAS|O zr230hjGv4^zN23&F&wk1y=T${H=+4FHdY(8`|*~w!I7ESq(}Ov_&3S;(X-MO`9aBd zIB8@5PuRV0b?(oHov_24Oren0QHcot_%Vp4aqS*;De_^0%_|oBL#i_oc^e)NPAMeW z2x0q^#9)lUk|=mE8qMh;zlYmlD0XplVx0Gxa#h2^K`ODCUy0S+;rjptaioVE{%;0Z zJAmT&%GKCzaW;zv<4&}O0QC(sd~%v+eFptdSUhVD0~huvY@KInmv~Jjz@aN$Y2hQS z7zL>_AM}^&f-Yg&)H^FevTEWK&_`Pa$=5&@WSc>|^0RMLGSgyI@NB}dlEL{p9004N zL9v&GUq*yq9Ue8CA4!o(eb=G>WA!~f!nyX;~tW!2vj^jlbb;un?elp_2zVC_6WJ0(kKQ#?i6-{8)ZgaY-S^ z`hIHvx7!rS3%Rucy0ADbV5Hmap6{5Zy zYyRWNI8r008XM0@{PS8=(=7kDHXB}0SHSPPJD_FSbLGD{suTXCjv*E>Ob9FV$cN1i za4wac_>G|r;AOzzA{*-dOsP3pdEtW6#DW4+Ed@P$gX|rNiUCDD4DJYrH?IA+`H_eP z`yKd&{ZZeAa;se-2D+VVIk5c+IB>-AbQFGMv1BuO3dUCN@9!mBC`q~*yjjgMYXxvw zPVRS$329n>5ngo;DNv0j#mnAKPt?e{{VN5-ZuOU5GP{BWSsAq@lArQL7V-rIr?C_^ zNUmVKt%(fau@obgY4A6L#R-KZ_5m;#U4!~ z4~s#uNeO+9nX;?=;FLB%pzyBXP0b2Y#4ULaqVkU6wj9TM?qYl_r1||(7gfi&r#Ay7 ze87eZ2ePWs4AIp)H5t34?~#NvMznN{T)_Gfd2+d|G=)%{!|5JYyIFwB#YR9%N>jk- zrf1nK7G}{iSqj%pnCR{nfnavc*?eFUfi@q$|G=9k!-lNq6pdV>Akr9&RETWGBvc?t z#|Dl>ud9S^fI&I3Zi`bmp?-0blbNSnbx)7~4GjGdfOr;WMVpXn=jHg*+^tUS&bS-w zE5V1Bn}O6n=|y+w294@D^D-nh`TwYXZVrX`)$V~eT4wS|28$Z6u^6Y%$ijR< zp?rv7yB7c(VUZ%4;MIaCsC~%Q7>gBx7(f|9%`8|;L!t`>E#6E%6{3!MPjV2!j?D$z zJ|-c!E4nt2%r9}6n*%9W_JsI_z73|p-Neb_UB35pD}2oBf!PVih8tGRx<;IrbJ~eK+&d~Ux?Iq^Wv%~9vUf^T1;0IfWIfN+P{mI|vXpe!Phq^c z(qJe!!Vpka`QCsPQe`Ana56hE?Dpl|*~z8`sGHO`R(BMhm1nVkgqCJdtPUz*nSl7H zpB8K}DTi9-e@oOiqG0~Etq%O2s*ac)jJ;TBs0c-OSkN-!E6N77aizE(cks&tCFXyb z%P)7q?b$~=lcAW4YK)j|x89EZDD&mx_7WC+wAxqctVn86Jnx`LE?w0A#pkGHdFoHm zcquj%lGF%foiWXsmg%uGx;aVSw>aP$fMm$WPp`4{2!ts6bPW@&Y}ce;zcc4f3)UGS zJ+ZXH`EyJ#^=l}LzRH5jt`st2g7hxOGxX^XgO5qjDectbxX)QR=%a0F+ zW$Uy(*MF?{FJn$HS;1%!_b;ZbOWr`rkx*HL$rLKAEde%D0p4w(5ggumDS_%=qOzKY ze0&~S3nXmCT(>E% z@tV;l?WUP>>z440nqoC-DZ`kc=@N&S8(uUAzj2*V!TN~98cG~rYS?qS#6-?G|4gIThKS>KelfHTi4cI|MbMS);`>ny-8xLWcIa1fU z-3IX@-U;bFMV4%r7*yAiV%7Y^6sjP@p<+E46V+iaeU$!3Qd)FluI~Mtpqneje7NXjiHMtxVmKW#2Fw-IQTy>1?6%6Q6MB>Pex>s%p2OmdF2~B?2 z`oU0|<(K}O0!KF>?0?nolby1T2Aa`i_It1xnF{Q^Y8)33aXnfK%lyJc5gJv|*im7U zIu!FgI>iaR_@)a7RLE>py*->J#RUrAVWnmOCMwd&XCA7vrb4}i562dyu?jG9n zr5t@0AZsM&P`Bc0&EFsb1aY~NP}RY;wGbt_@p%WhjYEJG&>nDyKs(}M`;-_o7})@O zTLL)TG7wFau!_2XPYqcqfZ4*Z7*9Qzixz$~A&Fg@=xy`2yfRgH_?}%GNirm?Z`SyI zLPw^RF?a-J&>o3s&xR26KN`l&oTWM`5qR-SOU6D1_`VS+VHODfCe_>W9^dXA0`RTz zmx#hEz-VGN(5zclGp)DGBV)Ul0mT$z*jMf|{rL}OSb3bCe^qjLo)}`_TJ!AABNCfZ z7;td$UVl0sKo~P%yg^;X{SAb_jDO5E$PJ=1&2hsw&)8)2Zk}J0zZg27)pg6pF&E$E3cXwz*N!01Db*Ug9ZU1=|Lc51deTzoRq=tBK$IN4e!K@&SE11xN+Gg}z${zu zfT@6s;_yP&WY^1c!M5TZ%TH)j0d*~!cx6L?Ivl|9Z_&ylX438)nEN z&y992aiRgPF2c)5T0as;i+J&eNTQ80UgPUOs?+rox2?+;_aS% zzV?xcp=qDCwhz}=mi-`AAP}|SIL?W{+qWX&8KnV2$AkUKQ-{e)+koFNpX;VKc};S6B#1}#LYMG(;V`7nac4^ulIQ; zlkgr$vun+cJe+~L2Di*3xU!fP49vrEUX(yo9ITT;b6K ziK4X5fC_Bj^BVU@ET39Kx4fY9FQ2)8@~DY;j_u8{*s}iH?HN-X69tLo_a3QKS=|_&Law)(MKR ze~9E0&U4))`r@9qettqxvS~K()Jo;_Y00@6(+rh}3IYXBt`k8-8s27d%OCMz%HW#g zia{3%5^T3LPb!cuIP<1At0J<4jS{wQPqC_kCrycba&<%56y3aiAtdX3jhpkmy>#jP zH(@(($dCW#m%b2spQ}QH0^Sbm+RiSMqdr2CT7o5v@J>;@zwJl0glf?(V~`?^4BPK* zUfJ#7ma&BmO@V8t8WY{?q+dR~l>Z@)ydkaCZW~kQTgnDt1H}j{3sMW>Cr)$CctH;$t zvZBg{`AU6l<3HBqk-Tt|j_V#DUMX+QoFIDL$B9^n0vPn5WK$E!v9~qcIxvP298TDD z7V)=ci15Lk_#IGC14qQX2%UrmRce+4bF(ECZm^uFV1?|7P8IOWOpr=?ji6XX(nPad zB`(a0-{OE@Ea4cWS{^gAI>L#Nr&SIJcH26jAlXC_P^K4gqAHLCK>$cWg-7eB1ne%% zOZomjQM>x;SZ5zjaC6ziwj$yFzc^Ox`2I=@Xe%hYo+GrnH|4#?p-O}Z?ZPWqd({u! z;C@0`q_5((2ttK4YsQBLj19HWr`pJfWKDkU(Fe^#p|JW7syHH-2p7_OZYGAy*X%4AP>Y%Cw_i z%A$ZKLJ)-PR+y(+qL%9JBZp$ES^;`p#9*DpK}--ncB~|6p{{U#_C|?D3-W(fw!n+) z#mk(eF%Ec-cTA&6fYatj2hbyRWDVKJusq4OLyz;K7!k^_<^0U{NKq+Bq@@9i=e1E# z3l^`Trl0_>%ET>m9wy8>7ya?jhDG46{R7Z$;jM=ztw38FlpJTzz(Hc8;0_*tNQmZB z?}ncwXe?ahK})n?YlgxuD0GH|?Ix$I!_LJUyi>>ARVCW3q%O`x&BkQpo$8Edc_gYa7HuPYJO;C~ zCK!&@tjP>mrHPYv4Fo9zp{3qJpaVYL)|u4Cp22g;=rFBHttsh~B^);lY(;knND%rf zNGdk1Adwx*2v;>TPDzEM}3KnSxe)2DX(a!$G=+@j!{9iEWMbmk|4 zzuDZ#s{2b07+wnzhbhN5WltQ@%YcM2i_0jSQEUT$?)JjozRzPt(NUaw&(CEyZr1bY zP*#?jJxX6l-@Rb(!c;5DVRD8GdA4y-rQN%5m8#X$s&HM$DAf#(^@+5c>-JXakWM>Lfa`rV^m3?bCsMLo|;0{80{-fs6l8shg&cQVOj7jkq!6%5O65Sx+VZ7g%V-9YoUa%Peb6_u%{@FY?Kg3Tyfd3 znG(eSmy`}G4DKIp*`tcj8YTM#GN&b`UWJ99tffPN?TFt z{`dKBjpnt9y5bCPg9Vm<)_2^&w>Cem_OL@-D$#~1LRSW_7wDz~MPy0Q&TpNyN@F$y&U#sni}~779u#5RqB3EJrzs4;oBJGeo;eh7_4*RIy?C0<)1M2_%XZu& zpz}rSw-=p$kzpQ>h^K0JBJX9<(D~=B7U3tl{ur!k?c_>`Mj_>r;z6)Th|!0L?d?+p z2YBRarT9k4WMF?rcz3_^t8p`;vzw#|zVRxcL#)N|zqz5stDVn}Qhs!&oNpqf0Q!opb~atV9W^=!wYW83ifzxnN`j2B$qEanm?;?h@V!|oqZu94yOE= z2y9+k-un&BjM^#wi z8T>gb^x_77*I@+<({IL~(u$PPU)>a?IgpRUks?L_k9Ihr)a^^!Zl>|UwlL&fGN0jz z_g^RH`DbRkgMoLkTgRd=Db=}HkCdCdnfn_6I)R|xC4IL`eJcV{sxY2t=&1-#9{EaTb};wE45xnO9f3y*XU)FVHp!moA2N;Sb^Uf!R#2N6|+dx>+Y z!=r{?+vCb7D>C%Ai17VWUgE>iys6VOtb73_08tQw z`>sWrnsVJU0Oj;OQ17ZfU_bfpStn5q2P#{eK2h_87`)cI z;J~8LGS0Woo5oP2i(m0IB|qpB04m;$*&ZHKR8~p{A|W1GSjwt2zJ$ucq<35VZ&%^F zbY<>k*z0e&#xiCZn zWI&!bqL>u+;Yfz8ui}?(ls%fI6M{JXscd1;XbK>2Ms*^R6}+2oyXAVW9ny5VW!W>+ zaC+qWESt;u`Y`BAo|)(8zs-dqQfs=(8y+JS{bc!#Qc##@@pHEP8u=sOCwHIr%4Pcq zFvkHdz<6w*m-SnO83*W@nL;?8zu=uiH*$BOdHQ>&i$jVqKVVMBc^U;3&FzzXD* zhOXXKK~nG6*XrS={UYO6)^BPS@mBsM|7~Hgwy3VcDQDUtJ4;f~8JX-d6BLK3q&h#~ zJcl8bm@|dpP}UFd|CC%O8;%X%o1Wl8h^slA_=s2?fW}}Gy|rC40JBjTy@+TpsOa2^ zpGfKH-~trT;9!4~nCT<~93E0x+fm7fU2}f2Rkcx7XG2Lb7rKkaN8AjTAK?LlYdXt$ zCnrO3S4jM*#XipH&LDvuC_*cWDl4Ue#pcJ^E4GL2g7B=4?*@o>`mtMpC({%A(mU8ZD^Y;avo*qcbqH{wD zRfdWl4W_YRa5lo#@d^u0V~hzsN3f#Sb5sP7P+6TX9B|ZFcI?8Ka<^w{k_eXNpGFDb zpB@ka7C}_gu|%}X=Q9rlD%YkM_{=`ofdx|%bHvD*SCJ+AomVl)5~YRouNd=6jy9S|0q@pyRIbc3iRid8n^!8H z^DOjv(1!hvey5LhZ~QGv&uzaqv5D@QqA_f^yn+g12?}^?18q-9=?3$pk}w_uSAC9x z1hGW|sNL|E!SLwsO(;VEm;6WtvD059@flm>4$1b8B@0HXnzsW>8Gq?7)<+gipI6mj&4YVrT)3{AJ-nT-JS47{4T^LqQ%jZ-2hJvl7h zrK%YvXqv(PVXUZmO{WXW8(g|ZoT^yUP6Py;)d4Wv?a~@g2C6-$5`45Qd~oW1D&M{_ zr1m}D|IzeSVR1E0w*v!%ySuwt9wbU8tyUU^GJv~s``mE^HPA6uRi+sTmGOuy1AVv*3PLq?HVuWeJV1i&iU-x!mA7PUX#jE>WA>3X3EMV4R zIP)dIdBn17`rnVvvq`AFJ>b=go5q+IfYdrHN|Ho%FHulymPe}s>pmU3}-(sJ)U zLN^NMJMi*ghhNRD1+DUa%{e6G=H^Wr*{um2Ezzu$?1MUmf8mpj}kGsCl$NgnnvL? zVbayq5{m&EdDEn%kY{+|Fhgp)X(N_bt+_glhKJ03o~A#xjMV&MGDn6`;Ep3?$KliI zA;DU_FYtll#?Nk$44)A+?TYbuW*mxI+(Tf$f2IY5(q}em#&QTkPZz-84+mvU*_Z=O z^ot6H)OLvpM!x8?_Dq(Y1fHR+v+GCEe>-mqdkuq^QfM2u919Ho*I0fH^@e_-47?fV zr@I1g=7Q-xbhea{(b;1H)b3&9VcFQ?7QtvQi{gN$DgkK0X2$7$(p3XERPAHrX;Vv> zV?xwfSPg536kz`WCq`rND)ub)`bZ9k`B9j;s55c>r4u@KFF_WGPocvuHdV>6NDGq& zayL2dgAAG`d(!TnWH>)8?Mio|_zz8c2M~>foy0u4U$?%x17aEPF}WOCQ&=>sdL|i= z{gl65@GRU(l!m_((SNB;*7$#Bx4^^=gXg&y7Rp6=vTE^LA|eqF-!xQjkx~{<9@IR) zGOMWV+2B>e6DI39fJl(wbzjcy=*$gVii z6HkI4Q2#@1J>AWKz=UJu;7~MeQ5opYxJk!t+3A2>Yno1G&E86~Aaa&X_RQ+O zkDw%HIxED>?dwnh`jQ6vAj>l8E=2Gr7&FT9@yT>jr(g5SH{i?MCrEfIuDrVHLDy$g z@^Fd?1~<6XPY_s15alVnsOh}9XKhf_vURedoj%VWo?CVBb#FwmJ~7IlZ;vK4jrBu^Ktk@53t)DMFtvYVlAM)o(z7m&=m={FMOKN)D3L9M!AnE^<5#B zL}rO$!hS~Rxks11>Cemn0ca31%$rJaOHXr($Q#n4+mvi|hZCT4ESR5D(^a0{^yrDN zgbI)}2^qYZQUYD-HI~H^WHu<6D#3gYo#rL+#ev#SHP9-+%?|a>%U{iR?b(yys}4Cz zJNM+NVmtgIhl%G+Ohaj`pKM7s)ye)YLEro*H_ellzeR%7DsHqF#R{t9NuHrtx^EoX zZP1)Kgz{kWbZc6R0P(q#6Kfwp8){HdiP3Dl1D=4P$%6^odtaR>nJ-=BCj$B(X@|Gu zT-S!7!h>r*aKz+PAfTVL?JUP9^^l~HCmC%)VaV8R_rT0z6EWxuG9bXNvO!!uR$uip z`@qOGL9~omF`W)IO`328Skuw@8$YrUmd&P6bchpvw!x9y;F%lpb>#x9!6E1>*}3){ zQ0f1@;=2|mvtUP{<>u6HqgEF5pEY-(;RrImYHg*ySvMc#xfG4p$O_dP!5f(4IW}z3 zibbE8ar8D33{Mjla6I%1gdmT+_&YBW`XNtD((c6Cs=7t(@b#|+!iAbrgBz$1>!rQ* zz7o-kC#vvy4s;^jGUay|FnhgkR{E11=h2g8Nk*}}tbd@pTt@UkR540GhG-I6I}xJi z(#qbX+(SKRy^P#I-#jQ#c+v95lLUDIgb^_e8_-OhY{TMCEGv7)E4#EYroFJ{^@*+? zho0q$=U+8~fZ3+@TF8i*F$Kr?Wv+a4_re@K?c-aIU-;eMSkyS_%F^os`2OryMi7OlY=I8Gu1S+Sk+QRM zbic|K&pE?$-N+)JMfzXt7_tfhP%V&cPl89Jae$LLCYW5(ZJGj=lvx$c-?92*hrd~> zn64QuKFaxB;#$1tw#TK+A&lji!l4C8Cu)RyPwi3w(4w|!dvQjwqn`o*leOwZC}vKV zfcu5R3**PCe>4}tBYpZ7I4%wb4Kl06jATY3aUjtxzQSPk3--wvDk333=bB2v-UB9R zTJTrcRvW9qt@(eC{h!Jwfgu{`@EKA#>!f+N=w|gZd9Kf z)~MESNl5Oef|70`XO13R2_`Qud&$wOmlB%4v)u2YAJAIE)p&sK2s<}(py|W*RVb`q zFwLpQZ~ zs<~!}N4D=`SmpP35#gVxNIJX1(1-K+j>4CZ(|PyTMf(`sPBl6?Jjiw%DZhJmj|A`7 zn$8>~9*{bTuAd1Wo^&m_2V;VayQ7aJ2llzzwk%40-bG@c&Zu;=wPfA=Ua;DLjV|fg z{VSbs(3f~j4b^Vk&fi(U3;>AfGrcdcll{-MJ-qrz9Ls1NPDqjPDm@#OZlMP5Xy*9E zQc6Nv!mgeM++x<{In3DR#qFZYvHdsL88c#BgGCjh!QL9zc`&zBbCukrvom#}2W}D< zIe=1p7d?c#Np|5oIB#xjAfpa`+SJ_M00_nRnDD}m0oq6LrQb{dzMTba)|e1D{RnGj z+rU(B!|$$H^O*V!tt=ewCRFDt9Fy)wouJg znR8T7NdRi;%IOxXA3>hSI&e^r2dGfctENi90U!Rj`a}nu5C{2pg4@tp@eClOZCf{5 z7oM;Q-|y5nPg*AQ;3Av9A#3P!YoNEV-Hm;G3l*zLB772OLt%`0K~G#Fa5Zm;cX^QF z^W195qFVVut4VEkMP(KuE&I{%eq04UBD|co-cGweQ2Z%2zu)r$GBN}B6e2bQaHn7X zp|Jpsgj!31A0@^PDd2fe5@Jqa$7V3{9#!N#E%7|9#j_^f>HAfFDE7f8<>^mn+sTrB zJ@nZ>dy&!;j{2MLq;Q>|*RIs^Q^kvu+hh!duGAmZQ4vy|fhSTnEdkM-eq>qD=IbV_ zb}qxJ^41LARytCF(JRvr@72Vi#zsQ8_LIzOe!aUKAwG7CN@8()tF$*AQ zni$jICCo?G8#~K*D`H9r7&^k+Izd@}FP_dox+v_EW9>n$ALjUf)-lH+ZiB)#kZk%U)Za+>oTU6) zB9ZD)t(yPUy>V}`JD!5_z$gWAxvkKpZ;>|-t>@Kq>+#bb77>B-v_L+~qe)&LfkCF9 z4s8^A84-)qDsKp~t~&;5lpX~-H=v}z0#$)f{6jczggZF0QIS&^HW<b3&*b3&mMzDi zRMD2EUe8)=$xyF&c;R40YxXem!Zc+<(JXD1!>%aQy z<50{G^=HtB$Vm}_@YOnV;AN`0v$cAY(nDq`Jmkw{I0^IHheIm)0c1*rM2b2!m8m>7j?TLRFi8=f*%TeM(? z^n6+vCndiH1J)EPQgtP$ho$bZ273%j?!pOjcD zEg-b}E6Zw+zvU|*2kWzMW2d>31v9#%5w!C(bEGn4yWi|wGYNLFu;Nlq`qi!LHEm+{+#gKGFZ*$@F>`gl+PA-LE}!m<5Okq%(|OpDf~|mHXOb=pl1x#?Acn{l9xL+VTSs zQEHP|Lu+G$b=T<@6QQK<&V{Zdd2+t&ad`J1J*)RrK%cYx<2LaO?)-MOr~iJp3;7U* zG0Z%ukL)d)uvLMM+m?VIy~be?WYY$iP~`H4^W?1wPB=J>d%}L3BjTa@wWW`0wiSMj zY6E>5x43K=EEtW>Cg2H~-**XRd1aYCKbL0tQD6%#w}B*>^}s4ie@gQ0fzphD$skwI`+O%V=MR~wt7S=qG;p%dLG`Lr~L;QX+7UbCz${F?i4 zZUJ)qRwDUY760SZ~gt3~*= z#bZKELmg8HMpbk@aU9Rsq}Xzn4YPgvK_`XJ;lxd6(?kY3@?WoCC%L}%rSigm6Fs=jM-k6I6${O2-wt|X%f_GX z;j4~GpE~ZAo?rHHB}105xEGA#K6F|G*-w7j`eYAstWF~r6J;qoQx5r3JE))4?~YyE z(;w3!xjblww{Zai3f2vLW842gpRodglkmkp;;(wucfB0uLcsW1OlvmM(tmFc#`p>^ zeQW_k3by3Uo2! z7|&Dj6N15Xydw+xT=8(l$cYIrDcd8CN~q`FGAQOhh)o_6ikM3sH4BBR{{}MTHRo1~ z;qg}5|5n`3slX~hJj?Zxsaww6hxJzjmZ%=HkV^7{2MWW{AWo+V`DPQRQvWCts2kAU z;d>V)qKmdjfvks^-4Z!fFyLy7yePxJ*LY0PW-Vrf7srXa&Kh=N7UZS`4H}+qH~Hqt z#1sx~1i!?zeS+fsQt17X;vBG6a~2jrHwW~?F}!nx{v|LfWqUMvX!ko~QVutb&ALYF zawx6e-M>?!*fQwbe|%`&Ww&BO`lrN#%pKMw3qN@^5!|7|kG3=mvm%)5iEV^33^k#sz&r=?ZmH2SkdQtRL`Hxmy`ZPg$VhOe-hRQOSe|A ztg3P$oZk|HT%rAm0R|q}OHz!YhjTFCO{*Z!kJ{azPH)STB5Jipw6)_XW7NR#XuhWT z^Qydx`Ne7RE0rxk_Q#O}Uz7S9{jm0QMC@{i$#WBbNbb(sUogNFfC9xH_dR3(LcEur z<;Sn!|5FJ3l*lL+?+nNJCkz$f?URU@jIUhXKtBP`a};pS=s76!LpUTw^yP2(913m_ zzzo0Hr$m`G`J;WHft~4lny@4`2LumsbWq4feq@RhK94KnJ#r-l#|(MC z+)WpPPNVJ;5Q_@#Ui%&`v@c;ees)!|vR$x=8*U`480<0u6HR02Zsw!g8tRov&NnR9 zVu-yKe(p+P*tCy8W(BFC@qjjjm^8Z&ygh&3x~+)t8|t85oA zEPUSf2Y6wYr+$A`L#v@64x4)zUaCdJ(-C_1hWp7_9Vt+Ud~GgyM#4yvC)&03z6I90 zDwtxBDnFNnK+CjZtomv?qt;_^2C^uyN^xLRBG4;8c5q!P06)n+O8agl^9Ms*GJq~G zAlnc-u9$qr={vBA6nXyX?=A9)jDAU+V@rb3m=Z(>+GTvRtickbeaR6KF!L)0ROG5q z8DOJXYYIBdM@LbtGGW$Nk0^@MN8`;^YCYNM;cKLMPBP7dQN>kdDGJ#NvbhjjX~qB} zU3MMZou^TP|H{(Z9S^_ZuU1R1;|{9@zqVJ*Cm*ZD%d>Irmfh~sCDmn5ujcoVE%)Aw ztipi8E|aB8srN-uP4oLh!yd5R_f;Ix%B>G9q?^O111Vw9d%auXn_?4eid|5&-o_LtiBoLk zgd+fi!ye3){vq{9t9!GRz}0YN9YsOSstD=uh_c=4MbFDsmjK}F*&CeUFT?Q*wd>vC z#nh!;F|=Nr=R%h=P7whh>Tk*~QlwAnF}AeB6#5cLfQVCn5>+p@Np;EZmSd#<&7$}- z#Y##@7xNf!VP2ppWFyHkgoJAXgTYr@ zj_ZgwI)BrV>sB#e5{L4OQIecVA_qNfe(;!!`XtsjvdOyXIpT?2RaLvkA2%_df%?7& zNqKNRNanh?%D2Jf=SfvR*6W@;C$rvOSQH-uYY4oJ>)Oz!zGH!$1td-d4Lq`1-pI1K zy-{!LARtHy8JWmuPB(|%$=}RsXS5hQtCg8abaA+`E|mmw&dPA9U(z+}_E2gjx6`QP zx)C6BXrJaYQ+Pm#?;9K`j-ueGioY|1z|@%%>06Hi&8|v|^m_0!87@Ap;5K)3G}vK% z5tr6+`?&VWd0;l)Z72>UZPGIH`V9rT+huQ_2u)z|-L#R(=RcF)(_I9RtP*pKyjETL@C#fT3%%yz3YwajEUEaf zoZ0?kER}C_tvBZauGk{sdTrdrk1VS2sf1CXQ#aICS@i+1-X;FNDu1DNpFVy%p_A1j zogcJ?q|pvf+qZOjws$};k=*Z&F}*%t#k{@d;rTzV*BD{y}#_BTTr9%^ow4 zsgm<(>h%+M8hHh{s`{iz3=w7eSprBVukpt}FIIjz|H-l0NzCW_fi9#Fr(oEp;S$a3 z8I@Zqaptr2Rk>cM?wQy4p3m>!3C-oQ6l#VGw)9X>apH?!S88@H3@{fjEJ> z>GtS0AoGT<+(7pR`IR&9Oboa;e2lv%hiPh#rGY72XlBt6o?PB>Ba&nt73IcBa%bQO z8!~)3{}sSaGZD_?r)$%6g3&q`7yL(-@9hu(owLvj+|9{C&K0?_RNtL3;UN zapz4nh}-_6Rg{lzHpj0N&PLczXYe&xt6%R$%#H06Kw%~5VHqKk9<|nZASe*Gy|ZKI z%BuHvhf*}YBOCfppI{w)sA5SULK{|8c$+)ZUIt9!s8*P0*|0}@If46f>3E!jIo^oS z`ChCbgNL)Y8UT1hUi_6)47pcIy5Wmb?#lTrLI*HF)g@W=+>(oSBv7m9IU3p?Gs}9P zo}{d~G9na-SP6$Tkp1j?!c+PgTv6fX@084<4jRW!C%XH<|BVxYJf|d6Vb>clMW|p) z(?5_MHB1n==U-Xvbs$*eBXYuIkXO@)744<~k^6ys)`x2(U^xfS=&8N~lP{{&q_1-X z)ksU2*H@cCWc^V}_ZmvNm$21TiEK-4%tW>8(8}Li74IOulPjTPEkxk<3T9$}r!z_b z-fbL<1~WYt3Zn{z7M_z}cPTTId9-B?_a@sakhhE-6)K@<*EeA7{_^CcMtA&3s>@#; z%d;JN$h3F)R?Cd|r90b;2}#?jacqEqQ4g^P)|oPFKb!-*@Wu+ky;O?i@@C@KJC`B- zj~OMa%WU16OanY_4x~4ukRxl0i^As+z{TedkY}#@2_4|LX}`ET18jSf@!SfvG@~Z7 z1*i&;$R!eW42ZirpxAzNuQ8n;*6&=Aq2!5hu?8wJ71X?}cwj8b}G$Mf>=v2%KKYs_c z#q)+-b`tm=AU<|c&m-|ixQ|1N0|Z6*mqoAM{qsfka~YSdO}CPSda2C-`J{ z<l@iPsbG|FH_7-51Z) zQ7_Bt!gKjl1UR6brzcrl(g%(8oN9(DxaC8kAM$zjbRJEc@L^MOnq_i+j#n~m-qoJkDVd7V(?#r8h5o`v&e5b8S6+Xlw; zSCNzyg%P@+Wowj^lnOj}vu%AqlTsz8kg4MS#G?$gisWSA1G=~#+S5#ts zecpa6bq4=PIj4H}5VD~4exNU3@+B#=vF)IRS-exh!WMZS-PTRx?i^iD=~P(yRG4aB#}7`ubkM1c7IxCJ z`pxUKl`1|P(m?^A@X-`m_dcBcut!n8Fd@0Q!bsd$tnT5^!l=k22R;^-2m9;At_2dU zwmYmYg5TXS99D}1pd0}%?IF7hG4+hghco2(b#YZqqlG+7A1}T|g}@S>1FuI0|AcRs zIpRRpP9^)y@7GxC>vF*DGfvr$$(Ggs!IMY>k22M!pBD?382Xs_WFmGhyk1_h#VGL( z=E#!fJ?^*w1AEE<226YQe2Qc-&$ttbl5O|e6Imq1xVoq)sKQtYH`7BHf(RL?oR3?U zl(QyJL$pm*2S(1sx1jO?svRWf!=hN2kD-Nolp6}vzA-eXZN-714}o@Q46MJEQMdXV z8>`wKjR`L>9cG1BB(t_6jKr!|n3W)j^V&SsmGA=a2Qi&w%N_A0?y>L5#Mn?VfDSBQ zI%1uhC0iQYo-JypH)X28iL;Qb{9+ovW%GOh{<9#0PKQuHGYIGU^)A=(72i|R*yv)q z4(;Di>c+cBn4wbh=a_v8{VmiS73~BdF0bqd5-4-;K&pxSNN2wX^Yw{p7_Jo_7Nl4( z3l9YxmE1ZQdNMk1S(@EBe_7yI_zz% zsLy|ie;6Y>a0xiIk^z|d(b8bPdY$nmoD{aS1K4!Td5@j?TIq0p@YSMc|K_#MSoUXN zL$nm1qu@=w`@MZ*Z7fN7C4Ab!?|lftv@}q{K-w(il5Q#*MvmmbT&AN?naiL+#LGl1 zH3$y>W1>;978dzKr-AtBzk>P=je9MH-{~fnny{qa)Y_a7W4NS0tXXH{!wp*~)S-eY zDmGi^vOJeMMlB>coy1L``=Na>+9)m7_?H*Htug@Uc!l9KE`tlr41BsBkpU$KKM{ zSU{k~ynp_$;7|a6dW#5d73p1_CNNgb9KQhG42&&&vZ5&R1&%buB-C^9L`M?z{x;(>fRPdG z13S*{i)~fPrU-+vNI4QbPkTEzQ)WK2d0&r9`PD}XQxcKKPo4XEBtdN{^V`fP?eAuv zFy&zbZiFTjZ@ajo>@0Y7b*}^cnPlVS^)>BZtLa`QO| ziDf?5L`8*av_}FMKTNlP8Fl{V>uRFlgbgvo+hK)Agb_F)0~go4z3;M?oTpFn0q1u* z`W+|#U((IcFgow5Kgl^M@yaz_{Bxx=4wKWA?U{|6(&TV2ub2^sL)Uu0QyfR!^%6-$ z46bR4IiJKdI4nlD#O5)ssMQ(R7~0`36jSB17N--7pjfXvx<9nrW>O z?tXI#Z*Cz2jfkK5x*VxT3wa-v85`UL1z4+UdP*8~f38@3SGlRD<35=A(|&Pqn3n%g zV?2!(aouM&bb#v;$BOR#!rFp7AZ>BIV?#^}8HIu~spQ;jf8Yv>|Qhh2DIS z;DY?hC~ZNptH`^48G60fv4M70L1;ATTfWYAc4K4SDENdTJS$Q(t24`W{0@R%=IRNs zX4kCe>>7*whar~$X?1(;_IC7D%b{EODjp4bj*oXIy$s8rE8;2x#v`00_~&3LSbee8 zuzV05kjieHY0oan%}D6`VYA0;b^XTBsR&mw0@K$sOXog@*UWI~NuI2B1212f5yKNh zj=Z`DA}<9h=HkXaKPm10)Iv}=cH3d;pYP-=PNl(u$*Z^Lf`TIiaARG3MSu3n`4xAo zs$E-iRpQfk`g8l~mRT&|vS9Sx!1pdznwb5nR=*a`-xIAx*}Frz|Nbdt0e~nnBC7{3P?(N@^8J^c0PXzFg>fw zM~`aeTEY&iB*A4&!J0ISvO^U{j2%$I8kJ&SSyQ%c+7SBnKy9VbKo)s21Ze*l_HGK= zc{Ih9EI_&@C-$qK#FPE-DdI}-?Jfv~8fuj1Nz;|+!mbJFa{>u$bNO8fG$mEUWTH!d zBu&|T6xKkVy2Ya@)MFWL$zk+EK4xOSHNhswAnvx_o7&=U;OlMZ_Y?5aPZlK-i994 zaz@UlD%^CJqQ;aHu{t69ppN7-widkIA7JEl)%LabWP`w_|ZHWwG zM7k4egR1ot-$V{!3C3ip*Q|0kGhV~8ZHiddLpv;E2CGk77abnMB*aDcj?xt_l<98K z6h_n|ks~kJa%vqr|5YF2_vz1$iP>Iv3X$)S&3%t$dPhZFp)=8wo}Wk99gIA_dpBf* z49jW(NW1u@cM5Tsal@vbU&p=$e+W-TAd}(mZ-PxEN2S)~EOZE+c=w0!@XlC^s#1sZ zmK<9E!0z;rFRU8cBIVK_#Z6zId~aOgmrT)}L|57@m$g`Iz?JG);m4f#RGD(aHM3?} zV6bzvlUXjPyBlG$A^xRfzjBq~>@>0@4$VHv!5oV$?9e6pP{N%p&tn7j1o6=6qrB8X z2-L1|f9CG1N|r7*vJSbF$ih&e0>Z1*i1ouyCEVz&wx93T6f#16`KdrpjBye!E~bx>S6!$&PmE>YKrUgNCjFqF|FNPLeow_Hi9JBOM&rtd+)Vj^f?Q+?oCl_tM;Y ze8XLU61smbilmAgcOtz=_?W@Pxx3Wb)(^v)IQLtTz%~RWS943(l(1})DD;ClJLD1F zeLa!#^ZHM(u&hj^wf!@n74zqD@)#i1-zDW#{IFnp_>jQ5*!9a znzpFnu%N9InGv?6a)y8(h^4QERpztoh)U;{XMNVrrt%!{LB=(qC<)81a9OBuiK?E` zZc~%CKTe{VOX2R}5j7EB4ythtad$iI+=~*O#|sk!;5;DJsAVmZzJ6<0S8 z%W5O^QyWw&ixnai=~c6thBsQ*sTlu;#c$=IZ>|zEmGE1cnwwKkdUrLu`O0zfH@sS( zcao=?UHMAwS=#`90WeJo#Pp_wabAs1)1@79D52L|!UN4329}6JmPL?(k8ruy0ncic zapZ1iI-th5f~i5fO#Scmr>;t zDVh>ku?xrzfsvfydL`&)qS~GLe76CH`XHuSLYahZ;>3Q5HlAYI9=`4S6gd_-Q@aay ze#nagUv*s3jXIg$HQ~-9Xh#GUKN1gCHx_H`Ox*sj+bLeUoAxqt`2hN4G;lt~XKhqge_RYkVDDLPH#1a3dDQ~#)oFdh^M9ur-|x0tI0%=CW4!C^*b78H zt~w6seE`(f6oC(dOIpnQ6EIVz&wo|NCf2HYIukyjHHFZqVk2$>HiyGdnv-Uni1$iU zl|Y^slB~N5kwmqa%&$nt{?PdTgwhMR+ni4eFed?4E-%phhYWfKV`Xp6VDcR#Xi|2| zp@}W%uNJb=@?(l>*-GW?noV&C(%A2LL&TlqI_BvWM;K+JQ2~a-oZ62*ZxXs3=3I3L z)X+cpz%Q5!-u;~@t;17J>7&a{{RUzDO+L9}EmyGBh-(*~IGA@`^hZe?{f$@bMLeJE zQLHBiRzhFpb5q%C_2>sW%lsDmtQ7^>)1<({};>tlt+f zS3IwU{d3Zkq|RHhDB+<%M4d$ulDzbhm<0oZ1gQ$a-s6l(8B@U~GPT1aHt6g0dbcKu zq7s*4#t=&aPy^dTy6~sB$fz?Q+x8V>9G(bJ5(tS=VDfvI8XyyiQCUF!$|&!&0T-^@_gU^XO`H|et8|blQ$}Fi;Kb>O8wJ5=@cSwy&HAR5cYV{Zh*W@=W zs6>lrxcoC{{#y$ssi7!l2NstQqo>0oHFFh0`D%Kc3F+p^Z#nW+k%hdhx`x4X_qla9)M63NjRt@7!PPSxX`z{FJA% z`yoEkdOkNgwJzLxL6UtDxbA{AfpiTof;gFI#XyG^iESS9Qv@(cyIkX-K8J_uGf%F; zJUMnz@EL+JdNk5aBRn(gTmuBjsD}YL|DLsSm6NXDebh!b>0wo}VUCN~tubDZEf=or zkrLQz)p@q~KQF-FsJEcoO^-1b;y;)hQ`nyj&mrew$h@H)baAvT^762;b6(5V?noi>QS5K`GWp);d=g@_mcZ7W3` z^`N?qL0pqR7FKzkB23vg$8v)|5=REhsw3^Lq<~8kdJ|iT4MCs< zX2OMy+E#2~8V%iGt?CvHmHatD^k0M!e;WG+NPnX)h1N%~hDC}iY^Jh*-rjs6ga~}B zI1Z;>;~HkgbP!TK2&!mMQ03i7hX-Vw^JwmW#_87z<@Pw8^bs#;ROZJTTyZ2Qo?1J#UmSA z{IXGfqi^qgwU0(&``#BfE$!_hC?x_h5JeHEDO-2v89STVcVeAZ zIF3P5Y5B+EAW#l@pZu?!@fSp#vVQ%w+7RrX$x(0#e=zy*T|2eVizdfZytqn=H~()2 zDplCI)I_=w)bU2%!N{ia0p@{Qe#*^8!1Quk%Z7(uZjv~~sfi;%HspuGwI+nZ64#c) zvfYyO3vUXiqd8(Z0tvZA#R(T(DkD$2ovGm>O7@~@Q(`!(3t8f=4d_ca2OGj%D$f|V z*q85=y1)`x-A@xTRnYJ>KCF}t=p_uXR(+&ne9?{FFtL1i;Cuio8Fhnq2i4N-g&w=a ztSl{tb}zsj=~YCIN&ZiNH!W>r24sCLOpLS_8jyia2B6~jK0pP4(>dmVhc=RrJ2&#$ z7|~Z1I4n--WV<-O!_E9eAO>A8P8NdNvcu!=@gumD;H}uR9VwlB3+f{go04haqWD3{A5!J98JeB zV1?6L`y$6Fq#rc4y@H8vE1Aq6Jd#(^{uxu@>)MvM8E1*Gmx5e5!7u;y#Ke?TOxtum z#wNe)@sw{+7FDA#ZYj})SRsCu5}>KH_EiuQ_@mv`Y}!|x^+uOj`ECTxuh)HYMx_}Z z=BXQe-?~RN22yEEB&!4!Smd-Sn1w_{z*z`aN(sxVDXG-ZTJt}PTrnaB3!c@)@y!1y z@c&*;*jFKQ+woly*9HJmoVqVN5@-_WjK>hh5iC`gy#|}K<|R&DY8?a>h9%~^K_kD% z7w|<27BS68uvqb=_gaJLB82>Ob2(8XG$%7Yfphij%(yf@;c1S;a#@1)ATR1}=GK@h zu_!cV)BSQan7VPCP(EmuY+kcUKP>PE@&)in`&feDK zEb9IMwh)wgA1-gDk`ax{BTjGSj4gCYeaD}O?sN}WGC-nZx98L16qWbN=boO#ceuVi z@)T(kY|h@)z6vQm`*p~A%aF;SMLi1AJnf*_C6uGy8W+-IxESne zqY*2vTT{DD?n|5(Y$mAajmKyIF>x=aYsvK^uD~vo`A$4-dZ)0?4sUVA>+D=t=~|xk zh}*fK;_F`!cLbp&e52gXWPtg+Mn9mb-mhtKHFsB@F5W!^nJ$3KXT0s`unxf>j>Kh* zMh)s5ca-W2iDjOco`U%6H5SxoE><=KOFHL?zZYU5cQSR(yJ=ZvP`J zV)rcw2L7cIEo{wt#vO$B#qeif{O9hlVvnT$KAf=%O3`RIxII+dK5|e zgRdgCR4AJ7P=w@cC#EL&s$Xi=0Fq;RaQ)z7BsR@~W24H-YCp$<`yi8; zU;ed!lP{a~ejCt839C_hkrTl4w7@ugPrSd=V#fv>tOvs3P(I=xGe{1-iQlKGeh5hH1>w^f$ zRDsfm9d586U;y4N8>rLTfuuH!UfwY(Hu`eTlPu$5!UWnXO=37DJI)sZByHXFqtrBm z*`)V*%l&eEkiF}^ElW4T#GO{KG*B=X0dO~{u>gE6C7_y!#*irQnBn$1Up~$Ff?_E- zfer24`BQ`M@(LIHjmZ{(a6eP;Zr~DGbO68!K>e?Eq`bz>-_ISn+er%j3>3npLEKwJ^-5(;ad;y3Ng7?2@dsEF2!c#p*jkQ$dC+32sXV#mw#m6pr z8{%Z*?7<2jmY~4F-j?I_nuJuC!qvf}T*`ZLmz| z&p0jZQO%Zx`=hqI4gSCjJ$NK5^b^$ThS}V8Y#$QIr*UPNkL2_;BPpiexKn6j>Zakk zEtQ~a-)t2Adu85z%Xd;ROsD_##5!)88$2Wn&icNOtINUC zy*n3^_tv;onr9^xXeK12&HOpchoYD}mS_o-8h~aD9Rg18-C6C@Q-Pw7O6=x&c+&qq zh>a*kwmV-jNbqxvgd@Y4`&Bh#?MD}2O&Uu{y9%u;7pqOHg&nDziM%f@+_39h2)jCt zQ_PZY3EzTkT8b%lA!C!w;`NX64)UDExQMCGaK0vhacgv$l_HoHGM?}!l+k89|EFwn zgu3$rdpJdUcolHg9|c%Mc|&+ELI**rzP|Ym53(I!Gt}OAg3xT2q_$(4mP3*l)v%(| zXfcwZKn$&0)SDRdww7eqJ`g=t6fmgu(XpAhm2r(@n8{oH&eB`8w-0C$FNs7EgWql~D@{~ECN~s_EaA~NV;wLw5%dKMjD%wo9OCI=}=(Vk?%G1r0>P!huctYi06u-np3R;iNbv zP9<@>STsQ?_{TIla78VT2crOjeBKT%`r_ZCDtdPR@QXn7g=u?E6#-2Zfu9bRk@0pT z(Gx%pp*c3w3G?iD-=8t9m^2=4=w(_qZX0fbW5E|;o5`eLTKS!EGbXqT&gF&|!oQH}#>AdPI7~jSIeo10L5b^pu9P5}v}Qbvy9%1K zMwj3cmcAZ_y-BLYmjqbLi}}+y9B|%Ysc?E_-Vr6HvMd4 zv&s!s{a*0I-`?b;cYmJP5fZLfw{d;~X$i)tYRG3=fdb1#2Ny4-kJnR#@hEh{Ue={u zJbgv@sFGvXwcg2#Tzr_b(XevoG6*y86#Cw1!z(V4fjyEK#C zEb4Trzp1u9d30$vt}=xP$#MpjcT*;;_m3V!S9n`vPE4K8Apck8-&FxTpJ$fxzR43& zDi8era`}M}-Pj-#L&;T5c1esTv$@V-IrMjbR)z0o#d^~GQE~WhOh&Hc$)dC*3+@N! z*><9g$12qHxey|`ma@e z2;m7&*upA5#p#BfXU|kXU)S~tSniOwF!3WemWJSZudKGNu+ATJ221k@R8?`%RK-TK z(%@mhgHBx?4SoyI^KiPZgB~=MN7Z*M=d*$p$bPeO$N^EqJckdkTNNd+wCGMdW=%xB zQ@PXhzS4j`VMoVl+SRx?3u?qOdO5s4<=+2C(^qgs!FFBG4BaqvNI7(O4c*-x(%p!3 zcMqK+DUE=X(%q8MDJ|Vde%#NyzO}BOaIO=3@3W7QZZsWekNAkoB7i|I)!V$?f9d@7 zC5ZIp?iKFo+bf3uLnb70T)%<)8)1RHd1(NC!R}?z+j0kyO${|A{-vI%xt9q1-1|PW zNK!%G!|=Ox(yhm~1`Dxk?MXUCXr&pBe%CisOK17_0Rk00j35jnAP9}#%!%va9^K}0 zZ+c2lP$YYuXM5fI6c@Pgk8Z>BW@xylHT9+O+85ogxL)U3G+~`fcjKmZ8t^aKLG&Zd zddzwtg>-)LSHC%pIao2uzXFPdU}r3`{(T=KW%G76cuKK4l|FA>`1sOBh>uLiBH_{< zf|lHtQL3F*Sff9hyaWxrRC$4xA@L})lfv7sXFVD7(xA5H!Dl$sm!-m#3)ro0Rlx5I zZG+%2oq7FZz8tw9uqYL5w@f{Dhx@*EJ$!U!X5pTFN<;j3Db((0U7261_YX~*NBj7% z&%2=?UNmXo{}6k>fQ&jLr!@D6?qV<(r1QdT=LGC)RBth^ttdP6JK70?J#N@%(}4N0 zB8pX~l#{G99E)-YC>(#Z=!Z?IYB{1T%`I!??gi5yxNE!@oL!EF{ z*H1ucnFuQ;>bFIRMX^q7Z!OFQt^8kO&Q_!M{CEx4ijWy5)nHOxffSX5nV$O8m*xK& z;b+&PEy*$_Ff)$`7iFw9z z2e7LR4X~0PXPMXF0^^$bm_9vSh5ML~gB*_%J=4jEuj0Imk8|~jh}!-hr*wsc zXslcnPG`8u zs#C)vRFHMu*+cfeznyQz9G${ ze@B%lk%-Z+&(&C)lg65qJ8vX>!^IWHzMGCP?h6E?BeI)u?eIf5s9qk z4R%(Z>R<|Pm;_p?6_{;Ul8@<>QZGsYIGO+b2(ObPzA^B%(EY8J(g0^=mF6V7#Mm5g zS4VgSt?|QQLo&v7D<^QDdFNyGvh>Wi4sD}*Rz83yR(UYCy@o$eFyRaEQf%kE;6?UY zcuQ8)N=$TS)_eM!Cr6?+6B$o*q1fn+KZgsyyqyIMQ&_bXatYGHN@92Isa|t2^UvVw z85Xl3RGWG+2^oOHHa{!Iu^mU+^zJ~VjgQt)-At)7=Ec^2x)cR4)){l=2b#Oedwb*Q zb};Dfl$!g{>wN+ev!bzo(4uS|_7tFVvstB4Y)Bgp_2YqHVg~s;j6#iPZXoPyP3^a3 zE%G#t4Y{W@3K&H8yKKO@46!k0IL!D8zxR5*EOO#eQabupjcMp>NS!!D{BPZ<%1`B> zp*(aqrMp3!LW~nJt&~UHk>#JUE}3^4EZvQVohjj;9oWjPHqu|y#I$&ZLPIdYVDTJ3 z@m^|!6xpgqN(CVvRYa_0+n6tquo$C_1M|<>g92bh$D@Q%l7TJ_xkiDHq&Nvw~B~`mF;{cOG+#s;pSkc z#%(c-j6nv%RdgW8Umef>YpYzXD?y7J?{G9l@jK)f;&Mm!?^G%{{hsIwfYFW%JaM&< zCgLrPJdq3|$seYzHbllZ`|2?54S)hwS_jP`q_aS3vydAm)oU<@kvh~e#uU``?y$u0 zzYpG#YV^DFk!1=U^s6jPy^x{J%&@E2lDAtt1;D|@ID&}M+#rM&a?)yH`ON^}uH|<3=z#+;=~zL3Ct`=IwR1zY(A7C)5W8uPs}B9T^Wk zo@irB5micq8J{mjyB`|1XD7QKQqHd#wk}Y-{XrtaG&^=Z+kKG?x=>~?XMHE&D5d^w z7pqrqA-2TXw-JF2O#TP6$Hd&%nz{u+COb^#h2Wh+Q2&4?FOPGOYQ_cyUjaA_OZ+ zF6M@er+jDU?oxyl%S&Wq45gzC^ne{$?r#Ylzod)U@;YG`^a;h&8z#<)SG%$)%?L9y z0e{U))|h|6B_g)!GTlJ7n)H4#x_C;zc#3#)p?G-=+R9IRqz$-5)uinT=_hqVc6s-u z)W_QbqFq>@D3A2)7>PiOZ82{6lZFlrowDlBnQXN?AZ-XfPL9Exg{ETMs15H0R%ye|0%!KI>r3Zn5yanl*IotxjUVqU z+nzA!gCCVgwuyPR3z$AEr_tMk$h8Rg*ovl|CO)})R>5_bmm7zeOvC{yE*vksOblWx z+h{=n*4v+wZZrCv9sFrS(O%Z~q|!w^_>Q}=?d`{UZ1_ir@+m*;sSA};dD6SjN1#ZX_-fLbE)ILq-zjParC6duTVW}d ziI{O?#s)WL$gZVU9f);6OiGx!al@O-PcTX@j~@M#NVbW!-1+c=Z4 z>wFM(9!V7tZB9SKp!($TUq$fcyKzgz!DFx8t*oIftM8X}Pvo!>(9S{>&@z(rM0UcJ zPCHhiiVM=_ifE$fR7aR>`+YNsCuOC>jQK3a_NCQ;NX=RH^(J9Qp@1&339wb)jqn zJB2DnoDR%l-J)|>g3iiy)R9+-OQLWEw53Y6)=-9cegPuqZNVHX&2_}{!SwUNu_V0q z>7Oo_QJ-_lL(qBo1c0+e0)%qGk@90|DG&b^a+*ScTf07e3*QLa{jH4h2Mj*M4TDTE z0;GGoUOzW|E1|;t&E%P|-Lf;ia7p=4jF$-0ayz%zrF2zZynBPn#oHDCC}Vik!KW&t z(cQi6UA+)+lB>0>$l{uRsU;y+VHY{slXEKCuczPb zy$q2o^kb#-Dp3j^Qb_(cZ?wrPuo9d5c9_Ae0pPtX7=4(y92HOlNrgRF>hA@mSePy@ z`!{&TpFIZdUrWxkr|(uLn{Xkk69KDPva_Ow1SVDuk*9;oYgr}$QYf@{5^LT~(rG#j z>C*;7zwvT?No^Euv|8)l6e%^Fsw#1Y*l0MAA!s0A5y6+E6ppxEl$R3350{7Ms&{_l z(c6j7&YRZs$AO_03il!C{abptKiudp`;vcgE|z6Ic!@lo)cp~-hp$Hg>2&{$+stFp z!9Jg${wYNPAAyAf0vg64*Z3gZhd3kZQjD_`!@)YOAtRuK86zDshg;Mz_vzBH?S2G7JHiR35tX)bqCFpyE8X?uQOe=Nb!B+`evB(J^}|MA!2e5ljf z`Hf=odDOd2jDn)kA$aERA&e;D=;q?4Qokz8wXZPihP@n}lM2*oPFp5bj_e7VlT>xz5tNm1+;9X>z1$FCDX+l(}PE4>#r0 zB(-mUn4FOTAldJQoR30RWIr(X^X6ZSQ7Dn^6aroOg~?t5|JP0=$D^cj>I31-|8xYN zSzs%TShduW&RWg)3RO8vR3pi3xh^pkc8Kjx|N3n{_1ObgM}i4*~$C#yhtA|`Drp5DY^zUWyau3;jc~$YS-NdX6aBg-<%`g<)UJ+9brDvZebmvogdO|9?n+h zlb~S5QC{dxR*_8N^xe?$-frw42~$kakZq)z8%@Pj zH{iF9T_{R0DmiWN1z=N+8UjOOUw@(`_D-6(<3^2OOurZ4Hu4#WUO_pNza#)=C;tf2 z0{+)?zKoZ|MqQ*=ot;%EB9#Rmk1{2N%G??9E8KfGeHkL3Zq0NH!tgBXb*IoT4<+R- zpU9^BsC2v`K*WCjdXP>Z%g?hJFrkw2|Fr-ipf4?X=*yy2t^&U>igh^$Oo{{MxAgU$ zznAj2nvRsi{hAPTZA-sl*xmbKI*5t3R5wY^%F!fHo7epqe&f@7wGhkpq0@YY(Zs}n zb1VwUG=?)}GIiK5{3|_a>t-xB^|uB$nF6L!ROMjYAa(mlcv>ymGy`=7aRx$cy?Dy1 zVJi7ls?z}}BtxZ??ck5@ObeY9y3EVZWS&kOVmhDR5luF`YCA-06QzqXuKX+7-(dhi$aSZ?wkR#x0uII> zlmfWEk)@Gq^>g^-**M@9Xs3v0J<^H@^kHVVL#ro12gqrQYslfOofxn^GeHU ztxC_5Nsmu>jAk?m-ed#>xR5iXV7vGA6;Fhq2d)foZMi4kHplH4yaB}s+glvl7cnrp z>^wW#oir)->HAq9DwM)Sq7|8}ui;L(t|iz|Tz5hdj)&WUq+9kUF3+-Nd8d?&8bTdF zf>s?B_08#ftr@}jt=o)v24+1w)za^k1#`*Q%6+78_3d88vSGL_aeW5l1_5vh30Pm= zJ^e3$RQiF9nP2|hO9l;Ly#j3S`4LX;FzzlbbSZn01u0tWE+g~!0KatHXT;!D*B8YQ z?v&)zWz6>1GAu^g$u(yB6Cnqlqw#se)H2CXq;RV&5Oa_iT*Npi07?Z_5|4*Aop-6X zlyoC$ro>|JC78)0rRsDS^#6%T&anv#kHT(FxbS45U*YO}c?dsAsdpxhe}@$piiU-f zg!=|@R4?h}Aeka;NVoQR!F9Xxth*JEt5AOHHD6ui9W>#dvj80+$PxO~Ol=IaQicuJ zT9?;>7Vs>MjIR77B+CKOxsGY%8x3_U3gi$HmvyN~MEdZJkYC?6(jVu2^a$S%Ou@@YhWs!wzkLCk^a`fj63m$Sj@{IJB6Ou7 zmvN61PVj!&m@>4xy0j4Uk{4!Z%D6{U(IVldiYBC*7BJ(7xm)LNL{&YYAo6!Vd-n&_ zo^z;DfPj@y{ElKF_NKwig!3|wLz&oTG|O82@=EG0Y@u{Yq-F*oPF6t56s_XLJDpgD z487PfEAPe@k&jo4iynB~+ODnzJ$rq0fOKMkxNhaJl)N@&bB3q-&@#Da(ZBObPeTD?IPJ zpD6@m3=*xo(ad?f<@D2PO%VTE?j}JxX3T+hey=S_Ex*1so zc$U^Bdbc+NQ2`+WC{u5iaR|4)S>*7+VlVxn?i2A>PQeN`{RHhnpWj5#)u_x2x}idJ;S%rc|LFc`N*W(Tn0nt4580S*N2B$#$+_gD;kW}!HOVO z#kJw^l9lKOFpB`p*kTTJxc!Sqp>61Y@zHZF!|*~1{=U}P@VcixutQGl`kHaVLT~&+ ztSoTcj?kg}$z;WcgWj3h46J5V(R??m`TpC4J@P?35XD)_))WzHRuX`Zba0EZ?3_;CIIZQ|8rEPRYHeHMStu3V-_=8ifXd=$IczIc z4ZP}GAMk>&f%?+TRo*$aFAz;Vu+Q}1xU+sa|cTl#8oZU$XNGPLpMIDh+8^BO3(HjVH zX!RD>lfwYAM_uH5i47Oa(nse+ z>=?#17)KeYQcMvDt^WzB%C8_?Y9-TU#A5WHR(_G&!4HBw1XB-plewF2J=7!(jrQstIh}y}-qEZ~mK!`hIHqAR9byy75{y ztNK7X^&fTr>2KCY?LEdVumBAds-j1ZGWM3gQ3P}uIwVK4}oNA`UkJ#}Q+?<5nPuL5sah){Q{+ zn~!deI=Fimgm+_EE}cTJ?Yu>wYXD6_wf~x~s13si4%dRx_FmR)x-Xr(Go-{bAiJ*2 zGeXI@s^O?^wEsrTya-3_1JYmvhK#D*W~s`-miE4#Y^^QGq$ehcI%J7^>9?n$_!QY+ z{uBIsQw-{Mc?Otb`;M6q?M76|xP0*Vn8T&DX1MJ)!DgDLbzkKFWxVLyI?^f77Rnic zE&J&BE&i9o6!YHvCynIJSw>uS?LR8 zul)L&lxupe$Nn?%a6xP=cN1ig>t)nVHo!A>WB5_N|3%+?xHC+>{E+1g5&2-JVkV!< zEMLBzKtuW$b4G-8FHTCNOylu#6N*#`YrgdW2aF2JXC^lH{zgr40>qqz3bEDeEBQxq zyG*2>X{1>;Z;;}yjUYwrlXEVn51&WOTi%eB{`Iau_EK!V4GlyWJRELO#zj z&}pnhBrB+^^`d2Sk*EGrXw@e3=)j*mxzloY5g@hY6Ih2J`xSFINc4ZY-t5{m+s>;s zhXr!AnF(I&^%jS|PMFOIE)QF{9rd_;uOLgF&YoPyt0kkj&R28qK{D={%HMg; zZgg-st6b^1TZ7fdgDd7ByXHDA0B)1dgb<9=A|C}SiTKFmZ6s4Beq0xeRL9|yoK`j= z+$p`&dfOs8bScp0aJZ`)mm?DOPYvCNo*vi984*vfoN^2Y1z&~D|z8EPLpuu z6StLXii^Xp?>{f0z)2uftp8Oeq+XwZ-{1M!RuoYJj(OP*`0=M8r%0UoPycO~5X{8S z*JXti%o+LDEni#rJ1+amT+kDV(=)yA*G}4>mz&dNX=@ZZGNVtuRHOVfxjw1UbD-#D z(F(%W?n`&MqmrVdEVVu};HRtSETH5p9nAOpT=3OJkoT zSY{kg1N|{#ipz{LsQ%H2J)1V*VWtlOUPIw#$Fu0t|1|^?X_$o>``p+m;ZYa?hz(w8 zkb>#!ehG)sRc^ABT*8O&jI;vLd4e(W`9{P@5#{=%t{?(PGg1cs7|n9zB!sX3?d7RC zNHFv;P6=0S{~*HIcq(j@4cOlvi6%;trMM|Ekt*LLyg zknm6lV1+>RCWV;70Gne{vkz4eeE;O1gI1ZpJj*|6ErY_Ei$5hoN}zjB?-`jWT!OGt zq%zERX}f%&ldE(mS>NdpA^TExDac&yh_~X$MLq(WBkew0 zOrlV3nI8iLNAir^->XbP5eNN@CK(0-zCB9#igBR*!CzXhVo4Zn3!rFU#14idZZIy3 zWvch1$4;jIBFJa`8=q4DUkDruI;V@W-_rh6z&7hIy4nYSZY4KI)rBY=;y(Uab^p3B z21U94yzdzk5K9dgMDr1*w;gBersPUc_ZXo}g#o9Z9sYaTiJtSia>B4Zi{T|pBo1E) zXA;h>>*A4stxSdktcOvd&X3;P&OuvGps|nnH=BKE+a)0?W|-taeKqbl7rU-$KFSfT z11Nkm*t_xT3A`UUsvhwNYt!(P_3ueZPetx<{LUlhovg=$BiajrUL_H{tUN5 zC501VomeA8mA(XTBCPg7~H@rQy@=K$Cp5 z45WJp6TMfBI8AN@W}d;Z@^@<3I7b1Q@RBQoi>&yQABfr5x<4Hn|H=8qZ)ATX5vkBS8)XmY0C}0tQ>`Rn8+84vwZILZZTw`1P}3*EPMot%Jnv|{(n?4NGg>; zv8s4Hf@-D}DX_h`*dCDo=yc5&-*TTquEj8O-$T`0geEFg<-x*MjNw)6=SrL;fIm?9 zoJ@25Tbk$0D^e%L#xl=?yDO!R@ANIU3Oae`-3!wa-Xw70G$HuKMsd`4s58v=i*BS<3EviHW$fk>(rqKh|Bk ze{#+fHbrNCa@J%emEX&qYtMxE~Dy_Z^e=q4||i>$yK3&LSBTxA*H zNjklm{Ao!*oDiT?kv@<|YwAM@^dYF>O-}U_*-3-`OASV=Sq}H(fLRU@I6LWo{icQ+ z3~uH8>bTTsbvb5~-UAs|9Bd!<$R#v2VPee;9g`*k^8ygcM~@v7$u-=QlpOQt-yadsKMcPKT#x&IV(3(4+2k*X9eGs_pgpW_ec+TnO24pzrE|iEgf9_ z(N-yIE&qwKFk~}$l*~X!DBprSFk|@VZj$+ag))i-0ui#0hLwPLA9f>_Eyu}PPFea_24hA`cNTu%TB=4PoqOHyJ2y<6oH9(^K?6(rg5N5me}e&S3Ra~Z2wU1x zz#OGRIHtMNm99q#zc6zX;z%MYDA?_f9qw!<_6N6qX#d7(Lay@q9J5Ko8*+Z!N38yI z_#UW$ps3u!e<=*greFg`rSeQ0#oo{bMj-P+0*cHfCk)DW? z-A1|-WBp^dnV23>T-pnPq**)lMS#tgv^;YVoeHhH8|^r3gl+BXSl@=13Px9`JCb{q zcsdDM8eo1VjUg@~Yf(X%oc<1bVJ0s%(p-x{pO_P3DDgFoIHnR=E@p1j)CBu7yCGPL zfyV{W&pFVh4_{(25{e`rsTXjHlHnj}{+@W2HFTsIFs6`22o!IA69AQv40Og-SxhTX z_m>dFeRag;_!LF*V2) z=zx!^0AD%dV*SY`Crg$3pP|`+6TZ6G_(wI&qW&fphh10f`vZHC|A35#YbWRl3~}w1 z_Nz+YhvDYmA(+O72`x(_vfZ~-(P!P_2T7et+nBkjK z+R>B?O(M4q@gKNM7hTVweoV~3L9bI&QruJ$@aDjNuZi%ZEno*fr!FZ6l${cahkJfc z4}ILCGIN6gv+!)Q8qSBMz&Up7FiL@_Mn@V%n0T)hX{!qrXT}GhX-n_|V`1vJ*o0Gi zX?b#Z|4~mLiczPc9P=(AL&cpbm@Es^0V2IzLO zL-rL;Eh^Wl;>U-`^{y~0-(%t8FKt-D<~_`t5;X@Y23TwNVci+2fi8mLk*Ok5EMear z31XIIc5=sQQQr@nUxrFkzA3khb_lAxG}X*3c&To?XdX^})pxN-Q@6>6-GW%?B)kT_ ziWZtDk<0uRXsD7+0gLLehPjW{8iTo!M?#BI8>wk=Io4+9gXdS4G33%jSvKA7Bs;6W zXlAZ7uPw_zbCN2WrbKcD)2GE#w?NqY1+`jz@1<%|I(!U+eQ>?TJ=3R1;7FK*B$tpq z$I9#UM1!k1eWG1Ms^{rO=^ruG20OJUX)%;-X(Z-8qffoVSSrThOk07Q6*z04hOH$z za>Bn*yY)5R#4ySy+XJManUXhx!_vC;4?v&YnE|f2zN+@nHAGE}oWlHQK={mWWiiF$vRcX0+G4i-M#LD2gD%y30 zgHnAyHD;2SXO-$@@yI(B9s3v#&}|K!c2PZ>q2MlhVT`R!DE6Y$nM=8?^isOSQo#v- z*qi_Se)Vv3yJgnYCc2^DTlZA3+ybrr%8Ti7MRK-C%MPayxsMbWPhm2ADM?r0S2|~5_1dQ58V5^uV%7*ovAF;Hi9`yq zlzOmZ7q8Lyl1t^BR{_dH96SXe0G#pHSkA;zpY4BO=j`{NT)4glj)OekG~&;gA0YwT zLJp)G|B!@#zC3n8$^9QYzA-+>KLu^h_6ejI)nDjn>g{?{@J=(d$^ybNmvw>*w?k01 z7MZk)*^_{0MRyz4^rHk60knNkBoXVFY!+FGc?*JO7`KfYN$q5zyDE>XKC={l8`TmZ(Zsk4BJQQX31iA8X&6rLx9I7mzU9Tb?Ef*6Q>vZD zPMmD&!?|e|BXz#>S?9%{$#0*bP5W+7W zed21~&*Te+lvMpy@8YilE>2m@KENSVhdkDBBUYJEE=Q#SREk&1Z`t(f)^Mp(gVFzf zn@=R&wn4(b+>{5iNW8bToctZmhI>$r1+UQ(wDs`k6N4FX)9Ak@usveAmYP$2&1^o5 z7EvpDjKK_{t_{Gjj;^KneZ8z?1hYhSmRUoo<~I-#kHFHHuK5m)2;TTq-6vNF*PK4j z5ru(9SGummO(^Wdx68nwe5%1P#_$EX(OUyWIE2~!Ibo5+{QKr1oI^eql>N!ArsMV> z9`Fr|x+|3AcoJ$B2E=Yi%(c)h{x#&OFx)4jpnJ99CkLXZbg+3Epz-CGK<)lA5s z3>JaqeVGi~Ie0T^e{?zZG?z}vI>`$jxL>O<8&TXOw6nh6cMTXwWZVj43g`8FLGM(DFsWO{yr|TthR!&M;_Bg z=5=Ni9NdZGuI{hLa;9^$=%JP9q`<|u%wAS7DJIqJ>{IRrD_U`$UbL*d0pTCcO zjb6hwz0UHwr0XYv>KkLZ3n@OUJbQ~_{hyTK2pwIT+Cdmv2kHIDw0f8!k}#?}|0X1g z+OMSvLp}p)e*4BrUm~vzYxC2jVpQebrR2)t(Wbgn@TP$-SXOBwhO80#V>*iFvU27_ zG-mn8v+eIyrJ)qKW~1SG@~5c@uc_-@f(Kis=p&vuPsHMp!|*lLcAh5XPg;`F#wMe^ zlT3D<^cZ1s6$#TRvSp}RMTw#uKiH~VmFxxj=i}O&(bRN(F20iE6Rg{dzDJT!$_S6T zHi;=knrt_Gvo zer(W`GvAkgzI)0#Ta0Wk>l?Hfe=GDf)W`o{3xLl}RXp$QytsRkw#9_{DkV;7j=yG7 zHNLxk>mcT=y(5HIVT<~+vt3bL+~OmMJc^`G$2x9FA)tY7BmPSBrcI;D8=pw>nqfK$ z7g6ql=X!Q1@#`dQ5oPqd`glOKSOSXajRv+@Pi2Z0FHmh+UTT22BEp9meVL)EX=mf{Ri zJc!C?&PQ3!`q_qhfc;0i!_2s?Bw+!-jegV;q|j0D3(2Pa^B)O;BB3rKSp^X>-BQ~( zq0OY+yU_apJLKt~8lAEMtG9)^L4REi&O#cde!8OBPN}w})ovMxejQ5cL6%X%JZxu* zkSW3^>sclji}0?gm&Kx!TUXi^oyV|mNj^#yMI+CDes1UQb2Jc;5jFLRFnIO&l?A4% z;xpFB4#U%G2~Urd7V2g%risWODBjvq2Me)!3_`iy_86pChZ!t>p)iypk`!EAO38b- zC6X8!9*(-1I5mdT-W87EjLfqr>DvG?KCTJ?2l5Tm+hsE5rY*loVG8$j0IM?kEd@-n z)rl6-wi`HD2{(5UT5avF1C;3v5&xo3AuaMW-&gfq(LkFR5NZav9I$|dlK1( zF~uWC=9Dwx>yhVAORu|{_~y>kJaV-%ZS3&1Sqr5|Eox#x!8*o@DuLyIh^D$!%8gvl646rXn7lg;DNSE+smCuJ4v3(KaYVH9|NBVR5u7a4~o@`R5)oKP}hfxU% zS1dnG+qWNDGKwMVZC8m@l=~2neB@p3iupA*vb8(90$57Jg`r;dAQd(B$FA>Bp4PB^ z&-BSHJfyc8(Kbs}ek&a0@Z`36YLcrUoW-%ak8R3dP#iF_6XiW@g^D`#poXlfkjH;c z*W`$l-5-LKDnH&vwSyg9h%M$ZO~G<4ydi{0BqCG2sFwRd_W-x2n#>23L1Qthk-;h6 zP844y6t&MA2-!ST7mJV|i&gy&@i5whq~}{p{K6N&-*Kc23s=}-h#3kcjeyXFMWjZL z0Uz6!JRAn2MHs9_qtKb8#kM}k?<|5N!{*|O6r!EXyd#*C>y>Cx_-1yJOJm!$veoQC zSK)7_5}n(-Rzp@aMyw!SBUZ5`CIu>a?$2^Uv-T}*@pky^sI~DTK{V)37J#r1EJ4S2 zeir|*l6C2x;r^%Aul*Q7ft@G*wuhsOP!Je(Ig@6c(p5;>;*og6=l5xb<)EesdNz;I>%T<5k7HPDU(XYt@h*<%h;3aZ!nuitkB8 z`uaC%p5nG94lY7kocS3^3c$gp^DBOQ!fB>KsH9&{cd!{TnCgv(xm9Y#hRell>aHP= zhoEqAb>r5#Xj7r_V^0}g6J5Pb_D&38k(Z#WEjdhl@HZgBmi-3;>mpD6opIU8p37&}W>kdUym%*) zb2HROZrra7@*nh}L}0JJYY41>K;-~{AkrAs-t4TyY2QcHfUD&19RK^&J*gyu)-`1eNRfe%H7xVMJ# zY0$(u09MeI`z!`2yOWgW%yn6kJC?=}$6-)w#qF z&Z4tgr=T3Wzo6G6i7Sh{#;Xp`!ck}Zm>0e-jhqSc9n8o(0&!A^lqVl}su5Ch<#5Gm zO@DP;QsLvWvs~U?R7A)t;j&NSe+OaA_L~Tx;&uCDXpg^nx;)sBtT2Lv)-Qtp>+l#nB+4BKI418 z*sm8aJ6C`cQGT?f&&10BjcyM8W?WM#j?Ia>JGGT&W9s+tmRm`C09 z<}#`?-x-ut?v!cVV@Qfv?X6NEbtNfEP5DChqPvR zk+?WuI))bQDm1Eos^s64(XbGfRd#q3k?<}9L*H{E5bwJg_tnfWgVp(J%< zI~po4rG9xn&3_j!?W;((@-YksdiiBt|A~H_5G84C<1HHUBqn-ugx2TU=10^lwo0LTu-Cho6I!Xm}2vMGTobG?!qUz;_& zn<@f@?4A>s#eBKi=NQx9?+_K@&2y%s94Xvi`V722Q*7=M>%B?P2~B_7gN=}zqP?V+ zh^1F}e=x=phFhn$k}P`_iiU~#Mcq3ITfXj>1fl-VIz|fKYIN?b3_6TH9-pW2eenaw z!mz$Us1T4Sv4brB$AT9nYDRX>Zz-rw+Wl7l8Uk*&+>{TWY-u`+ zg^?AdZhd+4hJY#6zXUqH?RufH>H(~%-@JM}&YDS$st~RG8*K+m61H1c70A1dW6o56 z^@$nSbt~#2P9h*-4x<7iMv)N-kb^Gnd`&Vf3Q2t}cD!&8dxCG5d)JSD0zK{J6;|W#ff z{=X^i;$}!kdkh{!Gsrm=MJ4Jq^6a4R@lHk4`)tjXUu7t2!*gs(g#o=*NTDLDEFMZ{ zV_;MThq3atHtnCB_%Xs{^2JJkq8(^_xKX$obn+dD zai~?xL;pTtK5PCZ1Y^5HTvkI@wx#QlS3Blx<2GIK)qdyl9_JZh=qjn?tTH-YX_z-< zflzWX5V4e8Z9uKlj|(s<+!d3ry}5PuJGA44#jw4L;?})IHfL}8Mr}ax!`Gckui$+H zIes1Nww`kH#ZtC|8y~@8nWg9YC_%U)=7S@rEA=CFo>>&92WK4*J<;U6Lt` z$g47B6jXonbl>Lx6e244@B``h-8((D@cnP`Z+?BDuP;dVCwxF`bW?gEGdAj^bm~%; zJMgH6LG&+M?jNVm>_`-Jm}j+&%I#$*sj_+3NdJ=!Jwbug%@AI%!x-Lwo_^J1N{evlZlsW;I}5kZ{$ z{Vf(;waa3tB>K0SMmZ_)3(X2XB=5aPT?`DlWuzdD76mgMLo-+Yv7(lC__ z&~m*R-L+zRz`G%7Ar+jJzC(a;DSfb>l}xC&eHt}Q#Vy!U#tuCm1pBFx(frAAIdpng zCAz1l-WJ8;Q=EQ{Mr>8YW20S@b&5iJ^+NspKWjzzfsD(I3tzuFp!3Z^ zd+_n}Vir>OUs_|9>bt3DiVZkCBY>Zd%5?#nE%MD>y6d`uE+is%0T*Y2_ur3V8w=yhuBPe^QAb(S z*(KEYlQtK8TR`2WJ_u55R+id3;?qK*WYKj-7*5fveji-<^Ci9jvk2uhL;F_iCuH7e z-TV@60>S$I_TTVZ=!D$Qe`Kq-h#)A!&I z1It=pDL?>wYm43QYzPsd_gqI`%-}!9Qv;|tF+Cl^dvBggasUjwKaG3%)^+7gB0?Cg z3hp|J8p7pIn?m5Eg~eKwC8#?Ct)FfkdV*fwucNWCp|E}6YED<&GP!<%2}Ku~|PV+V}Q*P+a%qiufSY2~1EXyPQ#_8X3P#=o_ESHGXl_$4jWuetE20{49@Ad7SQ@)B zt~;w-1*I~z5x~_2!5U}Y@&xajld()5<;N42ISyBkJFU`C`j}vaYBTg4-Jbj;wDTRO zPoHKStc_?QIUPIw=E%JU>OOAIpP;*JWU3+93z)#pccgKr^Z^-x-MYYBRUl_L;OAlioAC z5n)LR-#?+VsZkXg%^4u)_fK-?MP zt8@cg_>S!EttHt{I_X>J1Q|~H7zL&hd1o|)2$QHF^IzBO`^{p}lry`QsKTl+M=Jn@ ze;SV#25;xbY5{*)(h`cbGPX@c!bvUMELA7A4cIqEp2z`)eFr9Ew7W3fIuUp}5LOw` z0@MIkdo*%ZwMx*M6CokM)E4A=)fhzRz2<-x;-F7m%U4oMdS%bwyD0)*tra;jYREsp zIvo^WiIFAX0uzzU6J#-^%-Hq}XyHop`~ZN0;(0FWs73p9jVt_hitEn=OQJSEwnm9q zGiBRvf&AHv1&2iG*9ssJYyj*!1sb4^3{~dTr!@gtL5HfPL2uy+5a8MqL9G4p;+{|UeYAM|zLoxd49khozK0(->o;iv zD7jRv?xcwv?Hb}%AtG4RBd9>3f`qlv@&vq5GK?h-ju@|b?#1&Dmi@cQR>^C>`}qBj@?1FS zr1#JXGMw}w>bAP5n4lrsK#OO$O!}rkr_n+I-4vDST=|80+s3G`k=SoVX5E|d<_Z1B zd)xh+y~l%c%R%5{CX}KYF*IRjN+7dmC?ib-2Nre(ANCo@JCHWUG9|6;%%Eg}_zBEe z=bC{kR+5wm;3U-^2&kx{#D?U7t_-sXc)c6OjoBk(k*pR0G!F?9MLOmaV5TTK{1WE5VnA>3OTiyzU zXU@&-)yHC&GCc8x0fh0plTJG64RoM@-#YUie=%Tyu6HiqJ8#{GY=>$Juxq`q^yxO~ zX5_3QWPh>ydALO1*)CiG=-u{#maH)~rFisYh~5nD#n6EY=D83mNBGQQ_Pt`?1zgxSc3^=^M%{pYO~U722$u`v%kzd`e*r#!7XH6#4u*YC z*f&PmE7+1lmLdtV?eVvz8wSp8NRf?>zb!v@`*(9Bx3=HJ^*zTk|8OGoIY*99&`CPw zfqxpEAj3%?qW~lrV{j{N3R|k~!mdY+_`yKBb?c_ZevRGKJRUggTg^i7?ZfIUxv$Zk zaomX@xZ7B>egI%XvEX}3n5GO$SuF_!u9E$D-?ZFRX7_5yy9gxv43r5-MFbhSE(vN6 zs1>6i0jMlsMgpM{%U>0A+!|CEH7gjB#F9mp$l<_4MF*nywoe>#c#%Fjvu99Of7Apq zdFBVqm;mldo32QdKv`XFHO$18>zvi%^8$SN0@P<<3qXUMD?zcg7T-&~%$^3N+$>rK z8UT{G-HxeskgCsHRSyM#B~ga{p2TT`CGP_aR0!+WzzSjoCFEElJI{Y@QSA4*Vu;rX zYEU^0z(6<1Fi))ilM9dc(YUwH>PaX4o$0`%;O-A+{QZsBV%T@OJB4$B_#Jyn@ojVb33OE5vafy5?I?18Cr$35b;;0>EG)5M)ZVU8f*J=xdu3Y8`=f z(?A3LdsW$rn=WNZe%ba!?RW6L1XTx}p`L0CLg6Zw`CWKco+Sk0@ex=afKQ(iK0Pp& zxoTJhW11vaQ%Vh#wjExMSrxF-k~!*lCFvt~AcOP62r6J$KjY&L0GER>u6Rf5Ir$Q6 zG7kex3NX~J+EcYqh&@SOsvX#O!oEp3#I^zZN~rsU{X%&DQt|Z*sr+7fh5u6E+9iK6{X63eZ@A6=yea52rz`7(W}7R4eY?@`KoMdIO$J8bf+u2 zkEuK_Mx2xB|C8th8BY2>6o7_~KLgFp{awxR8-kQ@+^)-fcMay3k)AYA6h#0Fz=E?VdT70cNh$}HkrawRi#mwn z8|>T~YS~UKmTj@|;_UR|?;Xf)d}oC(fCeP6!#!heSi{3An;Ph(-u`(W`%_EBG!y1C zp-wl{Nkb>sMRPelJhO96aJrJC`RG>n2#9rXWIqwM$D*=PA_T#q;L5Z_gby^0!a^Q& zSk(*W-m?reMAE(45RV8y^;Ls79sMq2MGK;78(}YxFTTu2V1AZmV+Tzx#*yRepiebQKK0o${7d1 zP%U~bd*`D}rAF>WI6vi8?{j02BOkZoZQ#QJ63!yNyhG)?Zp;q6$8icO3TkCcRB)<- zmowqZYsL92czG!}p9It7(FyuDRlRQKS#bGYCt#Y8fSO*yx>R>kSSe*EeM~@&-=qLU zVzeHmmr%d7sj#zw}r7Rf@J}2*M|3Rf^T0L zZ{M_%#Q%P6`1U4vzf@dr#Xt$hvT91|vNG0{`#r-r10HFijs=#q@iuB<1J=*H9S6 z?SE1)0|=-a7V*f?CF}sAT}YruJivr)IEZpCYahn~Knit^1M4>eD3PWS648H;1o3|6YY`h82ip2;Tx1>&8Z`yi zqjM@k2MPpK2_i+KgrK#${T#xsSPPh1FO?aKL5QY zf=878TL%(+)YI5RIm;gb8M+Jug?3;gDK<_HmTLzzRl>NY9Aq&4Mk{NHrb;Y~brCEp z3@p%ShHq~xzI_9}eFeUL75wl2GQPb5*QH`vCft?@%i7Rd!LqKnEloj-JLRi0h)h#MjA`*aq7tsFhQ;P{EJ7Nz#B$N81H)4$3f56;8-FS5Xr$ju z5tO-rh0qwvqhLm^L{x#_kNvng3}NPaH=;$q>10`nQF=p=)mg!NnX*uhcK6P{6s^v2 znssgn7Fad}N;v^Ify#nfRxDG)y0q|$wAL$I!1_n13cm>8eBT}l=#?+LDfftn2yPXT ze!l~}@cY;Q>r;d%#*H8vmS9tz3anJ-2p+hnsFaOY&HE!B`5?Bf{#KChS67GpI`VVe zr)@_WwgjxZXDCf=p}qR`=_W}t{*iWPS6#23n$WX-rhR6fNGwO++cF8__n&uX_v}D~ zz0rvNpdTG9mN2q^z8XBT9(?`qbvzw19}nG__5H}K{{Rgf=cO&^kPI<5h`mf=M3h-y ztPEz9hu$qv>9e5$Uh&6))&L1MrLwbJqZn`l_Klzz#xpR05!Q`bwH)O<0q66Ic?Q1x zM)>Ww2`{e&=Myl`g!x1$wV@V5t)yHGGoeoX9uZVbXbMDVNyB(i+IEoJKAr+R4)@PU z%6<-rj{&EuM?Q`wf55D=-Xa>-#!)GP8^D$xv8)OtEbAJeg0ONgyuS(FF2Mi!O8EMf z@b!1b+c%9;Ty6#HqGc*?H^I^btu8%VK@QFLm_#eN8?T1&BPri9cUep<}%cb-TGkV*kcEm#}dz17f(iAarGm@*c(AX&A^ z_|GBz!5r=2Ler>?C|Q+;B8Dt^X|keJ3N}O6t6^x!s9>`2%OXIP6(^YhOdv3p8*sZZ zrW;Tb^R!2b#6-5Jf++d8xt}?TA%h;S6nD_X$2Z(XwvPf9%p7xD0RWwomg64)03ZNK zL_t*JWD2%GjdV0t5Qtn+<4&06(91Nf)qrKNUcaltRFA)PmCm#k}4Tc3ia1TOF;)uedr=_C{a0lrrr20Juc`Tx17z?L#n3gGkyvlI|M|LEOa@Og@RG|>&cMqT!TF_Ong!FT zYU#;pQ4E!|qQtD#lt3Koy;p~ts#So1Bm&G4V8~@Yw{z#A@)mM_oC7(!ut&H$Uxh>p zgsTyn>Zji?YX=w_BPvEL3vgS3r6~`?x@Z-x%e&y)H{k0V@IPNGe*Z@J`jzqi1}rxM z-wN6dSgwraCRkd-+E%PQ3m(qoKcw9rkC4!`An?;YhxKrV61b_L2 zf54)Z7xy1UT<_)K_2V8KI2mZW@2!Pr{ib8guAmfvN<&>$0V=g=dW0!AzR<62#Wbn% zQjC$CD^LZIf>EkQG?00~8ze$4^;JQIwgQ^MVWgDVTeMAi2zAD8TX=A^G^Z0xdPzwZ z*?EcFF!xZ~LeMgeRgXudmvVH&HA-lbpcc(>U7LCQi$EsegaY8C9->*W%#3Mf)QPd& zSV^U#e_co+JB5&<*Y|>igT+kM12{`?bk&_v8iu;6Ep>89Le? zC8|UCex-C7LNLQQ4l?pGjNA$;w|rf7-~q$?eu2I6Le^l?SY?CAN()ECSKeIG_CQoZ z5DWF^CIQ&$?h05GkO3&Ve%D!ng_l?0;B+pSPry7gh}dL&QE-6-wHBOCq_VsU zfEJ)lA&W>XK%Hth0fKwMbTbErOQCQw=r!eg>Ux+v5c(vD`4T3xM}FvcDql4H!K3DU z=wh6_a)d<=Hghm|1+8@@O^I-z!9a#(1+G`Zw{L>CZ@{-V;O%$D`#0d-%2XfnScDKJ=@>BJwb+N z`?;1DX%RKI)>Yq6vzcQVInSWO4kO4|vA-2F8arApCguN-wTcksc>bU>e0ctkS_c~J zJ#|k=Z92Z!Q^9ntD5>%TM6lijr!&ybmTFOq6iT|%Hksi;?p-jl$VP-#m8Dx)z{HSM z4Q*yZsRgx`q%@BF3L%&!MJb3@wGdWAx_M-S8$XDys|O?oNy2WAd4=pF$YZMgr$Q)I zBRPZtDeCp7iEx7Uy;coJt#iXPHQZ*#a%;HW7`H2wg_^m0wL%FyxPgh#ii!zb1CsX9 z9{V{!4zdwYQjRZMID#sTIsjrGqap@EkO42INNL1~K5*Fs$4rJWNH3r1B`-y*y!i9L z4(geQuJd;I_&RXd$=MEbmUrz(p0k6s;)nKs(CDwE4}IKI&pzrpE!|bH|0!+DE&PbA z_q*G7P0cMR>)++qw8ie&2g>}mD}%w zFRz%7XFrhxv^|{y20S~+5G6Ayfw_RCv{5pSQy-Q z1q+I(aa^je#cUA}+zr_P7!1YPwPL9fO3-*)3T|t`Z3S*C;q3x^y)?XC7QDX~T;3{f-x$|7 z;QFpkuXXjx6f(%84R&yM9n|r1E)PocLHfR%oev;|{g0-{{+&RDZF4>8iDCb9+Y@AX zwjXaUkj!|BwKI&GzNs6*xdw9p4TlSmTVV-YKo?McPB9LKU$ux0b)V%(SoD^4jD7FB zp>@1(zn*@yJKJu55)VV{hAtI=7NFP@t>lM<+eI*a6P#ZJuU}M&IlmU1Ukc_4m?ptI z6WUxt;b11zi9oc1n<;5kjQp_ym>>o!l&T$SEGUhQ!P*rO;~TIc4dd9W8gP$hVGqxz z&Nl5_4Uqyv6oR>rvnfgmYyb!(1%iUsA`=w=Q0s)LE|Ho+SfSM!Opz!6RgGvUg)q$p z%Pgo9p;pHAqI?b9x`7hu0zfFQWO=X+ZQaq$IE4~^L6zLOe&l*vr zJQN0`7zmJ}Ka?=i!G@GZApqF>Wa9;lWnl@HYhm+@Q>b(q=VVtzDJEp-Uj|6IQL%qc zA3S86d;O#But?3L1;doads{qC@A$AcKZc8ZI1G=EJ4E(ST%vsk9IM3!N`*Q0_tF%P z=7m24f9QFEVYoZ^+C9a|b{_6bkAAubNF@21TRKjThUju=U+LlS7`|R4Ai1CasZj<)g)c z+t{D9GQ!FVEG$rtA4|Iffcd163X@fPm`_?#`SsOGD^CDX0}6*cDpey16*OR!%BWMp zR3)Mz1ez}A`c*B(%2g`3QLKcg12L{Qc-EYj)%2O5-+EckQfiVFK3DdHkIbx5h|R~! zq8Ql5Wf29nO^!F#s6uW9+!PR4R!woZErMlPv91K>g0%s+mCFaycMqkVyb1v8C+>a23IYdbw)d>MW4Nlbs6oHr~!aP^3H^%k7;q+qOl9z(hnQ)o~ z(`;l@C&D~yB?Ar5FOaJI3sYrqQSZ#MHr?JrI>o>NBK1_%syA4bo)XGp_ijiJA7vG? zjjaSDQfR4Y*-r86fh&EVYze^1#F|9K6e$6rL7a>rv7?Js+Blsm=4sL~ni%ukuq+BT z+-^N@G*`Cp`CqGgp_oLy{F0QG0-&*h5kOJ2CMj~R=;#CGN*))09IPP&7Hk*i(&%80 zoh!fwFLenp5x6lZj&C54M(TIx3T#AHN77|2QpCIscpRS z(qJ6g(iWt_2^>h??VQ%7U~G~d+<6+<rPGl!1tV8{uYEBG$EHSqaM`V6FgW zbwYsgeyRBS2E4ry-rot=YsK5Qg12uA-rs=hwcvIY+^!AFGEOA{hB4RySP66$B%TVF z$&V1YpIkV8VtN11wSSL_{cL~n_5>N8?WbA~TTD59NvP{K{WUzi*|P>G1C;O<0O;Od z=z=fi>19ph??Q05!l%z$i1-(_Lxbv995=h2`pL|wOXA3ceF$_nE`Y3Ppc+;Ma8#d$s4>*Mq1&(wAXRe-VIrFow(VnZBzXTU zttt#)Glm^(Agg>7YcgyNOyGh73}hbve%=5(DDyWYIg~)oP9oi}4FUrZq!ZxU;wh{V zVO}M|*W-1CMlBeJ4@9r|bOK&pfS1>T*Vl?^7MxCOz`m82w1@>SNf{)}m3sB33QTiB ztsa#C1>%FN!T|**8CI~N6a~2{21VtCkGY`B4o`x!>0o1FbIb0TM!5;tq9g*8U{P=- zA{p$sY>^!Xy0j)}1~4p5$GLIqoC^zCX-dYr3f9)pSV4%d-wOWstKjWz#rwOUturof zg10xu>fnOQe4%)LlKxPAcaffpN`pA!i@W zFzX9f$^q=I=>C4diMOla(Gn-|i#&>*-TX8h>0m#3@QHT6ZH1bc!UVLI4em59KF$qT zU><_CVXcI^Pzt#OuJ3}&nQ=Z*VDS#tcu5Ym-d87N<jEP2v#r6ogOBL0d&cHWWD6@tAm!?tJob-9Nv_M$-PP`|!p#k?F&O4SnC9U*bE; z+x$Qi0~(L}B=?4}_k%X$=l?${r;(S=zd`f6{#`ibhb1fSlPXI(3r5<)%h*TeYwA44h7c(`mx#1f0$Y@P4v# zlbsyf!4Zk;B#42*R@!o^m}>21kxj}`zOEQA10N!^QOiU|N`$TFXkGI8UF)SrwIiC5 zs%I&ssQls<96bUF!pn8(shnhb0np?M(z5J z_K`*YV+*d{)6KJV4rKdsW3l*3&XafO9QmVHf&(@{Br=?u!|bdo;V~HLVM=vJKK49`u!A+lNNL(Y}O@ku4UIU(JYX+noZ`aU-v-YQWBBDArO?3Sqecmv`k* zIGrY(&oj#0P^KGBFTnYQ@$v$^egVGxrrwzI*>gzAa!Li&SfN`jXsZ%YRSWL~F)9O~ z7K-VSq1B@zk`fR~;V39Y5G5|vlivR{eIQ+XQ^ksHP-1K(0T134S45Ly>IOF;21pqB z0x8>ft?JP_oi#+33P3jtk!_SBIxMnb#qeCjA3X1NHDXHTe-Kwfy+8g8ILW0)w-ABF z02$G!kX};M$tP`RxBNx@Rl-}aLARlZoZ(LMz!b`@#HdXlB5~6he97UMa%WkBC%MDm>YE%;` zw}PfQ2+Fi#I#tZ)g6UK+&9I0CUw9Ga1+y3;(P*?X^xI{Ib;#E00{v4xh`}bKgcOh` zj}mpAEJr^A%9}H+FIEycZm~e79#z4j(GVWxq2!L57U5vEGzDGO1qv`UOCxCn=2s9$9La~!l{-hud5HmR`NBT`p6fDrrr zaoV5x+5Tqj2{Js}PdCjmFCqMT*j|4HR1mEIFabS8)AbhPSIBWx<5^TWG9aNN`Pw|l zPYwSry#LsCxKbatw9tP3==fCUdr(F&6LGl@tVM zbPv1{L?BCS645gDg{EI{0$X$g7N}8e3etVFKEH01cYh^z%?}_2U%~@Lz$#?@KnK8h zyq4Fam7waqrk)!u=wtf**#z5% ztG_(zNw0Kszo-3>>7dAIBtO<38Qzb2w2zCrqvZO3F_=&F?^m>gWo;u5B1paepQ|n8 zb~uF+kl>bT`YCVTZAa}@E^A30S>5zLoh85o+XrUIlgq3TXbU;NK?sNtqg&xoNRM6>AF}R4+rS|-ukOc z37;DZUQ3j|TywfeQ}DpYfg6EMxA1XVR|N``uR=>w-WK5U-mom9oD3`B>(~C9Izu$A z;BP{2JrK%-}c$wwXHz;LzdN_QFyoe z`YJR64+OFZ&;Dhvnst0+e~jbN^JD!xRP<;2kGCht@N7TTJmo<{&ANi3^_aVo;Efy5 zOo??RBl07&m9Vw#sE5u6!*oiUVDrMNu+Aaff51gs%iI#${SQv|y?vo%*U(up_Q>ZF8vpITw8O9O~eD{wo(>T?6PcLfSpAU>+J} z-U-QP9ljj4ou;h317N~xsS{zFQ!@4eNK*I;Da&@L0L67Z& z?iE0w$I^TJfl=#Y<^V%PEaiQ+_a!}>7;pIfT+_#fJOE4a_>PBpqg?)IM8HpRzC5vl zKV2IQ=l(2bqQouGJa{^X@3T=qS~S~82CqnotpLdCKMstd4uNv_X}hf@!V_x*M3%1z3swL^we54$|cDcfrw1B8xA) zVkM?c*^vYUBc;jZ<<%oT50Gx$d+#&m|`asBIJ{09j6sU$`n_EcZr4IMlW#z%h8)rb$HK@|$dkd`J7y zq~5JP^hn<1A46|&R%siPv|8WmzbhsUHty8JI)h-!@;pmX(xvGJOjR&X!0B9Zd2cws zAyOzVFU5!>YtJv8#AJ#&`5?MKGm)%zf1NwnZ-|XatGXz15Ad*?UH#o zS8V-vVJLrtfii{lMlczvq=hdyBYP$#Ev6JRp$~S+_tO-wsJbCa2!-up5VIHoOyqIj zppmA}n9`h#C&3tEp0(P`Lk2VkNHjQaK%&~#jyY!>#)n~l4AGc#O@~QIueA@s5 z_J4Tt?RS$_eIvCilLz151KX+(eQx__f9BHhkoJeqxg>Z7fxETN#%t)?Y+ijc%D{0t zPI{ICd`QQR=fFXO&$is{Jk|riu#uawPp*leJrh{9oK~F!;0>N^crKmBHI#dCsT-aCf2-(7pNg z#dgahfOr7xc1QeVOV4)5(e&ePUC$ygKdy(9PAiNJr%NAW4vDX8TltzlCM8u4hWOe3 zw(SWrJljt-^$cq%zM8&g5fBSDYgA%R9%Mg&@v|FFyO2^_R7+@X9>%8z^14b2ojb^_ z+pYfn%kNt7ei&m&3$pBuki&gwE48?Li@h&N&^~f_x^e4zX09ZlV^}b8L0bqb36=uf z-Yce4!TZ~S)7jG|7%ykSd=jlJF*lsgjPvV+c_vU{%rmguDyF%iOoCE@d9GGPpNV9YZvMN4I#P8HK!;G7CCFBQ{V zQ71LbyLZ5e6UjIzpt-y$+J^`c!C*tI62D;x|UlY?hLT&+q^t3~`RTi`+xLpO;%ZlrTu`F7uxtH73-71YyAYPiXl!AFKN_spg*<$Y>BQO9VB}``H zNgzQzdhj;attB2|M3anG7`15Bf<}*tl0g$`6bUJxg7_p8jehiF<@<2d`uZySeLw&e z2p6=qSo8s~t^{rc%c`Km<-%C(_jOUw;c{hsdk5a$7}twclUN1czIM_~=Tu-;-l%5B zZ49hUz^x~%Mb)Hj|2Jc*&W-$RQCv%0<9^sX5azP>0q4z-<~ywUh1=yaE906rmV!Dl>a6#EJ~hcq_PyaLUz+^<1 z8D7TGjvo~o@-t%ZZQaQ0ldm=Fq6Y8vy5e?4lwizleC@3O03ZNKL_t*Gds;5s-UML0 zITe^sjAqNz)6#F?u6r|Afgau2hkRs79UtF2hKrbX3 zJw}8ngIBS`*r%fw>u8dfJrYnLXpkmo>l7fv5&*xTwSxcqKgP4LuthQuF4q;`-hst`e5bR z=c)G3bt={;6>dXhxt?q|(*EENPQEc}4i5OB2}Z^5W2HYUX8zjq$2HRX|%Vd>$x5s@HCmmlWD_?byiMS%ulI72wep|0Xp{_Mxx23r`5* z7qx5y;QqKC&F~H|_?j>Kx#{yW8S&wr4c>&j)8@NPr?ydYA4L|>-ECc1$<9zoL5KSe z_~+rd-bRq`t;^zfCS6W7LR%)Q2myTicEk0$T1iUA|Nei1brIa0_;Cd;SHZV; z)gQQ7I>kb0P6W9iG8I{RTA^wNog9=xMsL^7u2c9Uq4Q7r{3$fdTi2&ZER7QYLa~ z(Fra_#tzzm6}C@z-l>wh)tR1h=kMR1Aj7l$REvd^#q#|%EM6B)1Y5l~4V4bWp<4#A zf&aH%A!&FG2Ig2WyHPRC6Mz*Ws(6Hvmrh0bkhcGwHF@m8Lxt?uFPRx&aF4Zs;UWEG zycDj?#gIz6W{u5+hqJZD>g_*h9h5MUP3p;~0+a@<1keq*RpnfmW+Oek0`Kp@+c#&` zo-j=n^J&6-T5-KJyu1Rhznw780+a7-3agy=<4I zT(~i9SEQaO7xbn;7gBPsrlGsb1$g@^3NW1e-+%pg0nQ5C`0bw(f|Y{94$cMP70~dvyOGIAfZu3K6fH|g7;_gbPoceJl5dx0YZ7nwiI2@DTd>+*ch?`qX)b9c}EoVK1W_)TLLf*Sg)KGs?m6r%Du*ze~N3 z@Ar`kgORlZ)&Hn_ou8ZDSFvv>Z?iR?LbfSak3YWMxan?G1>1G4)%3IiqN)II)5;NT zZCINUJYFsuna3%5!5t!Uv~`ZXMn5Rn@GCj_Ml>dPEp z>+ZVhHWeDxK}=yJdNx~!ieg;-G2j|Y&-P=1Wk;6+}mgSJGLjt@NB=N2{!rt6@4u> z8Q04OT8(Iz2p^jFzFmIp-{%_SR5V{+j1{sqNn!5Y@IE}>N8O;!ezf^f9w_9Wxx`&_ z^q_wxi^U`_g%!k&K$-e!6i_G-Gi;Ndfn`y^Uu-YBFE_kcg^E&Dfm#H(T^aMKVV(rQQ}dqFd|Xwn136 z0&MS>g*C^NO?$M)4QSj2GgD{1RwaQf`GRq~F1TF`WOxA}nx=9(LkT!*K`9ez5ipTm zgBg;p0BvTNw{bd~apGadp|ln-xiSy)3L215#0VDcdo!p#dLn2nB%hH2TnS|>jg16D zry0anKP`*^Nh+y<5bBk({S9mi1>0JIgKmC~uE=?$$4L2*jlHfhK6+!;uR z=Zm%AK=J>haE3s!FfQN2NhLm!rB{N?XfMUQ&2BLF=c}*d`mL_P1q7`g!A!OWH(V|Y zF7IjpyOA6Cs2#o-!IU_s4bt5e0LAtr0+)hT+88o!GD{;Clni;Spwl0~k^u&LWmw5c zTZ^2Yh=owZswx<7gr<&IG^hwXpv_j-l(M$*BnKd7Wu3*w0lu~?7lf9fG-Ag_Q3c|5 zTk!VAcz+js`vxqxf@LXMMZ&;^n^%We2CsE+tRau$25$Oxq{H^#NidYC&Kxfp*U(4~?=SV857rmt@!1K#J7uUvBb{5r5Tx{M z|BUto8J_JIGzuj?8?I!+IFQkzf)ha3xx@Y5ejMi{3edtm&(mZAi!8UCdrmyJJoAO}!++5nja&%@03|>!4x|xqBsc@a=g84+Ka}U2GlmHFZ#%2U-iLNqr zL)ecgP^D#E3YswzI!6IfNzM;b@T7rPTeUxAmG z6|cW(g^JTlkA#>e^>9oRqm~8_aV##Z0aY%{Jd8n&A!k#HIIb(AIneK~kKIwpOlGen zq0t`8%Atg=4QQ=^nXHa_HR3CcLI9L_@epKa8V;%+Acf zse<`LD7BaI)>ks1TMc0rO_3VN0wt;Brv1}7);Wq$U^~$u2kt!t3OHquAkC2! zbvgoq>3xAYNm4)Ww0@7aKja0W|LB#lVy*>HaGy+eTx)XPj^;d$=|`^6-|KJ((z1T* zWy9lge^z8Z+h3|ZL564hMNQq54m1claa^&5(!Jdb3QuS!IwHd=whKJjnkivW_(By5 zPgp12_$&ol_Ro|LVnK>qwvqIEdZ!NoNx(HL2dU$3{2AN(fGPXstvVf=@hG%*VMi( zGojQKD8SpBrc#{0x={?A&%nzU!pjTc{4%3WN;XLbRFnuaF96h&mHgaE2^Wa4hIHD6 zvGn`wbFY+=LI~9}uF)<9%+<)NILc;uSxirZU;)}fXiLSq6s!x4Aj7&0AcHDc>P6G3 zTd=Gx3E|F`-LJ~-JUe-0*KP#_=Bkk>P3yVOvjPn-ufTF^`0}OT^inZZGOcqQuU)9_ zZi>cDV|D>phdgp>zanl!9h@Aiw5MyU|Z8#Yq zBq$ptCv_7!JeD>HQx0=auDqLLD0kyR(GlikwJkr68aE(?FEDK{@ z1aI#gM33)pz_+i2w|Bw&yMgI!!1~f78G>g)JeqX$j+0x`=n-oMap<~VfcXXCq&mG= z1fR9vep5ioMTeG6_v#lE;F4CQ;LVzC;6kC8J0G`kBL|5D3K9hvl^s%BU2>S$4^Ha3 z?yzb8Zo85vLPp}i+PAs|#8N_wRcWK~2>C%PRIF-M)^&~4*+CifRMf2uj})Az!1>3v zC&=(@zo7Y|)PV+RYEYEz3TGkwR0ISzBA%Y|P|PS2V~>-~{=Te|0R{v@oic(9xU?-G zNEN_53=EHO5H#!KPLVkPYSqZyLJA3$*!=&g5qqSM#iHDgr=UVX6N&IjR~FzNRh6=u zsxTK!RS_O;4QsP8DJPUN0Z>o#^?Jqa#<<Y8sMce{`Idfh&#{9jy%sZ zYOO}(*cFtwZ|`VaOlc6XDW7D@)?^@zj*}`q>(Z1nK_>OwELz>6Ssrg|>Zw^4hOjYV z-vn2ysoq)&XWgWrc#00UYJgAo*oOg(hyH>QAYuqsn*%mJ=tQD={A^s0mPqI~{-|5oSTPMkB2*yfi#vgq9n&5lGLX#YtAiH` z3XLFv;zwdrjKllnz~xqf>-v|60C%P5p7(bPY>w(wfBpk(utD%X79w;<4FzBtQ3L+o z!PKqM_sKSZ27}iPan9&8Y*i2ktYCe$4bo{JMpgZd+dd^`Zx4Lu5jS2!ig}ia-=f+Q zk$D-(_U%f3qyW;!C@XdhzmIy_VQ%N#YZ#E4rsD8y>lVN0{(olh>={sFY>##j$#DfL z2Q%B+`)kHANXrzr)uR;!%T4h9W|a#J>=o|M0mVX=pCkLDIAyHW#$~HP36)To08$>E zb-C$vd6Az2Oa(YiYUnCofS?pXdnuS_!8!w{Sx{$So(S_ySZ{>W$*W{l%(Lo4#Q;<` zeM*`F34Lh#{1M`iwZe=W-+DBkO#?6Utjp>No`S~k2)+gD0(|}5Qy^3(e*M;ggWC;w zekLJ+A-tdq1`=6B1FR^MGyUbAR>dx-seEp{}<-;_&UD zR}SC;OzxsT{}JKmbI}2%bWG~0N5iq=`HOexn#smFk;W}_nhi*kBQu}vpWU7y!?XS3 zW>EcB;SeRiQz74ah@dn7l0`XCWGAQ;EP%LhL_u7Jtw8o; z8fh=F0683#(E{fP!t|v6@FQ%^k1A}~Kg#3Q+Way1wZ1{5z2~EqCsnQL-9lFq#_{c= zFUN1gs{+(J6$tM?o5GOwxjfn$sM~`N`^0)<9Cc@=;VjM`IWc+-viC#(%g|_hDc^BL&zcPOn%$Z_idddl`*I2Tq>}%)_(qZt469q}dV9}NG;C$nQSUt!8 z_0&$|=-7BHA!euz8KbAZ`jq<#L9c}j+;OgV{uT9i5YEPw?wVeAu|YgooKGeA~`eaC}(p`9#f4ZrK%X53Ivlazv)WCK;;tn!xxu{Koih+VyS&Cyz%oD+?2qiMY5@Oup__9t7!u>evRdx%Q zAWg?Ez?nisP6pgnYisCVADHhMGz8zp^U16D(Ix1F>ooM0RmDIOGw`&s;Ljfmr?FEN zzGhdW!wwaJUe@reX~utT$5ViHO-}S1niODo>1GV*XzaRidM*k&rs!(zIe_iJNsdnb_r% zH#Ul}K{p~rrVlO-{W5A@w&m~7*F3@^H`HjPSo+-KLE zN;BLwyChj_XEK=(!YAf}B-r_2u7E_e=yU&)LpD1Y;KvH@i|uFIVj=vaWc<(bEU}i* zcyi?cF-kAU{JNWSad?7P58XdN9ro3Wj0y>YJK8S)CgSCmOobQT3!BfOtzW&2dHQL1aVbC*Geb%g-UnluaWWE+g24;_ zQmNE>MrV&>q_CV{JDu_Db?yq$94Poo3##&e7vRBJ{OFQmR91;4rBa(m;4(G%7dgQ_ zAyQ(_3g}a%?0Ms)Nw_XI${ax8D z^SZ;tZ2Qzq%|g`%DJcy0kJ}jF&EmS;JDQAE0EzX5qQPZaB9|a_G&TKduydmE{r&%3 z)XT01u&y4=6*ro@HXfqt=f9#^Re-Jz6i`jO8Lh)S`&KOio#dpBDRR-|E0c9NDXF*8#!i6j_Q;b5;HM zm?GJ<&cC5lM6$npcmO8dn=_-&jF?|~b({grJLl(-tqxxVZ;9Z#eXq0I1y~n~NLBiW zJ_`nzr$YP7^eM#r3zniA3gaCQ6s9iA!UCkA`igU3%5w2QX5je8cwRpX8Bc(`^;;sE zFlE5lhW3I_X)PofzALkYFERx#iG3}?ze@*GBnS_zr)yE$N|lZIBq~d7oPOUgZ5~F7 zj-xl4hUQV^skm_o8az4%<*dZuuv5;%2;e--n}}wp6ChlC#Tz*yLN4}kmLyIiL1S!Uxi@X|wmQF8q7~v*q=U^O z^z7tTtaf24dzYasZYK;?YZY61YL?3TRm-a9pKhqr>R@a5H>3}qzg10a_m6(Rq_KxU zc0}^~KjUAwMuR50D-RF^FQNir8eV6S%D_N%5*uKl7ijLx4KC2a6(#WDX;~QSHFPPN z_>*=D1OVY8m}BooDBOz)&=gqjE34mHA@vp*&^bD5I-UfG366aV5UYl2m2FeZkky4P z(_sE;#=S(U1FdNByTY(?81q#iiq9bD? zy!Z`V?K?8u@`;uLbIxm5|5tIiu7Q2MGWUbARK;J)ePcPns)|9V4a~0KWe-7*s=gb< z^$@H5rC8vyEKJhZtZwMCeZ7eK(q7*nQAsh{epo_E?|NQzX>|MhYbTg2cP<6EgG@%s zH^2L^aq6HU1puSkT#NIHt~h=`!7hdruA60p<`9!*BFdVCkVH2iRF49!Q`$qvSq`oG z{h2U#$~ragZP(f5Z`?6Dd%#k;ux<$3w`TzV;N`})Tqu}b@aTtrsZ?bx6Ca;1%u&G( z!J&a>k2g1I1MQjHrJ`ZhR;xucpf~cgcGgx#Fqf~@2_>P)ecc`GNq+aGV&W}vCW3cz z0Ea0vAN@Y=X$tt!@k7AJQzH-fO$Ar(b8u&AJvAvb1qkulj$NF&)n3N7JmMsB2WsT+ z;w=Yn#USrH@y1X|(z?;aUeUegPCC04F7T0By=(8HHpf`TOoXyXH6Jt2^D@_@By2K3 zY&L(|K1&7IIvi)E_%@A{oZD(0=%gPf4-4WFYFml2S#ov9pN<>~BK#A;dC~%Dpo4ddBVk#+N8aHEkkJ{j<*vWq6Zl3Mo_NCzg)} z^V;pADH1Wf*r-|;p!yKvT6O#n;-1OD)z@GJ!CvbZ$1dy8l1nSYI}h%v`*(rWi>K9_ z+BhIWAlrK-3xNmE6AEq!Bt4}47>;_RR$`rxA?PcI9v}W|<6h34?ehiiDA9Cr3RCHX znZa8+K`hcNw@lt_A5B9WA+@yD0?64jQ<|UHG2~ z2n6y0ehQ)k3TN9(Wpi%Ni%ZHd-%ZpBPYCoxnltsRelCL%kmEOCqW}WHmD*{_KQkc9 zi$}i4FF^?iL+}|g+GHGfbJzVFOdO^ioy)deIIwZ?GFwTzd6ZZ3X$kuui^*i&E{|NN zVVn42ZYORR=BAA*+@H3$5Ml2e#XEZlI`0MQ}E^E%pFXe0;1&xcF6{}t_sVmVYP;=Vm4 zL3)69kD;3q?LhvW?eGccXY20MVzl}5&T8{A75i$Z!)PWdH_Kz@0(WyDao*g1WNya; z2D_+-2*7-?U%Otg@4yz+$RK;ShA)LHRBIKp)#*K`u_QwnGcXl1W4JeZw8l**9zbp{ zn<4eX5`lZP5P8+I+`2xwmA&NkfU^J_^1GDdPZF5L#`^CJ5v)X0;Q)H?XkFaf@x0#! z^ERm5HeNDNScZlHOrKvi6NIl`9_QcT(C-)qxlx#gK1z5Z(hJiUf^PFu{;qMz zoUMEAI)d@7@CoJI|4$2Gp9*>UXap7-X_dk24_7qBP_HBGr}!sd+*`=Ny`kt>Y^x?0 ze|AUd(wF}T=J(gt$~`Aq`X2_b{-A4Qnl|u>2Y|h;h1n#om=ad34^X4E1D#|W?bekY zQ@j~7j%cpV8%fCbMhpIt#(?i?HS-N6AcQi+_0D`q!Q`=b=7C3cCfQ5lfESf(iKVIc z8Ed~gO42}&t52*rG?qxyyRl4zMpv~&ys^gdFDxX0nL#I$+7L}tfHft}y*GD21 z@#JW2zY7GRBuPkci*eJH$EVKRt@p4);w#qX)Dx1-qQP$B|K;R*c>@%wf`1!aE;Y}h z>+Vn>CFMk=4$zc(Jk9C$M(w(4Z`sIUeo%R-pDP(R2{zQ->g=`|{X5sLqiEZ=f;&}y zgO-DCN~9(?loTuq6E#Qh^YWqr=o(iBc2AslXxd;`b z?g040X+8L(CD`yEGjaG((%LB$60DkUA3Y4DjA?SVwKQn_@yb4<<9}#PPF@UNys3H2 zP46n%q&*-6h&An*q4r7|-GD^~wj&|DmE4A$a*&E?rOIP8Jq; z0^|IHS=5HJyNbf`*iXrqz2%Z3$j`tKOScXa{)oro!se65aJ0=FOP?$yT5>oEWMyJR zCLwj*x?5d_j$Dr=VJGy!By`I^F@}sWBXokO*k#P=x+3KTV<0djb&&I3*+m`=HfGNP zZ9=Rno!A)&d>g#4YLAKbErNE^qW{bbnpuQk9XPa#t%eKv<%BUXNC(oNMSgi(*OQBA z#;jN+rw|}}@$(x50>xw~9|KiudjhxN<6x(C2Yz!_S8ojRe;=LuZkiiCTtskL0CDT* zHElwuhTOVUAQKl$dX3}KmpruNXfh@_pgjEM4K;}~^p|e$;1_c)y!H8hc@3lwZDMRb zdfN8}lhWM>Q#`4YTfb?}7w$XLB1{0j{rXiKQhQP6mf%K}KVgFazbTmP^r7D%QswZ% z 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 QCm>)v(l*qp({PCSA5T5DqyPW_ literal 0 HcmV?d00001 diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 374f5df..6f316b7 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -252,7 +252,7 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform game = find_game_by_name(game_name) player_id_turn = select( p for p in game.players if p.position == game.turn).first().id - + json_msg = { "event": utils.Events.NEW_TURN, "next_player_name": get_player_name_by_id(player_id_turn), diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8145e7b..6e8b39a 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -271,7 +271,8 @@ def play_action_card(game_name: str, play_info: PlayInformation): verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - objective_player: Player = find_player_by_id(play_info.objective_player_id) + objective_player: Player = find_player_by_id( + play_info.objective_player_id) if game.turn != 0 and objective_player.position < player.position: game.turn = game.turn - 1 diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 0e5be27..6a2c75e 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -12,10 +12,10 @@ async def send_event_cheat_used(player_id: int): async def send_list_of_cheats(player_id: int): cheat_messages: list[str] = [ - 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', - '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', - '[ws | whiskey | whisky]: Obtienes una carta whiskey', - '[ups | ooops]: Obtienes una carta ups!'] + 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', + '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', + '[ws | whiskey | whisky]: Obtienes una carta whiskey', + '[ups | ooops]: Obtienes una carta ups!'] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) await asyncio.sleep(0.25) @@ -41,7 +41,7 @@ async def handle_message(data, player_id): elif message == 'ws' or message == 'whisky' or message == 'whiskey': apply_cheat(game_name, player_id, range(40, 43)) await send_event_cheat_used(player_id) - + elif message == 'ooops': apply_cheat(game_name, player_id, range(108, 109)) await send_event_cheat_used(player_id) diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..91bb096 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,39 @@ +import os +import pygame +import time + + +def show_initial_image(): + # Path of the image I want to display + image_path = "./app/resources/stay_away.png" + + # Path of the audio file + audio_path = "./app/resources/stay_away.mp3" + + # pygame configuration + pygame.init() + window = pygame.display.set_mode((400, 300), pygame.NOFRAME) + + # Load the image + image = pygame.image.load(image_path) + + # Get the width and height of the window + window_width, window_height = window.get_size() + + # Scale the image to the size of the window + image = pygame.transform.scale(image, (window_width, window_height)) + + # Show the image in the window + window.blit(image, (0, 0)) + pygame.display.flip() + + # Play audio file + pygame.mixer.init() + pygame.mixer.music.load(audio_path) + pygame.mixer.music.play() + + # Wait 6 seconds + time.sleep(6) + + # Close the window + pygame.quit() diff --git a/poetry.lock b/poetry.lock index f6145e0..401a124 100644 --- a/poetry.lock +++ b/poetry.lock @@ -478,6 +478,72 @@ files = [ pydantic = ">=2.0.1" python-dotenv = ">=0.21.0" +[[package]] +name = "pygame" +version = "2.5.2" +description = "Python Game Development" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pygame-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da"}, + {file = "pygame-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1"}, + {file = "pygame-2.5.2-cp310-cp310-win32.whl", hash = "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584"}, + {file = "pygame-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f"}, + {file = "pygame-2.5.2-cp311-cp311-win32.whl", hash = "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50"}, + {file = "pygame-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4"}, + {file = "pygame-2.5.2-cp312-cp312-win32.whl", hash = "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592"}, + {file = "pygame-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4"}, + {file = "pygame-2.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945"}, + {file = "pygame-2.5.2-cp36-cp36m-win32.whl", hash = "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9"}, + {file = "pygame-2.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92"}, + {file = "pygame-2.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937"}, + {file = "pygame-2.5.2-cp37-cp37m-win32.whl", hash = "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7"}, + {file = "pygame-2.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda"}, + {file = "pygame-2.5.2-cp38-cp38-win32.whl", hash = "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff"}, + {file = "pygame-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae"}, + {file = "pygame-2.5.2-cp39-cp39-win32.whl", hash = "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d"}, + {file = "pygame-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6"}, + {file = "pygame-2.5.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6"}, + {file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"}, +] + [[package]] name = "pytest" version = "7.4.2" @@ -700,4 +766,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10,<3.11" -content-hash = "d88f5df5874122ce388d3da8cfd9993025124b291d6b02d4d1f0cd3deebf66f9" +content-hash = "32bfa640def987edbe3b40e044b2c2103a523fa0d86068c359c6ee7b10b071de" diff --git a/pyproject.toml b/pyproject.toml index 945f237..e80bf67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ autopep8 = "^2.0.4" httpx = "^0.25.0" pytest-cov = "^4.1.0" websockets = "^11.0.3" +pygame = "^2.5.2" [build-system] requires = ["poetry-core"] From 89c7a9f2238b3e7a266d2ba4c6e237b6db974fc0 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 04:43:19 -0300 Subject: [PATCH 110/224] Se implemento la logica para la carta determinacion (resolute) --- app/routers/games/action_functions.py | 27 +++++++++++++++++++++++++++ app/routers/games/games.py | 12 ++++++++++++ app/routers/games/schemas.py | 8 ++++++++ app/routers/games/services.py | 22 +++++++++++++++++++++- app/routers/games/utils.py | 2 ++ app/routers/websockets/services.py | 19 +++++++++++++------ 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 79a058b..bca763f 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -49,6 +49,14 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) +async def send_resolute_start_event(game: Game, player_id: int, option_cards: list[int]): + json_msg = { + "event": Events.RESOLUTE_CARD_PLAYED, + "option_cards": option_cards + } + await player_connections.send_event_to(player_id, json_msg) + + @db_session def process_flamethrower_card(game: Game, player: Player, card: Card, objective_player: Player): @@ -116,6 +124,25 @@ def process_whiskey_card(game: Game, player: Player, card: Card): game, player.id, player.name)) +@db_session +def process_resolute_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + + random_draw_deck_cards = select( + c for c in game.draw_deck if 2 <= c.id and c.id <= 88).random(3) + random_discard_deck_cards = select( + c for c in game.discard_deck if 2 <= c.id and c.id <= 88).random(3) + + while (len(random_draw_deck_cards) < 3): + random_draw_deck_cards.append(random_discard_deck_cards.pop()) + + option_cards_id = [card.id for card in random_draw_deck_cards] + + asyncio.ensure_future(send_resolute_start_event( + game, player.id, option_cards_id)) + + @db_session def process_watch_your_back_card(game: Game, player: Player, card: Card): if game.round_direction == RoundDirection.CLOCKWISE: diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4a4cb41..fbe759b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -253,3 +253,15 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return {"message": "Card interchange terminated."} + + +@router.patch("/{game_name}/resolute-exchange", status_code=status.HTTP_200_OK) +async def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): + utils.verify_player_in_game(game_data.player_id, game_name) + services.card_resolute_exchange(game_name, game_data) + json_msg = { + "event": utils.Events.RESOLUTE_DONE + } + await player_connections.send_event_to(game_data.player_id, json_msg) + + return {"message": "Resolute exchange terminated"} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index e31bed8..9d05fed 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -153,3 +153,11 @@ class InterchangeInformationIn(BaseModel): card_id: int # Card ID del jugador que recibe la intencion objective_player_id: int # ID jugador que inicia la intencion objective_card_id: int # Card ID del jugador que inicia la intencion + + +class ResoluteExchangeIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + card_in_hand: int + card_in_deck: int diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 629e1f0..6c7ed55 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -312,7 +312,7 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: # Determinacion if card.name == CardActionName.RESOLUTE: - pass + process_resolute_card(game, player, card) # Vigila tus espaldas if card.name == CardActionName.WATCH_YOUR_BACK: @@ -441,3 +441,23 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI next_player.hand.add(player_card) update_game_turn(game_name) + + +@db_session +def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + player_card: Card = find_card_by_id(game_data.card_in_hand) + deck_card: Card = find_card_by_id(game_data.card_in_deck) + + player.hand.remove(player_card) + player.hand.add(deck_card) + + game.discard_deck.add(player_card) + + if deck_card in game.draw_deck: + game.draw_deck.remove(deck_card) + game.draw_deck_order.remove(deck_card.id) + + elif deck_card in game.discard_deck: + game.discard_deck.remove(deck_card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index daf203f..be2cb53 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -23,6 +23,8 @@ class Events(str, Enum): PLAYED_CARD = 'played_card' PLAYER_ELIMINATED = 'player_eliminated' WHISKEY_CARD_PLAYED = 'whiskey_card_played' + RESOLUTE_CARD_PLAYED = 'resolute_card_played' + RESOLUTE_DONE = 'resolute_done' PLAYER_DRAW_CARD = 'player_draw_card' NEW_TURN = 'new_turn' diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 0e5be27..b087ff0 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -12,10 +12,13 @@ async def send_event_cheat_used(player_id: int): async def send_list_of_cheats(player_id: int): cheat_messages: list[str] = [ - 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', - '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', - '[ws | whiskey | whisky]: Obtienes una carta whiskey', - '[ups | ooops]: Obtienes una carta ups!'] + 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', + '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', + '[ws | whiskey | whisky]: Obtienes una carta whiskey', + '[ups | ooops]: Obtienes una carta ups!', + '[det | determinación | resolute]: Obtienes una carta determinación' + ] + for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) await asyncio.sleep(0.25) @@ -41,11 +44,15 @@ async def handle_message(data, player_id): elif message == 'ws' or message == 'whisky' or message == 'whiskey': apply_cheat(game_name, player_id, range(40, 43)) await send_event_cheat_used(player_id) - - elif message == 'ooops': + + elif message == 'ups' or message == 'ooops': apply_cheat(game_name, player_id, range(108, 109)) await send_event_cheat_used(player_id) + elif message == 'det' or message == 'determinación' or message == 'resolute': + apply_cheat(game_name, player_id, range(43, 48)) + await send_event_cheat_used(player_id) + async def websocket_games(player_id: int, websocket: WebSocket): await player_connections.connect(player_id, websocket) From 1a851127fe86f253fd77de64637b78d06756afc7 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 14:46:55 -0300 Subject: [PATCH 111/224] Implementacion de la carta olvidadizo y cheat para obtener esa carta implementado --- app/routers/games/games.py | 12 ++++++++++ app/routers/games/panic_functions.py | 17 ++++++++++++++ app/routers/games/schemas.py | 7 ++++++ app/routers/games/services.py | 33 +++++++++++++++++++++++++++- app/routers/games/utils.py | 2 ++ app/routers/websockets/services.py | 9 ++++++-- 6 files changed, 77 insertions(+), 3 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6f316b7..9c2fb2f 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -285,3 +285,15 @@ async def blind_date_interchange(game_name: str, game_data: IntentionExchangeInf } await player_connections.send_event_to(game_data.player_id, json_msg) return {"message": "Blind date interchange terminated."} + + +@router.patch("/{game_name}/forgetful-exchange", status_code=status.HTTP_200_OK) +async def card_forgetful_exchange(game_name: str, game_data: ForgetfulExchangeIn): + utils.verify_player_in_game(game_data.player_id, game_name) + services.card_forgetful_exchange(game_name, game_data) + json_msg = { + "event": utils.Events.FORGETFUL_DONE + } + await player_connections.send_event_to(game_data.player_id, json_msg) + + return {"message": "Forgetful exchange terminated"} diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index b61f0b3..9d35ae9 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -27,6 +27,15 @@ async def send_player_between_us_event(to_player_id: int, player_id: int, player await player_connections.send_event_to(to_player_id, json_msg) +async def send_forgetful_card_played(player_id: int, player_name: str): + json_msg = { + "event": Events.FORGETFUL_CARD_PLAYED, + "player_id": player_id, + "player_name": player_name + } + await player_connections.send_event_to(player_id, json_msg) + + async def send_round_and_round_start_event(game_name: str): json_msg = { "event": Events.ROUND_AND_ROUND_START @@ -75,6 +84,14 @@ def process_between_us_card(game: Game, player: Player, card: Card, objective_pl objective_player.id, player.id, player.name)) +@db_session +def process_forgetful_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + + asyncio.ensure_future(send_forgetful_card_played(player.id, player.name)) + + @db_session def process_round_and_round_card(game: Game, player: Player, card: Card): game.discard_deck.add(card) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 00e3b38..3d25e3a 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -160,3 +160,10 @@ class ShowRevelationsCardsIn(BaseModel): original_player_id: int # ID del jugador que jugo la carta Revelaciones show_my_cards: bool + + +class ForgetfulExchangeIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + cards_for_exchange: List[int] diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 6e8b39a..c231cbf 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -471,7 +471,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Olvidadizo if card.name == CardPanicName.FORGETFUL: - pass + process_forgetful_card(game, player, card) # Vuelta y vuelta if card.name == CardPanicName.ROUND_AND_ROUND: @@ -559,3 +559,34 @@ def blind_date_interchange(game_name: str, game_data: IntentionExchangeInformati if card_to_exchange and player_card: player.hand.remove(player_card) player.hand.add(card_to_exchange) + + +@db_session +def card_forgetful_exchange(game_name: str, game_data: ForgetfulExchangeIn): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + player_cards: list[Card] = [] + + for cardId in game_data.cards_for_exchange: + card = find_card_by_id(cardId) + player_cards.append(card) + + for card in player_cards: + player.hand.remove(card) + game.discard_deck.add(card) + + random_draw_deck_cards = select( + c for c in game.draw_deck if 2 <= c.id and c.id <= 88).random(3) + random_discard_deck_cards = select( + c for c in game.discard_deck if 2 <= c.id and c.id <= 88).random(3) + + while (len(random_draw_deck_cards) < 3): + random_draw_deck_cards.append(random_discard_deck_cards.pop()) + + for card in random_draw_deck_cards: + player.hand.add(card) + if card in game.draw_deck: + game.draw_deck.remove(card) + game.draw_deck_order.remove(card.id) + elif card in game.discard_deck: + game.discard_deck.remove(card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index f43547d..b1849c4 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -37,6 +37,8 @@ class Events(str, Enum): BLIND_DATE_SELECTION = 'blind_date_selection' BLIND_DATE_DONE = 'blind_date_done' OOOPS_CARD_PLAYED = 'ooops_card_played' + FORGETFUL_CARD_PLAYED = 'forgetful_card_played' + FORGETFUL_DONE = 'forgetful_done' @db_session diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 6a2c75e..65e548c 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -15,7 +15,8 @@ async def send_list_of_cheats(player_id: int): 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', '[ws | whiskey | whisky]: Obtienes una carta whiskey', - '[ups | ooops]: Obtienes una carta ups!'] + '[ups | ooops]: Obtienes una carta ups!', + '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo'] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) await asyncio.sleep(0.25) @@ -42,10 +43,14 @@ async def handle_message(data, player_id): apply_cheat(game_name, player_id, range(40, 43)) await send_event_cheat_used(player_id) - elif message == 'ooops': + elif message == 'ups' or message == 'ooops': apply_cheat(game_name, player_id, range(108, 109)) await send_event_cheat_used(player_id) + elif message == 'olv' or message == 'olvidadizo' or message == 'forgetful': + apply_cheat(game_name, player_id, range(93, 94)) + await send_event_cheat_used(player_id) + async def websocket_games(player_id: int, websocket: WebSocket): await player_connections.connect(player_id, websocket) From 96c4c13be7d0aa17bd599f06db650cc8500533e7 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 14:59:08 -0300 Subject: [PATCH 112/224] Cambio menor en el nombre de una funcion para que sea mas descriptiva --- app/routers/games/action_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index bca763f..190720c 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -49,7 +49,7 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) -async def send_resolute_start_event(game: Game, player_id: int, option_cards: list[int]): +async def send_resolute_card_played_event(game: Game, player_id: int, option_cards: list[int]): json_msg = { "event": Events.RESOLUTE_CARD_PLAYED, "option_cards": option_cards @@ -139,7 +139,7 @@ def process_resolute_card(game: Game, player: Player, card: Card): option_cards_id = [card.id for card in random_draw_deck_cards] - asyncio.ensure_future(send_resolute_start_event( + asyncio.ensure_future(send_resolute_card_played_event( game, player.id, option_cards_id)) From 73da466d21375356f7f1bbc37b22e9ad472f4fce Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 18:10:43 -0300 Subject: [PATCH 113/224] Implementacion de la carta uno dos --- app/database/models.py | 2 +- app/routers/games/games.py | 13 ++++++++++ app/routers/games/panic_functions.py | 38 ++++++++++++++++++++++++++-- app/routers/games/schemas.py | 7 +++++ app/routers/games/services.py | 12 ++++++++- app/routers/games/utils.py | 2 ++ app/routers/websockets/services.py | 8 +++++- 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 9e38578..4665822 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -9,7 +9,7 @@ class Player(db.Entity): game_hosting = Optional('Game', reverse='host', cascade_delete=True) name = Required(str) rol = Optional(str, nullable=True) - isQuatentined = Optional(bool, default=False) + isQuarentined = Optional(bool, default=False) position = Required(int, default="-1") hand = Set('Card') diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 9c2fb2f..0cb9418 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -297,3 +297,16 @@ async def card_forgetful_exchange(game_name: str, game_data: ForgetfulExchangeIn await player_connections.send_event_to(game_data.player_id, json_msg) return {"message": "Forgetful exchange terminated"} + + +@router.patch("/{game_name}/one-two-effect", status_code=status.HTTP_200_OK) +async def card_one_two_effect(game_name: str, game_data: OneTwoEffectIn): + utils.verify_player_in_game(game_data.player_id, game_name) + utils.verify_player_in_game(game_data.objective_player_id, game_name) + services.card_one_two_effect(game_data) + json_msg = { + "event": utils.Events.ONE_TWO_DONE + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + return {"message": "One two effect terminated"} diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index 9d35ae9..90cddf3 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -1,4 +1,4 @@ -from pony.orm import db_session +from pony.orm import db_session, select from app.database.models import Game, Card, Player from ..players.schemas import PlayerRol from ..cards.schemas import CardType, CardResponse @@ -51,6 +51,16 @@ async def send_revelations_card_played_event(game_name: str, original_player_id: await player_connections.send_event_to(next_player_id, json_msg) +async def send_one_two_card_played_event(player_id: int, player_name: str, player_list: list[int]): + json_msg = { + "event": Events.ONE_TWO_CARD_PLAYED, + "player_id": player_id, + "player_name": player_name, + "possible_interchange_id": player_list + } + await player_connections.send_event_to(player_id, json_msg) + + async def send_blind_date_selection_event(player_id: int): json_msg = { "event": Events.BLIND_DATE_SELECTION, @@ -67,6 +77,31 @@ def process_revelations_card(game: Game, player: Player, card: Card): game.name, player.id, next_player_id)) +@db_session +def process_one_two_card(game: Game, player: Player, card: Card): + game.discard_deck.add(card) + player.hand.remove(card) + + players_in_game = len( + select(p for p in game.players if p.rol != PlayerRol.ELIMINATED)[:]) + players_list: list[int] = [] + + if players_in_game > 4: + left_player_position = (player.position + 3) % players_in_game + left_player: Player = select( + p for p in game.players if p.position == left_player_position) + right_player_position = (player.position - 3) % players_in_game + right_player: Player = select( + p for p in game.players if p.position == right_player_position) + players_list.append(left_player.id) + players_list.append(right_player.id) + else: + players_list.append(player.id) + + asyncio.ensure_future(send_one_two_card_played_event( + player.id, player.name, players_list)) + + @db_session def process_ooops_card(game: Game, player: Player, card: Card): game.discard_deck.add(card) @@ -88,7 +123,6 @@ def process_between_us_card(game: Game, player: Player, card: Card, objective_pl def process_forgetful_card(game: Game, player: Player, card: Card): game.discard_deck.add(card) player.hand.remove(card) - asyncio.ensure_future(send_forgetful_card_played(player.id, player.name)) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 3d25e3a..a05c6b7 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -167,3 +167,10 @@ class ForgetfulExchangeIn(BaseModel): player_id: int cards_for_exchange: List[int] + + +class OneTwoEffectIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + objective_player_id: int diff --git a/app/routers/games/services.py b/app/routers/games/services.py index c231cbf..10050b6 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -455,7 +455,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Uno, dos... if card.name == CardPanicName.ONE_TWO: - pass + process_one_two_card(game, player, card) # Tres, cuatro if card.name == CardPanicName.THREE_FOUR: @@ -590,3 +590,13 @@ def card_forgetful_exchange(game_name: str, game_data: ForgetfulExchangeIn): game.draw_deck_order.remove(card.id) elif card in game.discard_deck: game.discard_deck.remove(card) + + +@db_session +def card_one_two_effect(game_data: OneTwoEffectIn): + player: Player = find_player_by_id(game_data.player_id) + objective_player: Player = find_player_by_id(game_data.objective_player_id) + + tempPosition = player.position + player.position = objective_player.position + objective_player.position = tempPosition diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index b1849c4..7fd2d08 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -39,6 +39,8 @@ class Events(str, Enum): OOOPS_CARD_PLAYED = 'ooops_card_played' FORGETFUL_CARD_PLAYED = 'forgetful_card_played' FORGETFUL_DONE = 'forgetful_done' + ONE_TWO_CARD_PLAYED = 'one_two_card_played' + ONE_TWO_DONE = "one_two_done" @db_session diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 65e548c..ba2bdd7 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -16,7 +16,9 @@ async def send_list_of_cheats(player_id: int): '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', '[ws | whiskey | whisky]: Obtienes una carta whiskey', '[ups | ooops]: Obtienes una carta ups!', - '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo'] + '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo', + '[uno dos | one two]: obtienes una carta uno, dos...' + ] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) await asyncio.sleep(0.25) @@ -50,6 +52,10 @@ async def handle_message(data, player_id): elif message == 'olv' or message == 'olvidadizo' or message == 'forgetful': apply_cheat(game_name, player_id, range(93, 94)) await send_event_cheat_used(player_id) + + elif message == 'uno dos' or message == 'one two': + apply_cheat(game_name, player_id, range(94, 96)) + await send_event_cheat_used(player_id) async def websocket_games(player_id: int, websocket: WebSocket): From 26fd8e22208065aec8fa80a51bd53ea9ba8ef69e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 19:45:37 -0300 Subject: [PATCH 114/224] =?UTF-8?q?Hotfix=20para=20la=20carta=20seducci?= =?UTF-8?q?=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/action_functions.py | 10 ++++++++++ app/routers/games/utils.py | 1 + app/routers/websockets/services.py | 10 +++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 79a058b..e0b68b0 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -49,6 +49,14 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) +async def send_seduction_done_event(player_id: int, objective_player_id: int): + json_msg = { + "event": Events.SEDUCTION_DONE + } + await player_connections.send_event_to(player_id, json_msg) + await player_connections.send_event_to(objective_player_id, json_msg) + + @db_session def process_flamethrower_card(game: Game, player: Player, card: Card, objective_player: Player): @@ -166,3 +174,5 @@ def process_seduction_card(game: Game, player: Player, game.discard_deck.add(card) player.hand.remove(card) + + send_seduction_done_event(player.id, objective_player.id) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index daf203f..de3bcf9 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -25,6 +25,7 @@ class Events(str, Enum): WHISKEY_CARD_PLAYED = 'whiskey_card_played' PLAYER_DRAW_CARD = 'player_draw_card' NEW_TURN = 'new_turn' + SEDUCTION_DONE = 'seduction_done' @db_session diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 0e5be27..6a2c75e 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -12,10 +12,10 @@ async def send_event_cheat_used(player_id: int): async def send_list_of_cheats(player_id: int): cheat_messages: list[str] = [ - 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', - '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', - '[ws | whiskey | whisky]: Obtienes una carta whiskey', - '[ups | ooops]: Obtienes una carta ups!'] + 'Soy Loki, Dios de las mentiras, estos son algunos cheats que puedes usar...', + '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', + '[ws | whiskey | whisky]: Obtienes una carta whiskey', + '[ups | ooops]: Obtienes una carta ups!'] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) await asyncio.sleep(0.25) @@ -41,7 +41,7 @@ async def handle_message(data, player_id): elif message == 'ws' or message == 'whisky' or message == 'whiskey': apply_cheat(game_name, player_id, range(40, 43)) await send_event_cheat_used(player_id) - + elif message == 'ooops': apply_cheat(game_name, player_id, range(108, 109)) await send_event_cheat_used(player_id) From 95a9e9df7ab59ac068e4cc2b608d52cd7ac6e919 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Sat, 28 Oct 2023 20:25:16 -0300 Subject: [PATCH 115/224] Hotfix por un error con los cheats --- app/routers/websockets/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/routers/websockets/utils.py b/app/routers/websockets/utils.py index 4840799..076a209 100644 --- a/app/routers/websockets/utils.py +++ b/app/routers/websockets/utils.py @@ -36,10 +36,13 @@ def apply_cheat(game_name: str, player_id: int, card_range): if not cheat_card: cheat_card = select( c for c in game.discard_deck if c.id in card_range).first() + if cheat_card: + game.discard_deck.remove(cheat_card) if cheat_card: - game.draw_deck.remove(cheat_card) - game.draw_deck_order.remove(cheat_card.id) + if cheat_card in game.draw_deck: + game.draw_deck.remove(cheat_card) + game.draw_deck_order.remove(cheat_card.id) player.hand.remove(random_card) player.hand.add(cheat_card) From 282d37df00eff2343846d948f6b26942748d60e1 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sun, 29 Oct 2023 20:45:52 -0300 Subject: [PATCH 116/224] se agrega docker-compose --- docker-compose.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..279e1ac --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + api: + build: + dockerfile: Dockerfile + context: . + volumes: + - database:/code/app/database/ + env_file: + - .env + ports: + - 8000:8000 + +volumes: + database: \ No newline at end of file From 337b6c05051474e0e20751f55a6314e83fefe9cc Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 30 Oct 2023 11:20:56 -0300 Subject: [PATCH 117/224] Se agrega ID del jugador que jugo lanzallamas en el evento player_eliminated --- app/routers/games/action_functions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 209abce..cfe552b 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -10,7 +10,7 @@ import asyncio -async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminated_name: str): +async def send_players_eliminated_event(game: Game, eliminated_by: int, eliminated_id: int, eliminated_name: str): players_positions = {} for p in game.players: if p.rol != PlayerRol.ELIMINATED: @@ -20,7 +20,8 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat "event": Events.PLAYER_ELIMINATED, "player_id": eliminated_id, "player_name": eliminated_name, - "players_positions": players_positions + "players_positions": players_positions, + "eliminated_by": eliminated_by } for p in game.players: await player_connections.send_event_to(p.id, json_msg) @@ -65,6 +66,7 @@ def process_flamethrower_card(game: Game, player: Player, player.hand.remove(card) asyncio.ensure_future(send_players_eliminated_event(game=game, + eliminated_by=player.id, eliminated_id=objective_player.id, eliminated_name=objective_player.name)) From 20a6ee46a9db2268990da5a87cd4d3827d5ae404 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 30 Oct 2023 13:43:45 -0300 Subject: [PATCH 118/224] Se agrega posibilidad de defenderse ante la carta seduccion --- app/main.py | 2 +- app/routers/cards/schemas.py | 8 ++++++++ app/routers/games/action_functions.py | 25 ++++++++++++++++++++++--- app/routers/games/games.py | 15 +++++++++++++++ app/routers/games/schemas.py | 2 ++ app/routers/games/services.py | 25 ++++++++++++++++++++++++- app/routers/games/utils.py | 26 ++++++++++++++++++++++++++ app/routers/websockets/services.py | 9 +++++++-- 8 files changed, 105 insertions(+), 7 deletions(-) diff --git a/app/main.py b/app/main.py index 3fa58e8..d410539 100644 --- a/app/main.py +++ b/app/main.py @@ -27,4 +27,4 @@ # This displays the initial image with the sound t = threading.Thread(target=show_initial_image) -t.start() +# t.start() diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index b8e8966..c446ca3 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -45,6 +45,14 @@ class CardPanicName(str, Enum): GETOUT_OF_HERE = '¡Sal de aquí!' +class CardDefenseName(str, Enum): + SCARY = 'Aterrador' + NO_THANKS = '¡No, gracias!' + COMFORTABLE = 'Aquí estoy bien' + MISSED = '¡Fallaste!' + NO_BARBECUE = '¡Nada de barbacoas!' + + class BaseCard(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index cfe552b..2892529 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -44,6 +44,19 @@ async def send_seduction_done_event(player_id: int, objective_player_id: int): await player_connections.send_event_to(objective_player_id, json_msg) +async def send_interchange_intention_event(player: Player, objective_player: Player, card: Card, card_to_exchange: Card): + json_msg = { + "event": Events.INTERCHANGE_INTENTION, + "player_id": player.id, + "player_name": player.name, + "card_played": card.id, + "card_name": card.name, + "card_to_exchange_id": card_to_exchange.id, + "card_to_exchange_name": card_to_exchange.name + } + await player_connections.send_event_to(objective_player.id, json_msg) + + @db_session def process_flamethrower_card(game: Game, player: Player, card: Card, objective_player: Player): @@ -147,9 +160,14 @@ def process_better_run_card(game: Game, player: Player, @db_session -def process_seduction_card(game: Game, player: Player, - card: Card, objective_player: Player, - card_to_exchange: Card): +def process_seduction_card(game: Game, player: Player, card: Card, objective_player: Player, card_to_exchange: Card): + asyncio.ensure_future(send_interchange_intention_event( + player, objective_player, card, card_to_exchange)) + game.discard_deck.add(card) + player.hand.remove(card) + ''' + Implementacion vieja: + objective_player_hand_list = list(objective_player.hand) eligible_cards = [ c for c in objective_player_hand_list if c.type != CardType.THE_THING] @@ -164,3 +182,4 @@ def process_seduction_card(game: Game, player: Player, player.hand.remove(card) send_seduction_done_event(player.id, objective_player.id) + ''' diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 0cb9418..1d6f310 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -310,3 +310,18 @@ async def card_one_two_effect(game_name: str, game_data: OneTwoEffectIn): await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return {"message": "One two effect terminated"} + + +@router.patch("/{game_name}/interchange-intention-response", status_code=status.HTTP_200_OK) +async def interchange_intention_response(game_name: str, game_data: InterchangeInformationIn): + utils.verify_player_in_game(game_data.player_id, game_name) + utils.verify_player_in_game(game_data.objective_player_id, game_name) + services.interchange_intention_response(game_data) + + json_msg = { + "event": utils.Events.INTERCHANGE_INTENTION_DONE + } + await player_connections.send_event_to(game_data.player_id, json_msg) + await player_connections.send_event_to(game_data.objective_player_id, json_msg) + + return {"message": "Interchange intention terminated"} diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index a05c6b7..abb5616 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -153,6 +153,8 @@ class InterchangeInformationIn(BaseModel): card_id: int # Card ID del jugador que recibe la intencion objective_player_id: int # ID jugador que inicia la intencion objective_card_id: int # Card ID del jugador que inicia la intencion + accept_interchange: Optional[bool] = Field( + None, description='Optional value') class ShowRevelationsCardsIn(BaseModel): diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 10050b6..a32e29e 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -8,7 +8,7 @@ from ..cards import services as cards_services from ..cards.utils import find_card_by_id, verify_action_card, verify_panic_card from ..players.utils import find_player_by_id, verify_card_in_hand, verify_player_not_in_quarentine -from ..cards.schemas import CardActionName, CardResponse, CardPanicName +from ..cards.schemas import CardActionName, CardResponse, CardPanicName, CardDefenseName from ..players.schemas import PlayerRol from .action_functions import * from .panic_functions import * @@ -600,3 +600,26 @@ def card_one_two_effect(game_data: OneTwoEffectIn): tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition + + +@db_session +def interchange_intention_response(game_data: InterchangeInformationIn): + player: Player = find_player_by_id(game_data.player_id) + objective_player: Player = find_player_by_id(game_data.objective_player_id) + player_card: Card = find_card_by_id(game_data.card_id) + objective_player_card: Card = find_card_by_id(game_data.objective_card_id) + + if game_data.accept_interchange: + player.hand.add(objective_player_card) + player.hand.remove(player_card) + objective_player.hand.add(player_card) + objective_player.hand.remove(objective_player_card) + else: + if player_card.name == CardDefenseName.SCARY or player_card.name == CardDefenseName.NO_THANKS: + player.hand.remove(player_card) + card_from_deck = get_stay_away_cards_from_decks(player.game, 1) + player.hand.add(card_from_deck[0]) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The card is not a valid defense card.") diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 736d3cd..d37b4e2 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -42,6 +42,8 @@ class Events(str, Enum): ONE_TWO_CARD_PLAYED = 'one_two_card_played' ONE_TWO_DONE = "one_two_done" SEDUCTION_DONE = 'seduction_done' + INTERCHANGE_INTENTION = 'interchange_intention' + INTERCHANGE_INTENTION_DONE = 'interchange_intention_done' @db_session @@ -553,3 +555,27 @@ def update_game_turn(game_name: str): game.turn = (game.turn + 1) % players_playing else: game.turn = (game.turn - 1) % players_playing + + +@db_session +def get_stay_away_cards_from_decks(game_name: str, quantity: int) -> List[Card]: + game: Game = find_game_by_name(game_name) + random_draw_deck_cards: list[Card] = [] + random_draw_deck_cards = select( + c for c in game.draw_deck if c.type == CardType.STAY_AWAY).random(quantity)[:] + num_cards = len(random_draw_deck_cards) + random_discard_deck_cards: list[Card] = [] + if num_cards < quantity: + random_discard_deck_cards = select( + c for c in game.discard_deck if c.type == CardType.STAY_AWAY).random(quantity-num_cards)[:] + for card in random_discard_deck_cards: + random_draw_deck_cards.append(card) + + for card in random_draw_deck_cards: + if card in game.draw_deck: + game.draw_deck.remove(card) + game.draw_deck_order.remove(card.id) + else: + game.discard_deck.remove(card) + + return random_draw_deck_cards diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index ba2bdd7..cc22ab8 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -17,7 +17,8 @@ async def send_list_of_cheats(player_id: int): '[ws | whiskey | whisky]: Obtienes una carta whiskey', '[ups | ooops]: Obtienes una carta ups!', '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo', - '[uno dos | one two]: obtienes una carta uno, dos...' + '[uno dos | one two]: Obtienes una carta uno, dos...', + '[sed | seducción | seduction]: Obtienes una carta de seducción' ] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) @@ -52,11 +53,15 @@ async def handle_message(data, player_id): elif message == 'olv' or message == 'olvidadizo' or message == 'forgetful': apply_cheat(game_name, player_id, range(93, 94)) await send_event_cheat_used(player_id) - + elif message == 'uno dos' or message == 'one two': apply_cheat(game_name, player_id, range(94, 96)) await send_event_cheat_used(player_id) + elif message == 'sed' or message == 'seducción' or message == 'seduction': + apply_cheat(game_name, player_id, range(55, 62)) + await send_event_cheat_used(player_id) + async def websocket_games(player_id: int, websocket: WebSocket): await player_connections.connect(player_id, websocket) From 17d34a4c74acaf590b428b470c073cce23fc2070 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 30 Oct 2023 15:02:47 -0300 Subject: [PATCH 119/224] Se pone sleep en funcion whisky --- app/routers/games/action_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index e0b68b0..d37d93b 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -46,6 +46,7 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st "player_id": player_id, "player_name": player_name } + asyncio.sleep(1) await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) From 3eb86b1a59cb95012ebbf49689ae0641eabf38aa Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 30 Oct 2023 15:11:36 -0300 Subject: [PATCH 120/224] Se manda despues el evento de new_turn al jugar la carta whiskey --- app/routers/cards/utils.py | 5 +++++ app/routers/games/action_functions.py | 16 ++++++++++++++-- app/routers/games/games.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 5318f83..ac8ed1d 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -39,3 +39,8 @@ def get_card_type_by_id(card_id: int) -> str: def is_flamethrower(card_id: int) -> bool: card: Card = find_card_by_id(card_id) return (card.name == CardActionName.FLAMETHROWER) + +@db_session +def is_whiskey(card_id: int) -> bool: + card: Card = find_card_by_id(card_id) + return (card.name == CardActionName.WHISKEY) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index d37d93b..75b1eb4 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -26,7 +26,7 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat await player_connections.send_event_to(p.id, json_msg) # Espera 4 segundos antes de enviar el siguiente evento - await asyncio.sleep(4) + await asyncio.sleep(2) with db_session: player_id_turn = select( @@ -46,9 +46,21 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st "player_id": player_id, "player_name": player_name } - asyncio.sleep(1) await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) + await asyncio.sleep(2) + + with db_session: + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + json_msg = { + "event": Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game.name, json_msg) + async def send_seduction_done_event(player_id: int, objective_player_id: int): json_msg = { diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4a4cb41..3623f50 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -181,7 +181,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - if not is_flamethrower(play_info.card_id): + if not is_flamethrower(play_info.card_id) or not is_whiskey(play_info.card_id): with db_session: game = find_game_by_name(game_name) player_id_turn = select( From 2c3b8d1bd83b3bae90825727b9cb22cc8f0a2f32 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 30 Oct 2023 15:12:47 -0300 Subject: [PATCH 121/224] modificacion menor en un import --- app/routers/games/games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 3623f50..7bc381e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -7,7 +7,7 @@ from ..websockets.utils import player_connections from .utils import find_game_by_name, is_the_game_finished from ..players.utils import get_player_name_by_id -from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower +from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower, is_whiskey from .services import finish_game From 32c59aed36ac536029428deec65c0347198f4602 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 30 Oct 2023 21:15:55 -0300 Subject: [PATCH 122/224] Se agrega id y name en el evento PLAYER_ELIMINATED --- app/routers/games/action_functions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 75b1eb4..1a82582 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -10,7 +10,7 @@ import asyncio -async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminated_name: str): +async def send_players_eliminated_event(game: Game, killer_id: int, killer_name: str, eliminated_id: int, eliminated_name: str): players_positions = {} for p in game.players: if p.rol != PlayerRol.ELIMINATED: @@ -18,7 +18,9 @@ async def send_players_eliminated_event(game: Game, eliminated_id: int, eliminat json_msg = { "event": Events.PLAYER_ELIMINATED, - "player_id": eliminated_id, + "killer_player_id": killer_id, + "killer_player_name": killer_name, + "eliminated_player_id": eliminated_id, "player_name": eliminated_name, "players_positions": players_positions } @@ -92,6 +94,8 @@ def process_flamethrower_card(game: Game, player: Player, player.hand.remove(card) asyncio.ensure_future(send_players_eliminated_event(game=game, + killer_id=player.id, + killer_name=player.name, eliminated_id=objective_player.id, eliminated_name=objective_player.name)) From aead93a6f49b9943b66add8eece87f9c20dda924 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Tue, 31 Oct 2023 00:55:38 -0300 Subject: [PATCH 123/224] se agrega tabla intention y logica para carta de defensa --- app/database/models.py | 8 ++++++ app/routers/cards/schemas.py | 8 ++++++ app/routers/games/defense_functions.py | 35 ++++++++++++++++++++++++++ app/routers/games/intention.py | 11 ++++++++ 4 files changed, 62 insertions(+) create mode 100644 app/routers/games/defense_functions.py create mode 100644 app/routers/games/intention.py diff --git a/app/database/models.py b/app/database/models.py index 96e6d1e..857f1e6 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -11,6 +11,8 @@ class Player(db.Entity): rol = Optional(str, nullable=True) position = Required(int, default="-1") hand = Set('Card') + intention_creator = Optional('Intention', reverse='player') + intention_objective = Optional('Intention', reverse='objective_player') class Game(db.Entity): @@ -38,3 +40,9 @@ class Card(db.Entity): games_discard_deck = Set(Game, reverse='discard_deck') games_draw_deck = Set(Game, reverse='draw_deck') players_hand = Set(Player) + + +class Intention(db.Entity): + player = Required(Player) + objective_player = Required(Player) + action_type = Required(str) diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 2e7af65..6576ca2 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -30,6 +30,14 @@ class CardActionName(str, Enum): SEDUCTION = 'Seducción' +class CardDefenseName(str, Enum): + SCARY = 'Aterrador' + I_AM_COMFORTABLE = 'Aquí estoy bien' + NO_THANKS = '¡No, gracias!' + MISSED = '¡Fallaste!' + NO_BARBECUE = '¡Nada de barbacoas!' + + class BaseCard(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/games/defense_functions.py b/app/routers/games/defense_functions.py new file mode 100644 index 0000000..8ffb39f --- /dev/null +++ b/app/routers/games/defense_functions.py @@ -0,0 +1,35 @@ +from pony.orm import * +from app.database.models import Player +from ..cards.schemas import CardActionName, CardDefenseName +from enum import Enum + + +class ActionType(str, Enum): + EXCHANGE_OFFER = "Ofrecimiento de intercambio" + CHANGE_PLACES = CardActionName.CHANGE_PLACES.value + BETTER_RUN = CardActionName.BETTER_RUN.value + FLAMETHROWER = CardActionName.FLAMETHROWER.value + + +response_to_action_type = { + ActionType.EXCHANGE_OFFER: [ + CardDefenseName.SCARY, + CardDefenseName.NO_THANKS, + CardDefenseName.MISSED + ], + ActionType.CHANGE_PLACES: [CardDefenseName.I_AM_COMFORTABLE], + ActionType.BETTER_RUN: [CardDefenseName.I_AM_COMFORTABLE], + ActionType.FLAMETHROWER: [CardDefenseName.NO_BARBECUE] +} + + +@db_session +def player_can_defend_himself(action_type: ActionType, player: Player) -> list[int]: + defense_cards = response_to_action_type[action_type] + player_defense_cards = [] + + for card in player.hand: + if card.name in defense_cards: + player_defense_cards.append(card.id) + + return player_defense_cards diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py new file mode 100644 index 0000000..af98eb1 --- /dev/null +++ b/app/routers/games/intention.py @@ -0,0 +1,11 @@ +from pony.orm import * + + +@db_session +def create_intention(): + pass + + +@db_session +def conclude_intention(): + pass From 1ed928e9efd923abab6051b18f1b782d3ec47d44 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Tue, 31 Oct 2023 13:03:53 -0300 Subject: [PATCH 124/224] reemplazo de la ejecucion del efecto a la creacion de la intencion --- app/routers/games/intention.py | 10 ++++++++-- app/routers/games/services.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index af98eb1..f348cf2 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -1,9 +1,15 @@ from pony.orm import * +from .defense_functions import * +from app.database import Intention @db_session -def create_intention(): - pass +def create_intention(action_type: ActionType, player: Player, objective_player: Player) -> Intention: + return Intention( + action_type=action_type, + player=player, + objective_player=objective_player + ) @db_session diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 629e1f0..dc75474 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -13,6 +13,7 @@ from .action_functions import * import random from app.routers.games import utils +from .intention import create_intention, ActionType def get_unstarted_games() -> List[GameResponse]: @@ -278,7 +279,8 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: if game.turn != 0 and objective_player.position < player.position: game.turn = game.turn - 1 - process_flamethrower_card(game, player, card, objective_player) + # process_flamethrower_card(game, player, card, objective_player) + create_intention(ActionType.FLAMETHROWER, player, objective_player) # Analisis if card.name == CardActionName.ANALYSIS: @@ -326,14 +328,18 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: players_not_eliminated - 1) objective_player: Player = find_player_by_id( play_info.objective_player_id) - process_change_places_card(game, player, card, objective_player) + + # process_change_places_card(game, player, card, objective_player) + create_intention(ActionType.CHANGE_PLACES, player, objective_player) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) objective_player: Player = find_player_by_id( play_info.objective_player_id) - process_better_run_card(game, player, card, objective_player) + + # process_better_run_card(game, player, card, objective_player) + create_intention(ActionType.BETTER_RUN, player, objective_player) # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) if card.name == CardActionName.SEDUCTION: From d499fcdccc3a337b85db753d8601e8af0e14ca02 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Thu, 2 Nov 2023 21:46:58 -0300 Subject: [PATCH 125/224] Tests de spring pasado para el modulo players --- .../player_tests/test_get_player_hand.py | 38 +++++++ app/tests/player_tests/test_players_utils.py | 103 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 app/tests/player_tests/test_get_player_hand.py create mode 100644 app/tests/player_tests/test_players_utils.py diff --git a/app/tests/player_tests/test_get_player_hand.py b/app/tests/player_tests/test_get_player_hand.py new file mode 100644 index 0000000..63755e1 --- /dev/null +++ b/app/tests/player_tests/test_get_player_hand.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from pony.orm import db_session +from app.database.models import Card, Player +from app.main import app + + +client = TestClient(app) + + +@db_session +def create_test_hand(player_id: int) -> list: + player = Player.get(id=player_id) + result = [] + for i in range(1, 5): + card = Card.get(id=i) + player.hand.add(card) + result.append(card.id) + return result + + +def test_get_player_hand_succes(): + test_hand = create_test_hand(1) + + response = client.get( + f'/players/1/hand') + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200(OK)" + + assert len( + response.json()) == len(test_hand), "El número de cartas en la mano del jugador no es el esperado." + + # Verifico que no haya cartas duplicadas en la mano del jugador + card_ids_in_hand = [card['id'] for card in response.json()] + assert len(card_ids_in_hand) == len(set(card_ids_in_hand) + ), "Se encontraron cartas duplicadas en la mano del jugador." + + for i in range(len(response.json())): + assert response.json()[ + i]['id'] in test_hand, f"Las cartas en la mano del jugador no son las esperadas. No se encontró la carta con ID {response.json()[i]['id']} en la mano del jugador." diff --git a/app/tests/player_tests/test_players_utils.py b/app/tests/player_tests/test_players_utils.py new file mode 100644 index 0000000..cf38687 --- /dev/null +++ b/app/tests/player_tests/test_players_utils.py @@ -0,0 +1,103 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Card, Player +from app.routers.players.utils import * +from pony.orm import * + +client = TestClient(app) + + +def create_test_player(): + response = client.post( + '/players', + json={'name': 'pepito'} + ) + player_id = response.json()['id'] + return player_id + + +def test_find_player_by_id_succes(): + # Create a player to find + player_id = create_test_player() + try: + # Call the function being tested + player = find_player_by_id(player_id) + + # Check that the returned player has the correct id and name + assert player.id == player_id, "El id del jugador retornado no coincide con el id del jugador creado." + assert player.name == 'pepito', "El nombre del jugador retornado no coincide con el nombre del jugador creado." + except HTTPException as e: + assert False, f"Se lanzó la excepción {e.status_code}. {e.detail}" + finally: + # Delete the player created + client.delete(f'/players/{player_id}') + + +def test_get_player_name_by_id_succes(): + # Create a player to find + player_id = create_test_player() + + # Call the function being tested + try: + player_name = get_player_name_by_id(player_id) + + assert player_name == 'pepito', "El nombre del jugador retornado no coincide con el nombre del jugador creado." + except HTTPException as e: + assert False, f"Se lanzó la excepción {e.status_code}. {e.detail}" + finally: + # Delete the player created + client.delete(f'/players/{player_id}') + + +def test_find_player_by_id_fail(): + response = client.post( + '/players', + json={'name': 'pepito'} + ) + player_id = response.json()['id'] + client.delete(f'/players/{player_id}') + try: + find_player_by_id(player_id) + except HTTPException as e: + assert e.status_code == 404, "El codigo de estado de la respuesta no es 404(NOT FOUND)" + assert e.detail == "Player not found", "El detalle de la respuesta no es el esperado." + else: + assert False, "No se lanzó la excepción HTTPException." + + +@db_session +def add_card_to_hand(player_id: int, card_id: int): + player = Player.get(id=player_id) + card = Card.get(id=card_id) + player.hand.add(card) + + +@db_session +def try_verify_card_in_hand(player_id: int, card_id: int): + player = Player.get(id=player_id) + card = Card.get(id=card_id) + verify_card_in_hand(player, card) + + +def test_verify_card_in_hand(): + player_id = create_test_player() + add_card_to_hand(player_id, 1) + try: + try_verify_card_in_hand(player_id, 1) + except HTTPException as e: + assert False, f"Se lanzó la excepción {e.status_code}. {e.detail}" + finally: + client.delete(f'/players/{player_id}') + + +def test_verify_card_in_hand_fail(): + player_id = create_test_player() + try: + try_verify_card_in_hand(player_id, 1) + except HTTPException as e: + assert e.status_code == 400, "El codigo de estado de la respuesta no es 400(BAD REQUEST)" + assert e.detail == "Card not in player hand", "El detalle de la respuesta no es el esperado." + else: + assert False, "No se lanzó la excepción HTTPException." + finally: + client.delete(f'/players/{player_id}') From ec830a993b040df7ad3a3bf4d9e4ffd83522d4c7 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Thu, 2 Nov 2023 22:09:12 -0300 Subject: [PATCH 126/224] 100% coverage report alcanzado --- .../player_tests/test_players_schemas.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/tests/player_tests/test_players_schemas.py diff --git a/app/tests/player_tests/test_players_schemas.py b/app/tests/player_tests/test_players_schemas.py new file mode 100644 index 0000000..f96a3a6 --- /dev/null +++ b/app/tests/player_tests/test_players_schemas.py @@ -0,0 +1,35 @@ +from app.routers.players.schemas import PlayerInfo +from app.routers.cards.schemas import CardResponse +from typing import List + + +def test_PlayerInfo_schema_was_the_thing_true_success(): + the_thing = CardResponse(id=1, number=4, type="THE_THING", subtype="CONTAGION", name="La Cosa", + description="You are La Cosa, infect or kill everyone") + + cards = [ + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + the_thing, + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + ] + player = PlayerInfo(name="player", id=1, position=1, + rol="THE_THING", hand=cards) + assert player.was_the_thing == True, "El jugador debería tener la carta 'La Cosa' en su mano." + + +def test_PlayerInfo_schema_was_the_thing_false_success(): + cards = [ + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + CardResponse(id=5, number=4, type="PANIC", subtype="PANIC", name="Not La Cosa", + description="You are La Cosa, infect or kill everyone"), + ] + player = PlayerInfo(name="player", id=1, position=1, + rol="THE_THING", hand=cards) + assert player.was_the_thing == False, "El jugador no debería tener la carta 'La Cosa' en su mano." From 294d66f2a5b4803dc02e8208c743198302e989e2 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Thu, 2 Nov 2023 22:10:14 -0300 Subject: [PATCH 127/224] minifix --- app/tests/player_tests/test_players_schemas.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/tests/player_tests/test_players_schemas.py b/app/tests/player_tests/test_players_schemas.py index f96a3a6..ff1381c 100644 --- a/app/tests/player_tests/test_players_schemas.py +++ b/app/tests/player_tests/test_players_schemas.py @@ -1,6 +1,5 @@ from app.routers.players.schemas import PlayerInfo from app.routers.cards.schemas import CardResponse -from typing import List def test_PlayerInfo_schema_was_the_thing_true_success(): @@ -18,7 +17,7 @@ def test_PlayerInfo_schema_was_the_thing_true_success(): ] player = PlayerInfo(name="player", id=1, position=1, rol="THE_THING", hand=cards) - assert player.was_the_thing == True, "El jugador debería tener la carta 'La Cosa' en su mano." + assert player.was_the_thing == True, "Error en el schema. El jugador debería tener la carta 'La Cosa' en su mano." def test_PlayerInfo_schema_was_the_thing_false_success(): @@ -32,4 +31,4 @@ def test_PlayerInfo_schema_was_the_thing_false_success(): ] player = PlayerInfo(name="player", id=1, position=1, rol="THE_THING", hand=cards) - assert player.was_the_thing == False, "El jugador no debería tener la carta 'La Cosa' en su mano." + assert player.was_the_thing == False, "Error en el schema. El jugador no debería tener la carta 'La Cosa' en su mano." From b0c20ac1e82670adeb31ff91d5beab8074d67590 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 3 Nov 2023 13:37:19 -0300 Subject: [PATCH 128/224] agrego relacion Intention - Game --- app/database/models.py | 2 ++ app/routers/games/intention.py | 12 ++++++++---- app/routers/games/services.py | 13 ++++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 857f1e6..a3af3fe 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -28,6 +28,7 @@ class Game(db.Entity): draw_deck = Set('Card', reverse='games_draw_deck') draw_deck_order = Required(Json, default=[]) round_direction = Required(str, default=RoundDirection.CLOCKWISE) + intention = Optional('Intention', reverse='game') class Card(db.Entity): @@ -43,6 +44,7 @@ class Card(db.Entity): class Intention(db.Entity): + game = Required(Game) player = Required(Player) objective_player = Required(Player) action_type = Required(str) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index f348cf2..cf35690 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -1,15 +1,19 @@ from pony.orm import * from .defense_functions import * -from app.database import Intention +from app.database import Intention, Game @db_session -def create_intention(action_type: ActionType, player: Player, objective_player: Player) -> Intention: - return Intention( +def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player) -> Intention: + intention = Intention( action_type=action_type, player=player, - objective_player=objective_player + objective_player=objective_player, + game=game ) + game.intention = intention + + return intention @db_session diff --git a/app/routers/games/services.py b/app/routers/games/services.py index dc75474..380d298 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -13,7 +13,7 @@ from .action_functions import * import random from app.routers.games import utils -from .intention import create_intention, ActionType +from .intention import create_intention_in_game, ActionType def get_unstarted_games() -> List[GameResponse]: @@ -280,7 +280,8 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: game.turn = game.turn - 1 # process_flamethrower_card(game, player, card, objective_player) - create_intention(ActionType.FLAMETHROWER, player, objective_player) + create_intention_in_game( + game, ActionType.FLAMETHROWER, player, objective_player) # Analisis if card.name == CardActionName.ANALYSIS: @@ -330,16 +331,18 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: play_info.objective_player_id) # process_change_places_card(game, player, card, objective_player) - create_intention(ActionType.CHANGE_PLACES, player, objective_player) + create_intention_in_game( + game, ActionType.CHANGE_PLACES, player, objective_player) # Mas vale que corras if card.name == CardActionName.BETTER_RUN: verify_player_in_game(play_info.objective_player_id, game_name) objective_player: Player = find_player_by_id( play_info.objective_player_id) - + # process_better_run_card(game, player, card, objective_player) - create_intention(ActionType.BETTER_RUN, player, objective_player) + create_intention_in_game( + game, ActionType.BETTER_RUN, player, objective_player) # Seduccion (Ojo porque esta carta modifica la mano del jugador objetivo) if card.name == CardActionName.SEDUCTION: From e4c76423d50c45ff66261d89f4b5a5bc84d08963 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 4 Nov 2023 17:51:53 -0300 Subject: [PATCH 129/224] generar intencion de intercambio de cartas --- app/database/models.py | 1 + app/routers/games/defense_functions.py | 2 +- app/routers/games/games.py | 19 +++++++++++++------ app/routers/games/intention.py | 5 +++-- app/routers/games/services.py | 15 ++++++++++++++- app/routers/games/utils.py | 16 ++++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index a3af3fe..40ec616 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -48,3 +48,4 @@ class Intention(db.Entity): player = Required(Player) objective_player = Required(Player) action_type = Required(str) + exchange_payload = Optional(Json, default={}) diff --git a/app/routers/games/defense_functions.py b/app/routers/games/defense_functions.py index 8ffb39f..bc53280 100644 --- a/app/routers/games/defense_functions.py +++ b/app/routers/games/defense_functions.py @@ -28,7 +28,7 @@ def player_can_defend_himself(action_type: ActionType, player: Player) -> list[i defense_cards = response_to_action_type[action_type] player_defense_cards = [] - for card in player.hand: + for card in player.hand.select(): if card.name in defense_cards: player_defense_cards.append(card.id) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4a4cb41..38c3139 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -9,6 +9,7 @@ from ..players.utils import get_player_name_by_id from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower from .services import finish_game +from .intention import * router = APIRouter( @@ -219,15 +220,21 @@ async def draw_card(game_name: str, game_data: DrawInformationIn): @router.patch("/{game_name}/intention-to-interchange-card", status_code=status.HTTP_200_OK) async def intention_to_interchange_card(game_name: str, interchange_info: IntentionExchangeInformationIn): utils.verify_if_interchange_can_be_done(game_name, interchange_info) - objective_player_id = utils.get_id_of_next_player_in_turn(game_name) + + exchange_intention = services.register_card_exchange_intention( + game_name, interchange_info) + json_msg = { "event": "exchange_intention", - "player_id": interchange_info.player_id, - "player_name": get_player_name_by_id(interchange_info.player_id), - "card_to_exchange": interchange_info.card_id + "player_id": exchange_intention.player.id, + "player_name": exchange_intention.player.name, + "card_to_exchange": interchange_info.card_id, + "defense_cards": player_can_defend_himself(ActionType.EXCHANGE_OFFER, exchange_intention.objective_player) } - await player_connections.send_event_to(objective_player_id, json_msg) - return {"message": "Card interchange intention terminated."} + + await player_connections.send_event_to(exchange_intention.objective_player.id, json_msg) + + return {"message": "Card exchange intention generated."} @router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index cf35690..ae92d84 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -4,12 +4,13 @@ @db_session -def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player) -> Intention: +def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player, exchange_payload: Optional) -> Intention: intention = Intention( action_type=action_type, player=player, objective_player=objective_player, - game=game + game=game, + exchange_payload=exchange_payload ) game.intention = intention diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 380d298..97d5a67 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -1,6 +1,6 @@ from pony.orm import * from typing import List -from app.database.models import Game, Player, Card +from app.database.models import Game, Player, Card, Intention from .schemas import * from ..players.schemas import PlayerRol from fastapi import HTTPException, status @@ -434,6 +434,19 @@ def get_game_result(name: str) -> GameResult: ) +@db_session +def register_card_exchange_intention(game_name: str, exchange_info: IntentionExchangeInformationIn) -> Intention: + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(exchange_info.player_id) + objective_player = utils.get_next_player_in_turn(game) + exchange_payload = {"card_id": exchange_info.card_id} + + exchange_intention = create_intention_in_game( + game, ActionType.EXCHANGE_OFFER, player, objective_player, exchange_payload) + + return exchange_intention + + @db_session def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): game: Game = find_game_by_name(game_name) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index daf203f..aa9a750 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -432,6 +432,22 @@ def get_id_of_next_player_in_turn(game_name): return next_player_id +@db_session +def get_next_player_in_turn(game: Game) -> Player: + players_playing = game.players.select( + lambda p: p.rol != PlayerRol.ELIMINATED).count() + + if game.round_direction == RoundDirection.CLOCKWISE: + next_turn = (game.turn + 1) % players_playing + else: + next_turn = (game.turn - 1) % players_playing + + next_player = game.players.select( + lambda p: p.position == next_turn).first() + + return next_player + + def is_the_game_finished(game_name: str) -> bool: game: Game = find_game_by_name(game_name) try: From d281f576b7a34e2579701d9bdfd73ca35ad6ab2b Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 4 Nov 2023 20:36:52 -0300 Subject: [PATCH 130/224] deuda de tests y hotfix en evento seduction --- app/routers/cards/services.py | 4 - app/routers/cards/utils.py | 1 + app/routers/games/action_functions.py | 3 +- app/tests/card_tests/test_cards_services.py | 24 +++- app/tests/card_tests/test_cards_utils.py | 133 ++++++++++++++++++++ 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 app/tests/card_tests/test_cards_utils.py diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 0e80918..53d603b 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -48,10 +48,6 @@ def find_card_by_id(id: int) -> Card: status_code=status.HTTP_404_NOT_FOUND, detail=f"Card with id '{id}' not found." ) - if id != card.id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Something went wrong with the card id" - ) return card diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index ac8ed1d..4a6c685 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -40,6 +40,7 @@ def is_flamethrower(card_id: int) -> bool: card: Card = find_card_by_id(card_id) return (card.name == CardActionName.FLAMETHROWER) + @db_session def is_whiskey(card_id: int) -> bool: card: Card = find_card_by_id(card_id) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 75b1eb4..7f3af93 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -188,4 +188,5 @@ def process_seduction_card(game: Game, player: Player, game.discard_deck.add(card) player.hand.remove(card) - send_seduction_done_event(player.id, objective_player.id) + asyncio.ensure_future(send_seduction_done_event( + player.id, objective_player.id)) diff --git a/app/tests/card_tests/test_cards_services.py b/app/tests/card_tests/test_cards_services.py index 56b7eda..3b3d578 100644 --- a/app/tests/card_tests/test_cards_services.py +++ b/app/tests/card_tests/test_cards_services.py @@ -12,12 +12,12 @@ @db_session def create_card_for_testing(id: int) -> Card: - return Card(id=id, number=4, type="THE_THING", name="The Thing", + return Card(id=id, number=4, type="THE_THING", subtype=CardSubtype.CONTAGION, name="The Thing", description="You are the thing, infect or kill everyone") @db_session -def create_test_player(name: str): +def create_test_player(name: str) -> Player: return Player(name=name) @@ -122,3 +122,23 @@ def test_build_draw_deck_4_players() -> None: draw_deck) == 19, f"El tamaño del mazo de robo esperado es 19. El tamaño del mazo de robo obtenido es {len(draw_deck)}." cleanup_database() + + +@db_session +def test_card_is_in_player_hand_success(): + cleanup_database() + # Create a player and add a card to their hand + player = create_test_player("test_player") + card = create_card_for_testing(1) + player.hand.add(card) + commit() + + # Test that the function returns True when the card is in the player's hand + assert card_is_in_player_hand( + "The Thing", player), f"Error: The function did not return True when the card was in the player's hand." + + # Test that the function returns False when the card is not in the player's hand + assert card_is_in_player_hand( + "nonexistent_card", player) == False, f"Error: The function did not return False when the card was not in the player's hand." + + cleanup_database() diff --git a/app/tests/card_tests/test_cards_utils.py b/app/tests/card_tests/test_cards_utils.py new file mode 100644 index 0000000..65866cd --- /dev/null +++ b/app/tests/card_tests/test_cards_utils.py @@ -0,0 +1,133 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import * +from app.routers.cards.schemas import * +from fastapi import status +from pony.orm import db_session +from app.routers.cards.utils import * + + +client = TestClient(app) + + +@db_session +def mock_data() -> CardResponse: + Card(id=1, number=4, type='THE_THING', + subtype='CONTAGION', + name="The Thing", + description="You are the thing, infect or kill everyone" + ) + + card = Card.get(id=1) + return card + + +@db_session +def delete_mock_data(id: int): + Card.get(id=id).delete() + + +@db_session +def action_mock_data() -> Card: + return (Card(id=2, number=4, type=CardType.STAY_AWAY, subtype=CardSubtype.ACTION, name="Action Card", + description="This is an action card")) + + +def test_find_card_by_id_success(): + mock_response = mock_data() + response = find_card_by_id(mock_response.id) + try: + assert mock_response.id == response.id, "Find card by id failed" + finally: + delete_mock_data(mock_response.id) + + +def test_find_card_by_id_fail(): + card_id = 109 + client.delete(f'/cards/{card_id}') + + try: + find_card_by_id(card_id) + except HTTPException as e: + assert e.status_code == status.HTTP_404_NOT_FOUND, f"El status code obtenido es {e.status_code} y debería ser {status.HTTP_404_NOT_FOUND}" + assert e.detail == f"Card not found", f"El detalle obtenido es {e.detail} y debería ser 'Card not found.'" + + +@db_session +def test_verify_action_card_success(): + card = action_mock_data() + try: + verify_action_card(card) + except HTTPException as e: + assert False, f"Unexpected HTTPException: {e}" + finally: + card.delete() + + +@db_session +def test_verify_action_card_failure(): + card = Card(id=1, number=4, type="PANIC", subtype=CardSubtype.PANIC, name="Panic Card", + description="This is a panic card") + try: + verify_action_card(card) + assert False, "Expected HTTPException not raised" + except HTTPException as e: + assert e.status_code == status.HTTP_400_BAD_REQUEST, f"El status code obtenido es {e.status_code} y debería ser {status.HTTP_400_BAD_REQUEST}" + finally: + card.delete() + + +@db_session +def test_get_card_name_by_id_success(): + # Create a test card + card = Card(id=1, number=4, type="PANIC", subtype=CardSubtype.PANIC, name="Test Card", + description="This is a panic card") + + # Call the function with the test card's ID + result = get_card_name_by_id(card.id) + card.delete() + + # Check that the result matches the test card's name + assert result == "Test Card", f"Error: The function did not return the correct card name ('Test Card'). Instead, it returned ({ result })." + + +@db_session +def test_get_card_type_by_id_success(): + # Create a test card + card = Card(id=1, number=4, type="PANIC", subtype=CardSubtype.PANIC, name="Test Card", + description="This is a panic card") + + # Call the function with the card's ID + card_type = get_card_type_by_id(card.id) + card.delete() + + # Check that the returned type matches the card's type + assert card_type == "PANIC", f"Error: The function did not return the correct card type. Instead, it returned ({ card_type })." + + +@db_session +def test_is_flamethrower_success(): + # Create a test card + card = Card(id=1, number=4, type=CardType.STAY_AWAY, subtype=CardSubtype.ACTION, name=CardActionName.FLAMETHROWER, + description="This is a panic card") + + # Call the function with the card's ID + response = is_flamethrower(card.id) + card.delete() + + # Check that the returned type matches the card's type + assert response, f"Error: The function returned {response} instead of True." + + +@db_session +def test_is_whisky_success(): + # Create a test card + card = Card(id=1, number=4, type=CardType.STAY_AWAY, subtype=CardSubtype.ACTION, name=CardActionName.WHISKEY, + description="This is a WHISKEY card") + + # Call the function with the card's ID + response = is_whiskey(card.id) + card.delete() + + # Check that the returned type matches the card's type + assert response, f"Error: The function returned {response} instead of True." From b203c3b19747afc7049cb5c56b780d7fa6d1cd07 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 4 Nov 2023 20:51:51 -0300 Subject: [PATCH 131/224] =?UTF-8?q?Actualizaci=C3=B3n=20de=20campos=20de?= =?UTF-8?q?=20evento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/action_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index c807a0c..7bd22b8 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -21,7 +21,7 @@ async def send_players_eliminated_event(game: Game, killer_id: int, killer_name: "killer_player_id": killer_id, "killer_player_name": killer_name, "eliminated_player_id": eliminated_id, - "player_name": eliminated_name, + "eliminated_player_name": eliminated_name, "players_positions": players_positions } for p in game.players: From fab9776b2941ac2275063a0cecd68ac9c066b951 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 4 Nov 2023 22:33:40 -0300 Subject: [PATCH 132/224] renombre funcion --- app/routers/games/defense_functions.py | 2 +- app/routers/games/games.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/defense_functions.py b/app/routers/games/defense_functions.py index bc53280..834b08a 100644 --- a/app/routers/games/defense_functions.py +++ b/app/routers/games/defense_functions.py @@ -24,7 +24,7 @@ class ActionType(str, Enum): @db_session -def player_can_defend_himself(action_type: ActionType, player: Player) -> list[int]: +def player_cards_to_defend_himself(action_type: ActionType, player: Player) -> list[int]: defense_cards = response_to_action_type[action_type] player_defense_cards = [] diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 38c3139..c1d7496 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -229,7 +229,7 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent "player_id": exchange_intention.player.id, "player_name": exchange_intention.player.name, "card_to_exchange": interchange_info.card_id, - "defense_cards": player_can_defend_himself(ActionType.EXCHANGE_OFFER, exchange_intention.objective_player) + "defense_cards": player_cards_to_defend_himself(ActionType.EXCHANGE_OFFER, exchange_intention.objective_player) } await player_connections.send_event_to(exchange_intention.objective_player.id, json_msg) From 7a97f7474e02921f6fa11cc0cbdce43da7e1307c Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sun, 5 Nov 2023 04:02:29 -0300 Subject: [PATCH 133/224] endpoint para jugar carta de defensa --- app/routers/cards/services.py | 2 -- app/routers/games/action_functions.py | 51 ++++++++++----------------- app/routers/games/games.py | 12 +++++++ app/routers/games/intention.py | 47 +++++++++++++++++++++--- app/routers/games/schemas.py | 7 ++++ app/routers/games/services.py | 21 ++++++----- app/routers/games/utils.py | 41 +++++++++++++++++++++ 7 files changed, 134 insertions(+), 47 deletions(-) diff --git a/app/routers/cards/services.py b/app/routers/cards/services.py index 0e80918..da6ba1b 100644 --- a/app/routers/cards/services.py +++ b/app/routers/cards/services.py @@ -1,7 +1,5 @@ from pony.orm import * from app.database.models import Game, Player, Card -from app.routers.games.utils import find_game_by_name -from app.routers.players.services import find_player_by_id import random from .schemas import * from fastapi import HTTPException, status diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 79a058b..4678d18 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -50,8 +50,7 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st @db_session -def process_flamethrower_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_flamethrower_card(game: Game, player: Player, objective_player: Player): objective_player.rol = PlayerRol.ELIMINATED # Las cartas del jugador eliminado van al mazo de descarte @@ -67,17 +66,13 @@ def process_flamethrower_card(game: Game, player: Player, p.position -= 1 objective_player.position = -1 - game.discard_deck.add(card) - player.hand.remove(card) - asyncio.ensure_future(send_players_eliminated_event(game=game, eliminated_id=objective_player.id, eliminated_name=objective_player.name)) @db_session -def process_analysis_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_analysis_card(game: Game, player: Player, objective_player: Player): result = [CardResponse(id=c.id, number=c.number, type=c.type, @@ -85,14 +80,12 @@ def process_analysis_card(game: Game, player: Player, name=c.name, description=c.description ) for c in objective_player.hand] - game.discard_deck.add(card) - player.hand.remove(card) + return result @db_session -def process_suspicious_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_suspicious_card(game: Game, player: Player, objective_player: Player): objective_player_hand_list = list(objective_player.hand) random_card = random.choice(objective_player_hand_list) result = CardResponse( @@ -103,56 +96,42 @@ def process_suspicious_card(game: Game, player: Player, name=random_card.name, description=random_card.description ) - game.discard_deck.add(card) - player.hand.remove(card) + return result @db_session -def process_whiskey_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) +def process_whiskey_card(game: Game, player: Player): asyncio.ensure_future(send_players_whiskey_event( game, player.id, player.name)) @db_session -def process_watch_your_back_card(game: Game, player: Player, card: Card): +def process_watch_your_back_card(game: Game, player: Player): if game.round_direction == RoundDirection.CLOCKWISE: game.round_direction = RoundDirection.COUNTERCLOCKWISE else: game.round_direction = RoundDirection.CLOCKWISE - game.discard_deck.add(card) - player.hand.remove(card) - @db_session -def process_change_places_card(game: Game, player: Player, card: Card, objective_player: Player): +def process_change_places_card(game: Game, player: Player, objective_player: Player): # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition - game.discard_deck.add(card) - player.hand.remove(card) - @db_session -def process_better_run_card(game: Game, player: Player, - card: Card, objective_player: Player): +def process_better_run_card(game: Game, player: Player, objective_player: Player): # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition - game.discard_deck.add(card) - player.hand.remove(card) - @db_session -def process_seduction_card(game: Game, player: Player, - card: Card, objective_player: Player, +def process_seduction_card(game: Game, player: Player, objective_player: Player, card_to_exchange: Card): objective_player_hand_list = list(objective_player.hand) eligible_cards = [ @@ -164,5 +143,11 @@ def process_seduction_card(game: Game, player: Player, objective_player.hand.remove(random_card) objective_player.hand.add(card_to_exchange) - game.discard_deck.add(card) - player.hand.remove(card) + +@db_session +def process_card_exchange(player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): + player.hand.remove(player_card) + player.hand.add(objective_player_card) + + objective_player.hand.remove(objective_player_card) + objective_player.hand.add(player_card) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index c1d7496..34b018b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -260,3 +260,15 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform await player_connections.send_event_to_all_players_in_game(game_name, json_msg) return {"message": "Card interchange terminated."} + + +@router.post("/{game_name}/play-defense-card") +async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): + utils.verify_defense_card_can_be_played(game_name, defense_info) + + if defense_info.card_id: + services.play_defense_card(game_name, defense_info) + else: + process_intention_in_game(game_name) + + clean_intention_in_game(game_name) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index ae92d84..59a4c66 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -1,10 +1,18 @@ from pony.orm import * from .defense_functions import * -from app.database import Intention, Game +from app.database import Intention, Game, Player +from ..cards.services import find_card_by_id +from .services import ( + find_game_by_name, + process_change_places_card, + process_better_run_card, + process_flamethrower_card, + process_card_exchange +) @db_session -def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player, exchange_payload: Optional) -> Intention: +def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player, exchange_payload=None) -> Intention: intention = Intention( action_type=action_type, player=player, @@ -18,5 +26,36 @@ def create_intention_in_game(game: Game, action_type: ActionType, player: Player @db_session -def conclude_intention(): - pass +def process_intention_in_game(game_name) -> Intention: + game: Game = find_game_by_name(game_name) + intention: Intention = game.intention + player = intention.player + objective_player = intention.objective_player + + match intention.action_type: + case ActionType.EXCHANGE_OFFER: + exchange_info = intention.exchange_payload + + player_card = find_card_by_id(exchange_info.card_id) + objective_player_card = find_card_by_id( + exchange_info.objective_card_id) + + process_card_exchange(player, objective_player, + player_card, objective_player_card) + + case ActionType.CHANGE_PLACES: + process_change_places_card(game, player, objective_player) + + case ActionType.BETTER_RUN: + process_better_run_card(game, player, objective_player) + + case ActionType.FLAMETHROWER: + process_flamethrower_card(game, player, objective_player) + + return intention + + +@db_session +def clean_intention_in_game(game_name): + game: Game = find_game_by_name(game_name) + Intention.get(game=game).delete() diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index e31bed8..80cd205 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -153,3 +153,10 @@ class InterchangeInformationIn(BaseModel): card_id: int # Card ID del jugador que recibe la intencion objective_player_id: int # ID jugador que inicia la intencion objective_card_id: int # Card ID del jugador que inicia la intencion + + +class PlayDefenseInformation(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int + card_id: Optional[int] diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 97d5a67..da24434 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -279,7 +279,6 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: if game.turn != 0 and objective_player.position < player.position: game.turn = game.turn - 1 - # process_flamethrower_card(game, player, card, objective_player) create_intention_in_game( game, ActionType.FLAMETHROWER, player, objective_player) @@ -330,7 +329,6 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: objective_player: Player = find_player_by_id( play_info.objective_player_id) - # process_change_places_card(game, player, card, objective_player) create_intention_in_game( game, ActionType.CHANGE_PLACES, player, objective_player) @@ -340,7 +338,6 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: objective_player: Player = find_player_by_id( play_info.objective_player_id) - # process_better_run_card(game, player, card, objective_player) create_intention_in_game( game, ActionType.BETTER_RUN, player, objective_player) @@ -359,6 +356,9 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: process_seduction_card( game, player, card, objective_player, card_to_exchange) + game.discard_deck.add(card) + player.hand.remove(card) + update_game_turn(game_name) return result @@ -456,10 +456,15 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI next_player: Player = find_player_by_id(game_data.player_id) next_player_card: Card = Card[game_data.card_id] - player.hand.remove(player_card) - next_player.hand.remove(next_player_card) - - player.hand.add(next_player_card) - next_player.hand.add(player_card) + process_card_exchange(player, next_player, player_card, next_player_card) update_game_turn(game_name) + + +@db_session +def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): + game: Game = find_game_by_name(game_name) + card: Card = find_card_by_id(defense_info.card_id) + intention: Intention = game.intention + + intention.objective_player.hand.remove(card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index aa9a750..f0381f7 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -8,6 +8,8 @@ from ..players.schemas import PlayerRol from ..players.utils import find_player_by_id from ..cards.schemas import CardType, CardSubtype +from ..cards.services import find_card_by_id +from ..games.defense_functions import player_cards_to_defend_himself class Events(str, Enum): @@ -466,3 +468,42 @@ def update_game_turn(game_name: str): game.turn = (game.turn + 1) % players_playing else: game.turn = (game.turn - 1) % players_playing + + +@db_session +def verify_defense_card_can_be_played(game_name: str, defense_info: PlayDefenseInformation): + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(defense_info.player_id) + + if not game.intention: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='No intention to conclude in the game.' + ) + + if game.intention.objective_player != defense_info.player_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Does not correspond to the objective player for the intention.' + ) + + if defense_info.card_id: + card: Card = find_card_by_id(defense_info.card_id) + + if card.subtype != CardSubtype.DEFENSE: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card is not for defense.' + ) + + if not card in player.hand.select(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card is not in the player hand.' + ) + + if not card.id not in player_cards_to_defend_himself(game.intention.action_type, player): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='The card does not work to defend this action type.' + ) From 5643a530053882ba00b4b6ec35f101a03106677c Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sun, 5 Nov 2023 04:09:59 -0300 Subject: [PATCH 134/224] fix tremendo xd --- app/routers/games/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index f0381f7..dc64d9e 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -502,7 +502,7 @@ def verify_defense_card_can_be_played(game_name: str, defense_info: PlayDefenseI detail='The card is not in the player hand.' ) - if not card.id not in player_cards_to_defend_himself(game.intention.action_type, player): + if not card.id in player_cards_to_defend_himself(game.intention.action_type, player): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='The card does not work to defend this action type.' From 8065addac5769ac2290042067065ee07b7af36e2 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sun, 5 Nov 2023 17:29:05 -0300 Subject: [PATCH 135/224] al jugar la carta de defensa se roba hasta tener una Alejate --- app/routers/games/services.py | 28 ++++++++++++++++++++-------- app/routers/games/utils.py | 3 +-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index da24434..779f793 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -365,19 +365,25 @@ def play_action_card(game_name: str, play_info: PlayInformation) -> Game: @db_session -def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOut: - game: Game = find_game_by_name(game_name) - player: Player = find_player_by_id(game_data.player_id) - +def draw_card_by_drawing_order(game: Game) -> Card: if len(game.draw_deck_order) == 1: - merge_decks_of_card(game_name) + merge_decks_of_card(game) top_card_id = game.draw_deck_order.pop(0) - card = select(card for card in game.draw_deck if card.id == - top_card_id).first() + top_card = game.draw_deck.select( + lambda card: card.id == top_card_id).first() + game.draw_deck.remove(top_card) + return top_card + + +@db_session +def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOut: + game: Game = find_game_by_name(game_name) + player: Player = find_player_by_id(game_data.player_id) + + card = draw_card_by_drawing_order(game) player.hand.add(card) - game.draw_deck.remove(card) top_card_face = select( card for card in game.draw_deck if card.id == game.draw_deck_order[0]).first().type @@ -468,3 +474,9 @@ def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): intention: Intention = game.intention intention.objective_player.hand.remove(card) + + # Draw card until a card of type StayAway is obtained + while (top_card := draw_card_by_drawing_order(game)).type != CardType.STAY_AWAY: + game.discard_deck.add(top_card) + + intention.objective_player.hand.add(top_card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index dc64d9e..5a50fba 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -287,8 +287,7 @@ def verify_adjacent_players(player_id: int, other_player_id: int, max_position: @db_session -def merge_decks_of_card(game_name: str): - game: Game = find_game_by_name(game_name) +def merge_decks_of_card(game: Game): top_card_id = game.draw_deck_order.pop(0) new_deck_list = list(game.draw_deck) + list(game.discard_deck) game.draw_deck.clear() From 317f832543cc1113893d8244e264626c2b57eaa9 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 6 Nov 2023 11:43:09 -0300 Subject: [PATCH 136/224] al finalizar el intercambio de cartas de borra la intencion --- app/routers/games/games.py | 2 ++ app/routers/games/services.py | 5 +++-- app/routers/games/utils.py | 16 +++------------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 34b018b..1174755 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -241,6 +241,8 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) + clean_intention_in_game(game_name) + json_msg = { "event": "exchange_done" } diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 779f793..c655f69 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -457,10 +457,11 @@ def register_card_exchange_intention(game_name: str, exchange_info: IntentionExc def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(game_data.objective_player_id) - player_card: Card = Card[game_data.objective_card_id] + player_card: Card = cards_services.find_card_by_id( + game_data.objective_card_id) next_player: Player = find_player_by_id(game_data.player_id) - next_player_card: Card = Card[game_data.card_id] + next_player_card: Card = cards_services.find_card_by_id(game_data.card_id) process_card_exchange(player, next_player, player_card, next_player_card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 5a50fba..87fd97a 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -339,7 +339,7 @@ def verify_draw_can_be_done(game_name: str, game_data: DiscardInformationIn): def verify_if_interchange_can_be_done(game_name: str, interchange_info: IntentionExchangeInformationIn): game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(interchange_info.player_id) - card: Card = Card[interchange_info.card_id] + card: Card = find_card_by_id(interchange_info.card_id) if game.turn != player.position: raise HTTPException( @@ -367,20 +367,10 @@ def verify_if_interchange_can_be_done(game_name: str, interchange_info: Intentio def verify_if_interchange_response_can_be_done(game_name: str, game_data: InterchangeInformationIn): game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(game_data.player_id) - player_card: Card = Card[game_data.card_id] + player_card: Card = find_card_by_id(game_data.card_id) objective_player: Player = find_player_by_id(game_data.objective_player_id) - objective_player_card: Card = Card[game_data.objective_card_id] + objective_player_card: Card = find_card_by_id(game_data.objective_card_id) - if not player_card: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail='Card of next player in turn not found' - ) - if not objective_player_card: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail='Card of the player in turn not found' - ) if player_card.type == CardType.THE_THING or objective_player_card.type == CardType.THE_THING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From ef4e578b4e3433558fed808196e3fe02994af414 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 6 Nov 2023 11:52:35 -0300 Subject: [PATCH 137/224] agrego checkeo de intencion antes de intercambiar cartas --- app/routers/games/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 87fd97a..b506b35 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -371,6 +371,12 @@ def verify_if_interchange_response_can_be_done(game_name: str, game_data: Interc objective_player: Player = find_player_by_id(game_data.objective_player_id) objective_player_card: Card = find_card_by_id(game_data.objective_card_id) + if not game.intention: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='No intention to exchange.' + ) + if player_card.type == CardType.THE_THING or objective_player_card.type == CardType.THE_THING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From 23abc496b3fba3b3372035334ffe9de3bef90d77 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 6 Nov 2023 16:36:17 -0300 Subject: [PATCH 138/224] se comenta el borrado de la intencion en interchange_response --- app/routers/games/games.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 0b7715f..fca6d8f 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -243,7 +243,8 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) - clean_intention_in_game(game_name) + + # clean_intention_in_game(game_name) json_msg = { "event": "exchange_done" From f3bf1a50507c03f46c18fd24684d5b9fb9395878 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 6 Nov 2023 17:43:18 -0300 Subject: [PATCH 139/224] Se quita codigo que no sirve de la carta seduccion con respecto al intercambio --- app/routers/games/games.py | 15 --------------- app/routers/games/services.py | 23 ----------------------- 2 files changed, 38 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index be079d8..bef92f1 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -324,21 +324,6 @@ async def card_one_two_effect(game_name: str, game_data: OneTwoEffectIn): return {"message": "One two effect terminated"} -@router.patch("/{game_name}/interchange-intention-response", status_code=status.HTTP_200_OK) -async def interchange_intention_response(game_name: str, game_data: InterchangeInformationIn): - utils.verify_player_in_game(game_data.player_id, game_name) - utils.verify_player_in_game(game_data.objective_player_id, game_name) - services.interchange_intention_response(game_data) - - json_msg = { - "event": utils.Events.INTERCHANGE_INTENTION_DONE - } - await player_connections.send_event_to(game_data.player_id, json_msg) - await player_connections.send_event_to(game_data.objective_player_id, json_msg) - - return {"message": "Interchange intention terminated"} - - @router.post("/{game_name}/play-defense-card") async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): utils.verify_defense_card_can_be_played(game_name, defense_info) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index e4cce52..8290f88 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -624,29 +624,6 @@ def card_one_two_effect(game_data: OneTwoEffectIn): objective_player.position = tempPosition -@db_session -def interchange_intention_response(game_data: InterchangeInformationIn): - player: Player = find_player_by_id(game_data.player_id) - objective_player: Player = find_player_by_id(game_data.objective_player_id) - player_card: Card = find_card_by_id(game_data.card_id) - objective_player_card: Card = find_card_by_id(game_data.objective_card_id) - - if game_data.accept_interchange: - player.hand.add(objective_player_card) - player.hand.remove(player_card) - objective_player.hand.add(player_card) - objective_player.hand.remove(objective_player_card) - else: - if player_card.name == CardDefenseName.SCARY or player_card.name == CardDefenseName.NO_THANKS: - player.hand.remove(player_card) - card_from_deck = get_stay_away_cards_from_decks(player.game, 1) - player.hand.add(card_from_deck[0]) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"The card is not a valid defense card.") - - @db_session def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): game: Game = find_game_by_name(game_name) From 8dafe9bbd9b126288f04822b697212f7b9a00ade Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 6 Nov 2023 18:11:22 -0300 Subject: [PATCH 140/224] Nuevo turno ahora se hace en la etapa del intercambio y se elimino codigo duplicado/inservible --- app/routers/cards/utils.py | 1 + app/routers/games/action_functions.py | 40 ------------------------- app/routers/games/games.py | 2 +- app/routers/games/schemas.py | 2 ++ app/routers/games/utils.py | 43 --------------------------- 5 files changed, 4 insertions(+), 84 deletions(-) diff --git a/app/routers/cards/utils.py b/app/routers/cards/utils.py index 5bc9db7..764fc04 100644 --- a/app/routers/cards/utils.py +++ b/app/routers/cards/utils.py @@ -48,6 +48,7 @@ def is_flamethrower(card_id: int) -> bool: card: Card = find_card_by_id(card_id) return (card.name == CardActionName.FLAMETHROWER) + @db_session def is_whiskey(card_id: int) -> bool: card: Card = find_card_by_id(card_id) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 9970980..530f1e0 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -26,20 +26,6 @@ async def send_players_eliminated_event(game: Game, eliminated_by: int, eliminat for p in game.players: await player_connections.send_event_to(p.id, json_msg) - # Espera 4 segundos antes de enviar el siguiente evento - await asyncio.sleep(2) - - with db_session: - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game.name, json_msg) - async def send_players_whiskey_event(game: Game, player_id: int, player_name: str): json_msg = { @@ -49,19 +35,6 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st } await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) - await asyncio.sleep(2) - - with db_session: - player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id - json_msg = { - "event": Events.NEW_TURN, - "next_player_name": get_player_name_by_id(player_id_turn), - "next_player_id": player_id_turn, - "round_direction": game.round_direction - } - await player_connections.send_event_to_all_players_in_game(game.name, json_msg) - async def send_seduction_done_event(player_id: int, objective_player_id: int): json_msg = { @@ -71,19 +44,6 @@ async def send_seduction_done_event(player_id: int, objective_player_id: int): await player_connections.send_event_to(objective_player_id, json_msg) -async def send_interchange_intention_event(player: Player, objective_player: Player, card: Card, card_to_exchange: Card): - json_msg = { - "event": Events.INTERCHANGE_INTENTION, - "player_id": player.id, - "player_name": player.name, - "card_played": card.id, - "card_name": card.name, - "card_to_exchange_id": card_to_exchange.id, - "card_to_exchange_name": card_to_exchange.name - } - await player_connections.send_event_to(objective_player.id, json_msg) - - @db_session def process_flamethrower_card(game: Game, player: Player, objective_player: Player): objective_player.rol = PlayerRol.ELIMINATED diff --git a/app/routers/games/games.py b/app/routers/games/games.py index bef92f1..a270a66 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -251,7 +251,7 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) - + # clean_intention_in_game(game_name) json_msg = { diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index a0ce144..0b49aee 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -154,12 +154,14 @@ class InterchangeInformationIn(BaseModel): objective_player_id: int # ID jugador que inicia la intencion objective_card_id: int # Card ID del jugador que inicia la intencion + class PlayDefenseInformation(BaseModel): model_config = ConfigDict(from_attributes=True) player_id: int card_id: Optional[int] + class ShowRevelationsCardsIn(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 67fd355..f183203 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -397,49 +397,6 @@ def verify_pass_card_can_be_done(game_name: str, play_info: PlayInformation): ) -@db_session -def verify_player_is_next_in_turn(game_name: str, player_id: int, other_player_id: int): - game = find_game_by_name(game_name) - player = find_player_by_id(player_id) - other_player = find_player_by_id(other_player_id) - - players_not_eliminated = select( - p for p in game.players if p.rol != PlayerRol.ELIMINATED).count() - - if game.round_direction == RoundDirection.CLOCKWISE: - next_turn = (player.position - 1) % players_not_eliminated - else: - next_turn = (player.position + 1) % players_not_eliminated - - if next_turn != other_player.position: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="The player selected is not the next in turn" - ) - - -@db_session -def verify_pass_card_can_be_done(game_name: str, play_info: PlayInformation): - player: Player = find_player_by_id(play_info.player_id) - card: Card = find_card_by_id(play_info.card_id) - verify_player_in_game(play_info.player_id, game_name) - verify_player_in_game(play_info.objective_player_id, game_name) - verify_player_is_next_in_turn( - play_info.player_id, play_info.objective_player_id) - if card.type == CardType.THE_THING: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="The Thing cannot be passed" - ) - is_card_in_player = select( - c for c in player.hand if (c.id == card.id)).exists() - if not is_card_in_player: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="The card is not in the hand of the player" - ) - - @db_session def verify_if_interchange_can_be_done(game_name: str, interchange_info: IntentionExchangeInformationIn): game: Game = find_game_by_name(game_name) From d2381532ee60b9f60bc118916b49252aa6f3c5be Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 6 Nov 2023 18:29:26 -0300 Subject: [PATCH 141/224] Formato con autopep8 --- app/routers/games/action_functions.py | 1 + app/routers/games/games.py | 1 - app/routers/games/schemas.py | 1 + app/routers/games/services.py | 1 + app/routers/websockets/services.py | 3 +-- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index d9c7264..f2b9cc2 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -44,6 +44,7 @@ async def send_resolute_card_played_event(game: Game, player_id: int, option_car } await player_connections.send_event_to(player_id, json_msg) + async def send_seduction_done_event(player_id: int, objective_player_id: int): json_msg = { "event": Events.SEDUCTION_DONE diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4d9cf98..f4ff5f1 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -324,7 +324,6 @@ async def card_one_two_effect(game_name: str, game_data: OneTwoEffectIn): return {"message": "One two effect terminated"} - @router.patch("/{game_name}/resolute-exchange", status_code=status.HTTP_200_OK) async def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): utils.verify_player_in_game(game_data.player_id, game_name) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index d3587cd..9e474a8 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -162,6 +162,7 @@ class ResoluteExchangeIn(BaseModel): card_in_hand: int card_in_deck: int + class PlayDefenseInformation(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index a92b958..5aaa619 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -643,6 +643,7 @@ def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): elif deck_card in game.discard_deck: game.discard_deck.remove(deck_card) + @db_session def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): game: Game = find_game_by_name(game_name) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index e87a89c..edf9ad1 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -16,8 +16,7 @@ async def send_list_of_cheats(player_id: int): '[lz | lanzallamas | flamethrower]: Obtienes una carta lanzallamas', '[ws | whiskey | whisky]: Obtienes una carta whiskey', '[ups | ooops]: Obtienes una carta ups!', - '[det | determinación | resolute]: Obtienes una carta determinación' - , + '[det | determinación | resolute]: Obtienes una carta determinación', '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo', '[uno dos | one two]: Obtienes una carta uno, dos...', '[sed | seducción | seduction]: Obtienes una carta de seducción' From f4715bd3a2e717b9345fd36a627204ca5078110c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 6 Nov 2023 20:17:47 -0300 Subject: [PATCH 142/224] Hotfix - Se agrega player_id en el evento de discard_card por pedido del front --- app/routers/games/games.py | 5 +++-- app/routers/games/utils.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index f4ff5f1..8d3241c 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -5,7 +5,7 @@ from . import utils from .schemas import * from ..websockets.utils import player_connections -from .utils import find_game_by_name, is_the_game_finished +from .utils import find_game_by_name, is_the_game_finished, Events from ..players.utils import get_player_name_by_id, find_player_by_id from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower, is_whiskey from .services import finish_game @@ -151,8 +151,9 @@ async def discard_card(game_name: str, game_data: DiscardInformationIn): p for p in game.players if p.position == game.turn).first().id json_msg = { - "event": "discard_card", + "event": Events.DISCARD_CARD, "player_name": get_player_name_by_id(game_data.player_id), + "player_id": game_data.player_id, "card_type": get_card_type_by_id(game_data.card_id) } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index f65a602..5f3b7da 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -24,6 +24,7 @@ class Events(str, Enum): PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' PLAYED_CARD = 'played_card' + DISCARD_CARD = 'discard_card' PLAYER_ELIMINATED = 'player_eliminated' WHISKEY_CARD_PLAYED = 'whiskey_card_played' RESOLUTE_CARD_PLAYED = 'resolute_card_played' From 57e06bbc5b20e87dd2fee3ac2c4ca764fcdb2257 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Mon, 6 Nov 2023 21:07:34 -0300 Subject: [PATCH 143/224] Hotfix - Se manda nombre de la carta en evento de played_card, y se arregla problema con funciones de action_functions --- app/routers/games/action_functions.py | 7 ++----- app/routers/games/games.py | 18 +++++------------- app/routers/games/services.py | 15 +++++++++------ app/routers/games/utils.py | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index f2b9cc2..ab95db7 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -113,10 +113,7 @@ def process_whiskey_card(game: Game, player: Player): @db_session -def process_resolute_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) - +def process_resolute_card(game: Game, player: Player): random_draw_deck_cards = select( c for c in game.draw_deck if 2 <= c.id and c.id <= 88).random(3) random_discard_deck_cards = select( @@ -132,7 +129,7 @@ def process_resolute_card(game: Game, player: Player, card: Card): @db_session -def process_watch_your_back_card(game: Game, player: Player): +def process_watch_your_back_card(game: Game): if game.round_direction == RoundDirection.CLOCKWISE: game.round_direction = RoundDirection.COUNTERCLOCKWISE else: diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 8d3241c..2e58367 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -5,7 +5,7 @@ from . import utils from .schemas import * from ..websockets.utils import player_connections -from .utils import find_game_by_name, is_the_game_finished, Events +from .utils import find_game_by_name, is_the_game_finished, Events, send_played_card_event from ..players.utils import get_player_name_by_id, find_player_by_id from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower, is_whiskey from .services import finish_game @@ -167,12 +167,8 @@ async def play_action_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - json_msg = { - "event": utils.Events.PLAYED_CARD, - "player_name": get_player_name_by_id(play_info.player_id), - "card_id": play_info.card_id - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + send_played_card_event( + game_name, play_info.player_id, play_info.card_id) return result @@ -183,12 +179,8 @@ async def play_panic_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - json_msg = { - "event": utils.Events.PLAYED_CARD, - "player_name": get_player_name_by_id(play_info.player_id), - "card_name": get_card_name_by_id(play_info.card_id) - } - await player_connections.send_event_to_other_players_in_game(game_name, json_msg, play_info.player_id) + send_played_card_event( + game_name, play_info.player_id, play_info.card_id) return result diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 5aaa619..b6b7667 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -291,7 +291,7 @@ def play_action_card(game_name: str, play_info: PlayInformation): play_info.objective_player_id) # Armo listado de cartas del jugador objetivo para enviar en el body response - result = process_analysis_card(game, player, card, objective_player) + result = process_analysis_card(game, player, objective_player) # Hacha if card.name == CardActionName.AXE: @@ -305,19 +305,19 @@ def play_action_card(game_name: str, play_info: PlayInformation): players_not_eliminated - 1) objective_player: Player = find_player_by_id( play_info.objective_player_id) - result = process_suspicious_card(game, player, card, objective_player) + result = process_suspicious_card(game, player, objective_player) # Whisky if card.name == CardActionName.WHISKEY: - process_whiskey_card(game, player, card) + process_whiskey_card(game, player) # Determinacion if card.name == CardActionName.RESOLUTE: - process_resolute_card(game, player, card) + process_resolute_card(game, player) # Vigila tus espaldas if card.name == CardActionName.WATCH_YOUR_BACK: - process_watch_your_back_card(game, player, card) + process_watch_your_back_card(game) # Cambio de lugar if card.name == CardActionName.CHANGE_PLACES: @@ -353,7 +353,10 @@ def play_action_card(game_name: str, play_info: PlayInformation): ) verify_card_in_hand(player, card_to_exchange) process_seduction_card( - game, player, card, objective_player, card_to_exchange) + game, player, objective_player, card_to_exchange) + + player.hand.remove(card) + game.discard_deck.add(card) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 5f3b7da..ffb429f 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -6,8 +6,8 @@ from .schemas import * from ..websockets.utils import player_connections, get_players_id from ..players.schemas import PlayerRol -from ..players.utils import find_player_by_id -from ..cards.utils import find_card_by_id +from ..players.utils import find_player_by_id, get_player_name_by_id +from ..cards.utils import find_card_by_id, get_card_name_by_id from ..cards.schemas import CardType, CardSubtype from ..cards.services import find_card_by_id from ..games.defense_functions import player_cards_to_defend_himself @@ -51,6 +51,16 @@ class Events(str, Enum): INTERCHANGE_INTENTION_DONE = 'interchange_intention_done' +async def send_played_card_event(game_name: str, player_id: int, card_id: int): + json_msg = { + "event": Events.PLAYED_CARD, + "player_name": get_player_name_by_id(player_id), + "card_id": card_id, + "card_name": get_card_name_by_id(card_id) + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + @db_session def find_game_by_name(game_name: str): game = Game.get(name=game_name) From 7701ea1b08572462e558d3176bd5386979a35402 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 6 Nov 2023 23:33:06 -0300 Subject: [PATCH 144/224] fix schema play_defense_card --- app/routers/games/schemas.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 9e474a8..267b499 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -167,7 +167,8 @@ class PlayDefenseInformation(BaseModel): model_config = ConfigDict(from_attributes=True) player_id: int - card_id: Optional[int] + card_id: Optional[int] = Field( + None, description="Optional defense card.") class ShowRevelationsCardsIn(BaseModel): From d10c0645f76f97522e9c5d79cfd6b68736a4f26e Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 02:12:55 -0300 Subject: [PATCH 145/224] Se agregan tests para el modulo de games, se modifica el archivo .coveragerc para omitir los archivos que no llevan tests, se corrige un pequeno bug en una funcion async --- .coveragerc | 3 + app/routers/games/games.py | 4 +- app/tests/game_tests/test_game_discard.py | 33 ++ app/tests/game_tests/test_play_action_card.py | 411 ++++++++++++++++++ 4 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 app/tests/game_tests/test_play_action_card.py diff --git a/.coveragerc b/.coveragerc index 1e7dd97..ff3f944 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = app/routers/games/__init__.py app/routers/players/__init__.py app/tests/* + app/routers/websockets/* + app/utils.py + app/database/utils.py [html] directory = htmlcov diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 2e58367..e2b0a7c 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -167,7 +167,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - send_played_card_event( + await send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result @@ -179,7 +179,7 @@ async def play_panic_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - send_played_card_event( + await send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result diff --git a/app/tests/game_tests/test_game_discard.py b/app/tests/game_tests/test_game_discard.py index 284dbce..c3dbea5 100644 --- a/app/tests/game_tests/test_game_discard.py +++ b/app/tests/game_tests/test_game_discard.py @@ -29,6 +29,39 @@ def cleanup_database(): Player.select().delete() +def test_discard_card_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + break + player_hand = client.get(f"/players/{player_id_turn}/hand").json() + for c in player_hand: + if c['type'] != "THE_THING": + game_data = { + "player_id": player_id_turn, + "card_id": c['id'] + } + response = client.patch("/games/TestGame/discard", json=game_data) + break + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + cleanup_database() + + def test_discard_card_for_inexistent_game(): cleanup_database() players = [] diff --git a/app/tests/game_tests/test_play_action_card.py b/app/tests/game_tests/test_play_action_card.py new file mode 100644 index 0000000..08367cb --- /dev/null +++ b/app/tests/game_tests/test_play_action_card.py @@ -0,0 +1,411 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player, Card, Intention +from pony.orm import db_session, select +from app.routers.games import utils as games_utils +from app.routers.players.utils import find_player_by_id +from app.routers.cards.schemas import CardType +import random + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Intention.select().delete() + Game.select().delete() + Player.select().delete() + + +@db_session +def replace_card_in_hand(game_name: str, player_id: int, card_id: int): + games_utils.verify_player_in_game(player_id, game_name) + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + + card = select(c for c in Card if c.id == card_id).first() + + if card: + player.hand.remove(random_card) + player.hand.add(card) + + +def test_play_flamethrower_card(): + cleanup_database() + card_to_play_id = 22 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_analysis_card(): + cleanup_database() + card_to_play_id = 27 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_suspicious_card(): + cleanup_database() + card_to_play_id = 32 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_whiskey_card(): + cleanup_database() + card_to_play_id = 40 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_resolute_card(): + cleanup_database() + card_to_play_id = 43 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_watch_your_back_card(): + cleanup_database() + card_to_play_id = 53 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_change_places_card(): + cleanup_database() + card_to_play_id = 48 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_better_run_card(): + cleanup_database() + card_to_play_id = 65 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_seduction_card(): + cleanup_database() + card_to_play_id = 55 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + player_hand = client.get(f"/players/{player_id_turn}/hand").json() + + # Busco una carta en la mano del jugador que no sea ni la cosa ni la carta de seduccion + for c in player_hand: + if c['type'] != CardType.THE_THING and c['id'] != card_to_play_id: + card_to_exchange_id = c['id'] + break + + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id, + "card_to_exchange": card_to_exchange_id + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_try_to_interchange_the_thing_on_seduction_card(): + cleanup_database() + card_to_play_id = 55 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=5, max_players=5, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id, + "card_to_exchange": 1 + } + response = client.post("/games/TestGame/play-action-card", json=game_data) + assert response.status_code == 400, "El codigo de estado de la respuesta no es 400 (Bad Request)" + + cleanup_database() From c758be97acf5246d831ea9242821cb367759497c Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 02:58:35 -0300 Subject: [PATCH 146/224] Se agregaron tests para el modulo games, y se corrigieron bugs menores en el codigo --- app/routers/cards/schemas.py | 2 +- app/routers/games/panic_functions.py | 37 +- app/routers/games/services.py | 19 +- app/routers/players/utils.py | 2 +- app/tests/game_tests/test_play_panic_card.py | 376 +++++++++++++++++++ 5 files changed, 397 insertions(+), 39 deletions(-) create mode 100644 app/tests/game_tests/test_play_panic_card.py diff --git a/app/routers/cards/schemas.py b/app/routers/cards/schemas.py index 801f7d6..ed1d8c4 100644 --- a/app/routers/cards/schemas.py +++ b/app/routers/cards/schemas.py @@ -31,7 +31,7 @@ class CardActionName(str, Enum): class CardPanicName(str, Enum): - JUST_BETWEEN_US = 'Que quede entre nosotros...' + JUST_BETWEEN_US = 'Que quede entre nosotros…' REVELATIONS = 'Revelaciones' ROTTEN_ROPES = 'Cuerdas podridas' ONE_TWO = 'Uno, dos...' diff --git a/app/routers/games/panic_functions.py b/app/routers/games/panic_functions.py index 90cddf3..2518d7b 100644 --- a/app/routers/games/panic_functions.py +++ b/app/routers/games/panic_functions.py @@ -69,19 +69,14 @@ async def send_blind_date_selection_event(player_id: int): @db_session -def process_revelations_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) +def process_revelations_card(game: Game, player: Player): next_player_id = get_id_of_next_player_in_turn(game.name) asyncio.ensure_future(send_revelations_card_played_event( game.name, player.id, next_player_id)) @db_session -def process_one_two_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) - +def process_one_two_card(game: Game, player: Player): players_in_game = len( select(p for p in game.players if p.rol != PlayerRol.ELIMINATED)[:]) players_list: list[int] = [] @@ -103,50 +98,34 @@ def process_one_two_card(game: Game, player: Player, card: Card): @db_session -def process_ooops_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) +def process_ooops_card(game: Game, player: Player): asyncio.ensure_future(send_ooops_card_played_event( game.name, player.name, player.id)) @db_session -def process_between_us_card(game: Game, player: Player, card: Card, objective_player: Player): - game.discard_deck.add(card) - player.hand.remove(card) - +def process_between_us_card(game: Game, player: Player, objective_player: Player): asyncio.ensure_future(send_player_between_us_event( objective_player.id, player.id, player.name)) @db_session -def process_forgetful_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) +def process_forgetful_card(game: Game, player: Player): asyncio.ensure_future(send_forgetful_card_played(player.id, player.name)) @db_session -def process_round_and_round_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) - +def process_round_and_round_card(game: Game, player: Player): asyncio.ensure_future(send_round_and_round_start_event(game.name)) @db_session -def process_getout_of_here_card(game: Game, player: Player, card: Card, objective_player: Player): +def process_getout_of_here_card(game: Game, player: Player, objective_player: Player): tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition - game.discard_deck.add(card) - player.hand.remove(card) - @db_session -def process_blind_date_card(game: Game, player: Player, card: Card): - game.discard_deck.add(card) - player.hand.remove(card) - +def process_blind_date_card(game: Game, player: Player): asyncio.ensure_future(send_blind_date_selection_event(player.id)) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index b6b7667..f646270 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -458,11 +458,11 @@ def play_panic_card(game_name: str, play_info: PlayInformation): verify_adjacent_players(play_info.player_id, play_info.objective_player_id, players_not_eliminated - 1) - process_between_us_card(game, player, card) + process_between_us_card(game, player, objective_player) # Revelaciones if card.name == CardPanicName.REVELATIONS: - process_revelations_card(game, player, card) + process_revelations_card(game, player) # Cuerdas podridas if card.name == CardPanicName.ROTTEN_ROPES: @@ -470,7 +470,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Uno, dos... if card.name == CardPanicName.ONE_TWO: - process_one_two_card(game, player, card) + process_one_two_card(game, player) # Tres, cuatro if card.name == CardPanicName.THREE_FOUR: @@ -482,15 +482,15 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Ups if card.name == CardPanicName.OOOPS: - process_ooops_card(game, player, card) + process_ooops_card(game, player) # Olvidadizo if card.name == CardPanicName.FORGETFUL: - process_forgetful_card(game, player, card) + process_forgetful_card(game, player) # Vuelta y vuelta if card.name == CardPanicName.ROUND_AND_ROUND: - process_round_and_round_card(game, player, card) + process_round_and_round_card(game, player) # ¿No podemos ser amigos? if card.name == CardPanicName.CANT_WE_BE_FRIENDS: @@ -498,7 +498,7 @@ def play_panic_card(game_name: str, play_info: PlayInformation): # Cita a ciegas if card.name == CardPanicName.BLIND_DATE: - process_blind_date_card(game, player, card) + process_blind_date_card(game, player) # ¡Sal de aquí! if card.name == CardPanicName.GETOUT_OF_HERE: @@ -506,7 +506,10 @@ def play_panic_card(game_name: str, play_info: PlayInformation): objective_player: Player = find_player_by_id( play_info.objective_player_id) verify_player_not_in_quarentine(objective_player) - process_getout_of_here_card(game, player, card, objective_player) + process_getout_of_here_card(game, player, objective_player) + + game.discard_deck.add(card) + player.hand.remove(card) return result diff --git a/app/routers/players/utils.py b/app/routers/players/utils.py index 950eea6..e587787 100644 --- a/app/routers/players/utils.py +++ b/app/routers/players/utils.py @@ -33,7 +33,7 @@ def get_player_name_by_id(player_id: int) -> str: @db_session def verify_player_not_in_quarentine(player: Player): - if player.isQuatentined: + if player.isQuarentined: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="The player is in quarentined" diff --git a/app/tests/game_tests/test_play_panic_card.py b/app/tests/game_tests/test_play_panic_card.py new file mode 100644 index 0000000..fd47056 --- /dev/null +++ b/app/tests/game_tests/test_play_panic_card.py @@ -0,0 +1,376 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player, Card, Intention +from pony.orm import db_session, select +from app.routers.games import utils as games_utils +from app.routers.players.utils import find_player_by_id +from app.routers.cards.schemas import CardType +import random + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Intention.select().delete() + Game.select().delete() + Player.select().delete() + + +@db_session +def replace_card_in_hand(game_name: str, player_id: int, card_id: int): + games_utils.verify_player_in_game(player_id, game_name) + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + + card = select(c for c in Card if c.id == card_id).first() + + if card: + player.hand.remove(random_card) + player.hand.add(card) + + +def test_play_just_between_us_card(): + cleanup_database() + card_to_play_id = 100 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=7, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_just_between_us_card(): + cleanup_database() + card_to_play_id = 100 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", + "Amparo", "Santiago", "Nehuen", "Gabriel"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=7, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_revelations_card(): + cleanup_database() + card_to_play_id = 89 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", + "Santiago", "Nehuen", "Gabriel", "Ignacio2"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=7, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_one_two_card(): + cleanup_database() + card_to_play_id = 94 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", + "Santiago", "Nehuen", "Gabriel", "Ignacio2"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=7, max_players=7, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_ooops_card(): + cleanup_database() + card_to_play_id = 108 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago", + "Nehuen", "Gabriel", "Ignacio2", "Ignacio3", "Ignacio4"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=10, max_players=10, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_forgetful_card(): + cleanup_database() + card_to_play_id = 93 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago", + "Nehuen", "Gabriel", "Ignacio2", "Ignacio3", "Ignacio4"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=10, max_players=10, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_round_and_round_card(): + cleanup_database() + card_to_play_id = 102 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago", + "Nehuen", "Gabriel", "Ignacio2", "Ignacio3", "Ignacio4"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=10, max_players=10, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_blind_date_card(): + cleanup_database() + card_to_play_id = 106 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago", + "Nehuen", "Gabriel", "Ignacio2", "Ignacio3", "Ignacio4"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=10, max_players=10, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + player_hand = client.get(f"/players/{player_id_turn}/hand").json() + for c in player_hand: + if c["type"] != CardType.THE_THING and c["id"] != card_to_play_id: + card_to_exchange_id = c["id"] + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "card_to_exchange_id": card_to_exchange_id + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() + + +def test_play_getout_of_here_card(): + cleanup_database() + card_to_play_id = 92 + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo", "Santiago", + "Nehuen", "Gabriel", "Ignacio2", "Ignacio3", "Ignacio4"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=10, max_players=10, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + if p['position'] == 1: + next_player_id = p['id'] + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn, + "objective_player_id": next_player_id + } + response = client.post("/games/TestGame/play-panic-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() From 2dbfac3dd1fa902d8047ef36493c95f0afac0a2d Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 12:00:10 -0300 Subject: [PATCH 147/224] Se agregan test y se omiten archivos para el coverage --- .coveragerc | 2 + app/tests/game_tests/test_draw_card.py | 81 +++++++++++++++++++++ app/tests/game_tests/test_finish_game.py | 91 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 app/tests/game_tests/test_draw_card.py create mode 100644 app/tests/game_tests/test_finish_game.py diff --git a/.coveragerc b/.coveragerc index ff3f944..98e6a4c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,8 @@ omit = app/routers/websockets/* app/utils.py app/database/utils.py + app/database/initialize_data.py + app/routers/games/intention.py [html] directory = htmlcov diff --git a/app/tests/game_tests/test_draw_card.py b/app/tests/game_tests/test_draw_card.py new file mode 100644 index 0000000..6259bd5 --- /dev/null +++ b/app/tests/game_tests/test_draw_card.py @@ -0,0 +1,81 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player, Card, Intention +from pony.orm import db_session, select +from app.routers.games import utils as games_utils +from app.routers.players.utils import find_player_by_id +from app.routers.cards.schemas import CardType +import random + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Intention.select().delete() + Game.select().delete() + Player.select().delete() + + +@db_session +def replace_card_in_hand(game_name: str, player_id: int, card_id: int): + games_utils.verify_player_in_game(player_id, game_name) + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + + card = select(c for c in Card if c.id == card_id).first() + + if card: + player.hand.remove(random_card) + player.hand.add(card) + + +def test_draw_card_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + game_data = { + "player_id": player_id_turn + } + response = client.patch("/games/TestGame/draw-card", json=game_data) + assert response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + + cleanup_database() diff --git a/app/tests/game_tests/test_finish_game.py b/app/tests/game_tests/test_finish_game.py new file mode 100644 index 0000000..db32274 --- /dev/null +++ b/app/tests/game_tests/test_finish_game.py @@ -0,0 +1,91 @@ +from fastapi.testclient import TestClient +from app.main import app +from app.database.models import Game, Player, Card, Intention +from pony.orm import db_session, select +from app.routers.games import utils as games_utils +from app.routers.games.services import finish_game +from app.routers.players.utils import find_player_by_id +from app.routers.players.schemas import PlayerRol +from app.routers.cards.schemas import CardType +import random + +client = TestClient(app) + + +@db_session +def create_test_player(name: str): + return Player(name=name) + + +@db_session +def create_test_game(name, min_players, max_players, password, host_player_id): + game_data = { + "name": name, + "min_players": min_players, + "max_players": max_players, + "password": password, + "host_player_id": host_player_id + } + return client.post("/games", json=game_data) + + +@db_session +def cleanup_database(): + Intention.select().delete() + Game.select().delete() + Player.select().delete() + + +@db_session +def replace_card_in_hand(game_name: str, player_id: int, card_id: int): + games_utils.verify_player_in_game(player_id, game_name) + game: Game = games_utils.find_game_by_name(game_name) + player: Player = find_player_by_id(player_id) + + player_hand = list(player.hand) + elegible_cards = [c for c in player_hand if c.type != CardType.THE_THING] + if elegible_cards: + random_card = random.choice(elegible_cards) + + card = select(c for c in Card if c.id == card_id).first() + + if card: + player.hand.remove(random_card) + player.hand.add(card) + + +def test_finish_game_successfully(): + cleanup_database() + players = [] + names = ["Ignacio", "Anelio", "Ezequiel", "Amparo"] + for name in names: + players.append(create_test_player(name=name)) + game = create_test_game(name="TestGame", min_players=4, max_players=4, + password="secret", host_player_id=players[0].id) + for player in players[1:]: + game_data = { + "player_id": player.id, + "password": "secret" + } + client.patch("/games/join/TestGame", json=game_data) + init_response = client.patch( + f"/games/TestGame/init?host_player_id={players[0].id}").json() + with db_session: + game = select(g for g in Game if g.name == "TestGame").first() + for p in game.players: + if p.rol == PlayerRol.THE_THING: + p.rol = PlayerRol.ELIMINATED + + for p in init_response['list_of_players']: + if p['position'] == 0: + player_id_turn = p['id'] + card_to_play_id = 40 + replace_card_in_hand(game_name='TestGame', + player_id=player_id_turn, card_id=card_to_play_id) + game_data = { + "card_id": card_to_play_id, + "player_id": player_id_turn + } + response = client.post(f"/games/TestGame/play-action-card", json=game_data) + response.status_code == 200, "El codigo de estado de la respuesta no es 200 (OK)" + cleanup_database() From 3d8732a7174f7ffa9fbf3c4775dedc83007d7e21 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 7 Nov 2023 12:35:25 -0300 Subject: [PATCH 148/224] Finalizado de partida actualizado para que la cosa declare la victoria --- app/routers/games/games.py | 13 +++++++++++++ app/routers/games/services.py | 30 ++++++++++++++++++++++++------ app/routers/games/utils.py | 26 ++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index e2b0a7c..717f177 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -346,3 +346,16 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation process_intention_in_game(game_name) clean_intention_in_game(game_name) + + +@router.post("/{game_name}/the-thing-end-game") +async def the_thing_end_game(game_name: str, player_id: int): + utils.verify_player_is_the_thing(player_id, game_name) + services.finish_game_by_the_thing(game_name) + + json_msg = { + "event": utils.Events.GAME_ENDED_BY_THE_THING + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + return {"message": "The thing ended the game"} diff --git a/app/routers/games/services.py b/app/routers/games/services.py index f646270..8df612e 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -253,6 +253,14 @@ async def finish_game(name: str) -> Game: return game +@db_session +async def finish_game_by_the_thing(name: str) -> Game: + game: Game = find_game_by_name(name) + game.status = GameStatus.ENDED + + return game + + @db_session def play_action_card(game_name: str, play_info: PlayInformation): result = {"message": "Action card played"} @@ -415,20 +423,30 @@ def get_game_result(name: str) -> GameResult: losers = game.players.select( lambda p: p.rol in [PlayerRol.INFECTED, PlayerRol.ELIMINATED])[:] + elif the_thing_infected_everyone(game): + reason = '''La Cosa ha logrado infectar a todos los demás jugadores + sin que haya sido eliminado ningún Humano de la partida.''' + winners = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING)[:] + losers = game.players.select( + lambda p: p.rol != PlayerRol.THE_THING)[:] + elif no_human_remains(game): reason = "No queda ningún Humano en la partida." winners = game.players.select( lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] losers = game.players.select( lambda p: p.rol == PlayerRol.ELIMINATED)[:] - - elif the_thing_infected_everyone(game): - reason = '''La Cosa ha logrado infectar a todos los demás jugadores - sin que haya sido eliminado ningún Humano de la partida.''' + + + + #elif the_thing_declared_a_wrong_victory(game): + else: + reason = '''La Cosa ha declarado una victoria equivocada. Todavia queda algún humano vivo.''' winners = game.players.select( - lambda p: p.rol == PlayerRol.THE_THING)[:] + lambda p: p.rol in [PlayerRol.HUMAN, PlayerRol.INFECTED])[:] losers = game.players.select( - lambda p: p.rol != PlayerRol.THE_THING)[:] + lambda p: p.rol != PlayerRol.HUMAN)[:] return GameResult( reason=reason, diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index ffb429f..bd69d36 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -20,6 +20,7 @@ class Events(str, Enum): GAME_STARTED = 'game_started' GAME_CANCELED = 'game_canceled' GAME_ENDED = 'game_ended' + GAME_ENDED_BY_THE_THING = 'game_ended_by_the_thing' PLAYER_JOINED = 'player_joined' PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' @@ -141,10 +142,21 @@ def verify_game_can_be_abandon(game_name: str, player_id: int): ) +@db_session +def verify_player_is_the_thing(player_id, game_name): + verify_player_in_game(player_id, game_name) + player: Player = find_player_by_id(player_id) + if player.rol != PlayerRol.THE_THING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"The player {player.name}, with id= {player.id} is not The Thing" + ) + + @db_session def verify_game_can_be_finished(game: Game): - if not (no_human_remains(game) or the_thing_is_eliminated(game)): - raise Exception('There are living Humans and The Thing') + if not (the_thing_is_eliminated(game) or all_humans_were_eliminated(game)): + raise Exception('The Thing is alive, cannot auto-finish the game') @db_session @@ -166,6 +178,16 @@ def no_human_remains(game: Game) -> bool: return the_thing_exists and number_of_humans == 0 +def all_humans_were_eliminated(game: Game) -> bool: + the_thing_exists = game.players.select( + lambda p: p.rol == PlayerRol.THE_THING).exists() + + number_of_eliminated_players = game.players.select( + lambda p: p.rol == PlayerRol.ELIMINATED).count() + + return the_thing_exists and number_of_eliminated_players == game.players.count() - 1 + + @db_session def the_thing_infected_everyone(game: Game) -> bool: the_thing_exists = game.players.select( From e17951209a6c76dc95d9c74c951b650962192fb8 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 7 Nov 2023 12:53:15 -0300 Subject: [PATCH 149/224] hotfix --- app/routers/games/games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 717f177..62825bf 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -348,7 +348,7 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation clean_intention_in_game(game_name) -@router.post("/{game_name}/the-thing-end-game") +@router.patch("/{game_name}/the-thing-end-game") async def the_thing_end_game(game_name: str, player_id: int): utils.verify_player_is_the_thing(player_id, game_name) services.finish_game_by_the_thing(game_name) From e2615ba7061b277f681beb7e1c67103a0d13e897 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 15:31:34 -0300 Subject: [PATCH 150/224] Hotfix - Se mandan los nombres de los jugadores que interacturan en el intercambio de cartas por pedido del front --- app/routers/games/games.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index e2b0a7c..6d0b9a7 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -248,10 +248,11 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform # clean_intention_in_game(game_name) json_msg = { - "event": utils.Events.EXCHANGE_DONE + "event": utils.Events.EXCHANGE_DONE, + "player_name": get_player_name_by_id(game_data.player_id), + "objective_player_name": get_player_name_by_id(game_data.objective_player_id) } - await player_connections.send_event_to(game_data.player_id, json_msg) - await player_connections.send_event_to(game_data.objective_player_id, json_msg) + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) with db_session: game = find_game_by_name(game_name) From fc54386acae01e262dc2240ec8a52063a6d39210 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 17:58:26 -0300 Subject: [PATCH 151/224] Hotfix - Se agrega player_id en el evento played_card por pedido del front --- app/routers/games/games.py | 4 ++-- app/routers/games/utils.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6d0b9a7..3a9b417 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -167,7 +167,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - await send_played_card_event( + send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result @@ -179,7 +179,7 @@ async def play_panic_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - await send_played_card_event( + send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index ffb429f..d9ad64a 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -55,6 +55,7 @@ async def send_played_card_event(game_name: str, player_id: int, card_id: int): json_msg = { "event": Events.PLAYED_CARD, "player_name": get_player_name_by_id(player_id), + "player_id": player_id, "card_id": card_id, "card_name": get_card_name_by_id(card_id) } From 5dac89a868decf8cad7815a519388cefb31b52a5 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Tue, 7 Nov 2023 18:16:22 -0300 Subject: [PATCH 152/224] Hotfix - Se agrega await en las funciones send_played_card_event + cheat para la carta vigila tus espaldas --- app/routers/games/games.py | 4 ++-- app/routers/websockets/services.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 3a9b417..6d0b9a7 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -167,7 +167,7 @@ async def play_action_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - send_played_card_event( + await send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result @@ -179,7 +179,7 @@ async def play_panic_card(game_name: str, play_info: PlayInformation): if is_the_game_finished(game_name): await finish_game(game_name) else: - send_played_card_event( + await send_played_card_event( game_name, play_info.player_id, play_info.card_id) return result diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index edf9ad1..ba0ceba 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -18,8 +18,9 @@ async def send_list_of_cheats(player_id: int): '[ups | ooops]: Obtienes una carta ups!', '[det | determinación | resolute]: Obtienes una carta determinación', '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo', - '[uno dos | one two]: Obtienes una carta uno, dos...', + '[uno_dos | one_two]: Obtienes una carta uno, dos...', '[sed | seducción | seduction]: Obtienes una carta de seducción' + '[vig | vigila_tus_espaldas | watch_your_back]: Obtienes una carta vigila tus espaldas' ] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) @@ -55,7 +56,7 @@ async def handle_message(data, player_id): apply_cheat(game_name, player_id, range(93, 94)) await send_event_cheat_used(player_id) - elif message == 'uno dos' or message == 'one two': + elif message == 'uno_dos' or message == 'one_two': apply_cheat(game_name, player_id, range(94, 96)) await send_event_cheat_used(player_id) @@ -66,6 +67,10 @@ async def handle_message(data, player_id): elif message == 'det' or message == 'determinación' or message == 'resolute': apply_cheat(game_name, player_id, range(43, 48)) await send_event_cheat_used(player_id) + + elif message == 'vig' or message == 'vigila_tus_espaldas' or message == 'watch_your_back': + apply_cheat(game_name, player_id, range(53, 55)) + await send_event_cheat_used(player_id) async def websocket_games(player_id: int, websocket: WebSocket): From 2a7f7c499aafc13cdc0592a739a5616017bf95d1 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 7 Nov 2023 19:48:33 -0300 Subject: [PATCH 153/224] =?UTF-8?q?actualizaci=C3=B3n=20sobre=20los=20even?= =?UTF-8?q?tos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/games.py | 2 +- app/routers/games/utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 62825bf..2c90703 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -354,7 +354,7 @@ async def the_thing_end_game(game_name: str, player_id: int): services.finish_game_by_the_thing(game_name) json_msg = { - "event": utils.Events.GAME_ENDED_BY_THE_THING + "event": utils.Events.GAME_ENDED } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index bd69d36..af55d02 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -20,7 +20,6 @@ class Events(str, Enum): GAME_STARTED = 'game_started' GAME_CANCELED = 'game_canceled' GAME_ENDED = 'game_ended' - GAME_ENDED_BY_THE_THING = 'game_ended_by_the_thing' PLAYER_JOINED = 'player_joined' PLAYER_LEFT = 'player_left' PLAYER_INIT_HAND = 'player_init_hand' From 8b0fa1d093377ce5afa9b08b8bd0047c9aa851ef Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 7 Nov 2023 20:02:22 -0300 Subject: [PATCH 154/224] autopep8 --- app/routers/games/games.py | 2 +- app/routers/games/services.py | 10 ++++------ app/routers/games/utils.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 2c90703..0967215 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -352,7 +352,7 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation async def the_thing_end_game(game_name: str, player_id: int): utils.verify_player_is_the_thing(player_id, game_name) services.finish_game_by_the_thing(game_name) - + json_msg = { "event": utils.Events.GAME_ENDED } diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8df612e..8cd8d49 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -430,21 +430,19 @@ def get_game_result(name: str) -> GameResult: lambda p: p.rol == PlayerRol.THE_THING)[:] losers = game.players.select( lambda p: p.rol != PlayerRol.THE_THING)[:] - + elif no_human_remains(game): reason = "No queda ningún Humano en la partida." winners = game.players.select( lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] losers = game.players.select( lambda p: p.rol == PlayerRol.ELIMINATED)[:] - - - - #elif the_thing_declared_a_wrong_victory(game): + + # elif the_thing_declared_a_wrong_victory(game): else: reason = '''La Cosa ha declarado una victoria equivocada. Todavia queda algún humano vivo.''' winners = game.players.select( - lambda p: p.rol in [PlayerRol.HUMAN, PlayerRol.INFECTED])[:] + lambda p: p.rol == PlayerRol.HUMAN)[:] losers = game.players.select( lambda p: p.rol != PlayerRol.HUMAN)[:] diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index af55d02..58a626d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -150,7 +150,7 @@ def verify_player_is_the_thing(player_id, game_name): status_code=status.HTTP_400_BAD_REQUEST, detail=f"The player {player.name}, with id= {player.id} is not The Thing" ) - + @db_session def verify_game_can_be_finished(game: Game): From e3cf59c3b1215d34d8bd414af22d485b9c917caf Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Tue, 7 Nov 2023 23:43:58 -0300 Subject: [PATCH 155/224] fix: al jugar carta defensa no se agregaba al mazo de descarte --- app/routers/games/services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index f646270..9d58d3e 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -657,6 +657,7 @@ def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): intention: Intention = game.intention intention.objective_player.hand.remove(card) + game.discard_deck.add(card) # Draw card until a card of type StayAway is obtained while (top_card := draw_card_by_drawing_order(game)).type != CardType.STAY_AWAY: From dc6cb37d775e06bbb1c504c4174bacae45b5281b Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 02:13:53 -0300 Subject: [PATCH 156/224] github action para correr los test siempre que se haga push --- .github/workflows/testing-ci.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/testing-ci.yaml diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml new file mode 100644 index 0000000..2a0461a --- /dev/null +++ b/.github/workflows/testing-ci.yaml @@ -0,0 +1,14 @@ +name: Tuki Testing CI +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: make install + + - name: Test + run: make test-all From 3650acb6b61cfedc9119cb5a7a3fe4481026d296 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 02:18:15 -0300 Subject: [PATCH 157/224] correccion en step de instalacion dependencias --- .github/workflows/testing-ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 2a0461a..1ba26c2 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -8,7 +8,9 @@ jobs: - uses: actions/checkout@v3 - name: Install dependencies - run: make install + run: | + pip install poetry + poetry install - name: Test run: make test-all From 97decbc054e00092e4e3b3db3e05aeec33f2d6ad Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 02:28:37 -0300 Subject: [PATCH 158/224] se fuerza la falla de un test para corroborar el action --- app/tests/player_tests/test_create_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/player_tests/test_create_player.py b/app/tests/player_tests/test_create_player.py index 9ceb357..0a63972 100644 --- a/app/tests/player_tests/test_create_player.py +++ b/app/tests/player_tests/test_create_player.py @@ -10,7 +10,7 @@ def test_create_player(): '/players', json={'name': 'pepito'} ) - assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." + assert response.status_code == 200, "El código de estado de la respuesta no es 201 (Created)." assert response.json() == { 'id': 1, 'name': 'pepito', From cab9b3b924e0f65b0f84e5b8b4a51999727f8833 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 02:32:09 -0300 Subject: [PATCH 159/224] se fuerza la falla de un test para corroborar el action --- app/tests/player_tests/test_create_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/player_tests/test_create_player.py b/app/tests/player_tests/test_create_player.py index 0a63972..9ceb357 100644 --- a/app/tests/player_tests/test_create_player.py +++ b/app/tests/player_tests/test_create_player.py @@ -10,7 +10,7 @@ def test_create_player(): '/players', json={'name': 'pepito'} ) - assert response.status_code == 200, "El código de estado de la respuesta no es 201 (Created)." + assert response.status_code == 201, "El código de estado de la respuesta no es 201 (Created)." assert response.json() == { 'id': 1, 'name': 'pepito', From 23b5d778482d2e9552558c9558676d8e595c8dde Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 03:00:11 -0300 Subject: [PATCH 160/224] agrego step para testear modulo Game --- .github/workflows/testing-ci.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 1ba26c2..5f71d1f 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -1,6 +1,11 @@ name: Tuki Testing CI on: [push] +env: + ENVIRONMENT: test + TEST_DB_FILE: ./app/database/database_test.sqlite + TEST_DIRECTORY: ./app/tests + jobs: test: runs-on: ubuntu-latest @@ -12,5 +17,7 @@ jobs: pip install poetry poetry install - - name: Test - run: make test-all + - name: Test Game module + run: | + poetry run -m pytest -vv $TEST_DIRECTORY/game_tests + rm -f $TEST_DB_FILE From 399375bfab7c0dc4173dd7a074069b5481850004 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 03:06:06 -0300 Subject: [PATCH 161/224] correccion en las variables de entorno --- .github/workflows/testing-ci.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 5f71d1f..c4b19f4 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -1,14 +1,15 @@ name: Tuki Testing CI on: [push] -env: - ENVIRONMENT: test - TEST_DB_FILE: ./app/database/database_test.sqlite - TEST_DIRECTORY: ./app/tests - jobs: test: runs-on: ubuntu-latest + + env: + ENVIRONMENT: test + TEST_DB_FILE: ./app/database/database_test.sqlite + TEST_DIRECTORY: ./app/tests + steps: - uses: actions/checkout@v3 @@ -19,5 +20,5 @@ jobs: - name: Test Game module run: | - poetry run -m pytest -vv $TEST_DIRECTORY/game_tests + poetry run pytest -vv $TEST_DIRECTORY/game_tests rm -f $TEST_DB_FILE From 2220530c52fb1324a9f762ee1ca05af524e30d34 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:26:47 -0300 Subject: [PATCH 162/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index c4b19f4..2fc7432 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -7,8 +7,8 @@ jobs: env: ENVIRONMENT: test - TEST_DB_FILE: ./app/database/database_test.sqlite - TEST_DIRECTORY: ./app/tests + TEST_DIRECTORY: ${{ github.workspace }}/app/tests + TEST_DB_FILE: ${{ github.workspace }}/app/database/database_test.sqlite steps: - uses: actions/checkout@v3 From 217a9b400a14ec1a74a7fb72fb4b612e0f28847c Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:28:49 -0300 Subject: [PATCH 163/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 2fc7432..a01b87a 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -13,6 +13,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: show env + run: env + - name: Install dependencies run: | pip install poetry From 17055fcda16214b8b903debbc7a7c66ed0bdb93e Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:36:33 -0300 Subject: [PATCH 164/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index a01b87a..ae5a4d7 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -23,5 +23,5 @@ jobs: - name: Test Game module run: | - poetry run pytest -vv $TEST_DIRECTORY/game_tests - rm -f $TEST_DB_FILE + poetry run coverage run -m pytest + # rm -f $TEST_DB_FILE From 78df92cdb09a80d558997388d60c43b04df3c679 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 03:39:46 -0300 Subject: [PATCH 165/224] update pipeline --- .github/workflows/testing-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index ae5a4d7..3217fa2 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -23,5 +23,5 @@ jobs: - name: Test Game module run: | - poetry run coverage run -m pytest - # rm -f $TEST_DB_FILE + poetry run coverage run -m pytest $TEST_DIRECTORY/game_tests + rm -f $TEST_DB_FILE From 490857e5bf9b8011ac6a9bb98a1228a4b8163827 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:44:55 -0300 Subject: [PATCH 166/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 3217fa2..4364ac4 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -13,9 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: show env - run: env - - name: Install dependencies run: | pip install poetry @@ -23,5 +20,5 @@ jobs: - name: Test Game module run: | - poetry run coverage run -m pytest $TEST_DIRECTORY/game_tests + poetry run -m pytest $TEST_DIRECTORY/game_tests rm -f $TEST_DB_FILE From bbbdba1be43dd1b202b55ce5bcd7eac91b83eb61 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:47:17 -0300 Subject: [PATCH 167/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 4364ac4..794fbd0 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -20,5 +20,5 @@ jobs: - name: Test Game module run: | - poetry run -m pytest $TEST_DIRECTORY/game_tests + poetry -m pytest $TEST_DIRECTORY/game_tests rm -f $TEST_DB_FILE From cf04be7af576fbbf7d08c24a9fa33c67f808941f Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:49:52 -0300 Subject: [PATCH 168/224] Update testing-ci.yaml --- .github/workflows/testing-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 794fbd0..837749c 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -20,5 +20,5 @@ jobs: - name: Test Game module run: | - poetry -m pytest $TEST_DIRECTORY/game_tests + poetry run pytest $TEST_DIRECTORY/game_tests rm -f $TEST_DB_FILE From 2dfeb940f566a908dd8358a650ffd61f4b97fc93 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 03:54:03 -0300 Subject: [PATCH 169/224] se agregan los demas steps para testear modulo player y card --- .github/workflows/testing-ci.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index 837749c..e41103f 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -20,5 +20,15 @@ jobs: - name: Test Game module run: | - poetry run pytest $TEST_DIRECTORY/game_tests + poetry run coverage run -m pytest $TEST_DIRECTORY/game_tests + rm -f $TEST_DB_FILE + + - name: Test Player module + run: | + poetry run coverage run -m pytest $TEST_DIRECTORY/player_tests + rm -f $TEST_DB_FILE + + - name: Test Card module + run: | + poetry run coverage run -m pytest $TEST_DIRECTORY/card_tests rm -f $TEST_DB_FILE From dcadcd1d5366760cb35d6ec2920fd0676dbc1670 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 04:11:32 -0300 Subject: [PATCH 170/224] se cachean las dependencias de poetry --- .github/workflows/testing-ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing-ci.yaml b/.github/workflows/testing-ci.yaml index e41103f..6432678 100644 --- a/.github/workflows/testing-ci.yaml +++ b/.github/workflows/testing-ci.yaml @@ -13,10 +13,14 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install dependencies - run: | - pip install poetry - poetry install + - name: Install and caching poetry dependencies + run: pipx install poetry + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: "poetry" + - run: poetry install - name: Test Game module run: | From fbacbc4655678a6bc595c8dfe6566cd8a3e38a6b Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 13:24:08 -0300 Subject: [PATCH 171/224] instrucciones en el readme para correr la app en contenedores usando docker --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 0586c61..61415ff 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,15 @@ In the Makefile, you have the following targets: **Remember to create a .env file with the necessary environment variables before using the Makefile** **To know the necessary environment variables you can see the `.env.example` file.** + + +## Running the Application in Containers - Docker + +### Building the Docker Image +`docker build -t backend-tuki .` + +### Run a container based on the built image +`docker run --name backend-tuki-container -p 8000:8000 backend-tuki` + +### Running the Application in Containers - Docker Compose +`docker-compose up --build` From fb68356d762529b78591e77b14b468f421bfbbad Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 15:14:00 -0300 Subject: [PATCH 172/224] falla de test a proposito --- app/tests/player_tests/test_delete_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/player_tests/test_delete_player.py b/app/tests/player_tests/test_delete_player.py index 2f84f86..1202cbd 100644 --- a/app/tests/player_tests/test_delete_player.py +++ b/app/tests/player_tests/test_delete_player.py @@ -16,7 +16,7 @@ def test_delete_player_idempotent(): response = client.delete( '/players/2', ) - assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." + assert response.status_code == 201, "El código de estado de la respuesta no es 404 (Not Found)." def test_delete_inexistent_player(): From 2c5eeb4cb7d12be870e3339cad517da14d9a88ae Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 15:16:22 -0300 Subject: [PATCH 173/224] correction de test a proposito --- app/tests/player_tests/test_delete_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/player_tests/test_delete_player.py b/app/tests/player_tests/test_delete_player.py index 1202cbd..2f84f86 100644 --- a/app/tests/player_tests/test_delete_player.py +++ b/app/tests/player_tests/test_delete_player.py @@ -16,7 +16,7 @@ def test_delete_player_idempotent(): response = client.delete( '/players/2', ) - assert response.status_code == 201, "El código de estado de la respuesta no es 404 (Not Found)." + assert response.status_code == 404, "El código de estado de la respuesta no es 404 (Not Found)." def test_delete_inexistent_player(): From fbfe143c0fc26c40cbfd28066915fb01f97caee2 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Wed, 8 Nov 2023 15:54:24 -0300 Subject: [PATCH 174/224] =?UTF-8?q?se=20coment=C3=B3=20pygame=20para=20evi?= =?UTF-8?q?tar=20warnings=20en=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 6 +++--- app/utils.py | 56 ++++++++++++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/main.py b/app/main.py index d410539..ee6cbb8 100644 --- a/app/main.py +++ b/app/main.py @@ -5,8 +5,8 @@ from app.routers.games import games from app.routers.cards import cards from app.routers.websockets import websockets -from .utils import show_initial_image -import threading +# from .utils import show_initial_image +# import threading app = FastAPI() @@ -26,5 +26,5 @@ app.include_router(websockets.router) # This displays the initial image with the sound -t = threading.Thread(target=show_initial_image) +# t = threading.Thread(target=show_initial_image) # t.start() diff --git a/app/utils.py b/app/utils.py index 91bb096..88eeed0 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,39 +1,39 @@ -import os -import pygame -import time +# import os +# import pygame +# import time -def show_initial_image(): - # Path of the image I want to display - image_path = "./app/resources/stay_away.png" +# def show_initial_image(): +# # Path of the image I want to display +# image_path = "./app/resources/stay_away.png" - # Path of the audio file - audio_path = "./app/resources/stay_away.mp3" +# # Path of the audio file +# audio_path = "./app/resources/stay_away.mp3" - # pygame configuration - pygame.init() - window = pygame.display.set_mode((400, 300), pygame.NOFRAME) +# # pygame configuration +# pygame.init() +# window = pygame.display.set_mode((400, 300), pygame.NOFRAME) - # Load the image - image = pygame.image.load(image_path) +# # Load the image +# image = pygame.image.load(image_path) - # Get the width and height of the window - window_width, window_height = window.get_size() +# # Get the width and height of the window +# window_width, window_height = window.get_size() - # Scale the image to the size of the window - image = pygame.transform.scale(image, (window_width, window_height)) +# # Scale the image to the size of the window +# image = pygame.transform.scale(image, (window_width, window_height)) - # Show the image in the window - window.blit(image, (0, 0)) - pygame.display.flip() +# # Show the image in the window +# window.blit(image, (0, 0)) +# pygame.display.flip() - # Play audio file - pygame.mixer.init() - pygame.mixer.music.load(audio_path) - pygame.mixer.music.play() +# # Play audio file +# pygame.mixer.init() +# pygame.mixer.music.load(audio_path) +# pygame.mixer.music.play() - # Wait 6 seconds - time.sleep(6) +# # Wait 6 seconds +# time.sleep(6) - # Close the window - pygame.quit() +# # Close the window +# pygame.quit() From 86f4e1709129d5918a7782f5bb59a7ca3cb15001 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Wed, 8 Nov 2023 16:24:08 -0300 Subject: [PATCH 175/224] =?UTF-8?q?=C3=9Altimo=20infectado=20pierde=20al?= =?UTF-8?q?=20finalizar=20partida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/database/models.py | 4 ++++ app/routers/games/services.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/database/models.py b/app/database/models.py index 1518fe3..fa1daea 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -14,6 +14,8 @@ class Player(db.Entity): hand = Set('Card') intention_creator = Optional('Intention', reverse='player') intention_objective = Optional('Intention', reverse='objective_player') + game_last_infected = Optional( + 'Game', reverse='last_infected', nullable=True) class Game(db.Entity): @@ -30,6 +32,8 @@ class Game(db.Entity): draw_deck_order = Required(Json, default=[]) round_direction = Required(str, default=RoundDirection.CLOCKWISE) intention = Optional('Intention', reverse='game') + last_infected = Optional( + Player, reverse='game_last_infected', nullable=True) class Card(db.Entity): diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8fdd920..4e41498 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -432,11 +432,13 @@ def get_game_result(name: str) -> GameResult: lambda p: p.rol != PlayerRol.THE_THING)[:] elif no_human_remains(game): - reason = "No queda ningún Humano en la partida." + reason = '''No queda ningún Humano en la partida. Todos fueron infectados o eliminados. + Pierden los eliminados y el último infectado.''' winners = game.players.select( lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] losers = game.players.select( lambda p: p.rol == PlayerRol.ELIMINATED)[:] + losers.append(game.last_infected) # elif the_thing_declared_a_wrong_victory(game): else: From ebce152c54ead9942217e31ee2322564cf5add47 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Wed, 8 Nov 2023 16:37:17 -0300 Subject: [PATCH 176/224] agregado el cambio de rol al intercambiar carta infectado --- app/routers/games/action_functions.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index ab95db7..4304748 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -1,7 +1,7 @@ from pony.orm import db_session, select from app.database.models import Game, Card, Player from ..players.schemas import PlayerRol -from ..cards.schemas import CardType, CardResponse +from ..cards.schemas import CardType, CardResponse, CardSubtype from ..websockets.utils import player_connections from .utils import Events from .schemas import RoundDirection, GameStatus @@ -164,6 +164,14 @@ def process_card_exchange(player: Player, objective_player: Player, player_card: objective_player.hand.remove(objective_player_card) objective_player.hand.add(player_card) + if (player.rol == PlayerRol.THE_THING and player_card.type == CardSubtype.CONTAGION): + objective_player.rol = PlayerRol.INFECTED + objective_player.game_last_infected = objective_player.game + + elif (objective_player.rol == PlayerRol.THE_THING and objective_player_card.type == CardSubtype.CONTAGION): + player.rol = PlayerRol.INFECTED + player.game_last_infected = player.game + # enviar evento de intercambio de carta From db4c951d7a4492f096939cbef79c2da07d406898 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Wed, 8 Nov 2023 18:04:03 -0300 Subject: [PATCH 177/224] hotfix --- app/routers/games/action_functions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 4304748..f381535 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -165,12 +165,14 @@ def process_card_exchange(player: Player, objective_player: Player, player_card: objective_player.hand.add(player_card) if (player.rol == PlayerRol.THE_THING and player_card.type == CardSubtype.CONTAGION): - objective_player.rol = PlayerRol.INFECTED - objective_player.game_last_infected = objective_player.game + if objective_player.rol != PlayerRol.INFECTED: + objective_player.rol = PlayerRol.INFECTED + objective_player.game_last_infected = objective_player.game elif (objective_player.rol == PlayerRol.THE_THING and objective_player_card.type == CardSubtype.CONTAGION): - player.rol = PlayerRol.INFECTED - player.game_last_infected = player.game + if player.rol != PlayerRol.INFECTED: + player.rol = PlayerRol.INFECTED + player.game_last_infected = player.game # enviar evento de intercambio de carta From 6ba453b7fd9199be9990e8da5bab095c6834bf7c Mon Sep 17 00:00:00 2001 From: Anelio Alvarez <72666259+anelioalvarez@users.noreply.github.com> Date: Wed, 8 Nov 2023 19:29:19 -0300 Subject: [PATCH 178/224] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61415ff..ff7f863 100644 --- a/README.md +++ b/README.md @@ -68,5 +68,5 @@ In the Makefile, you have the following targets: ### Run a container based on the built image `docker run --name backend-tuki-container -p 8000:8000 backend-tuki` -### Running the Application in Containers - Docker Compose +## Running the Application in Containers - Docker Compose `docker-compose up --build` From 41012221e3b028cfa9d76571ecbe0386997546ca Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Wed, 8 Nov 2023 22:13:00 -0300 Subject: [PATCH 179/224] correccion en el json_msg al jugar la carta de defensa --- app/routers/games/defense_functions.py | 10 +++++----- app/routers/games/games.py | 11 ++++++++--- app/routers/games/intention.py | 9 +++++++-- app/routers/games/utils.py | 1 + 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/routers/games/defense_functions.py b/app/routers/games/defense_functions.py index 834b08a..3ba2ac4 100644 --- a/app/routers/games/defense_functions.py +++ b/app/routers/games/defense_functions.py @@ -1,14 +1,14 @@ from pony.orm import * from app.database.models import Player -from ..cards.schemas import CardActionName, CardDefenseName +from ..cards.schemas import CardDefenseName from enum import Enum class ActionType(str, Enum): - EXCHANGE_OFFER = "Ofrecimiento de intercambio" - CHANGE_PLACES = CardActionName.CHANGE_PLACES.value - BETTER_RUN = CardActionName.BETTER_RUN.value - FLAMETHROWER = CardActionName.FLAMETHROWER.value + EXCHANGE_OFFER = "exchange_offer" + CHANGE_PLACES = "change_places" + BETTER_RUN = "better_run" + FLAMETHROWER = "flamethrower" response_to_action_type = { diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 7200d76..3790e6e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -336,11 +336,16 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation if defense_info.card_id: services.play_defense_card(game_name, defense_info) + intention: Intention = get_intention_in_game(game_name) json_msg = { - "event": get_intention_in_game(game_name).action_type, - "player_id": defense_info.player_id, - "card_id": defense_info.card_id + "event": utils.Events.DEFENSE_CARD_PLAYED, + "card_id": defense_info.card_id, + "player_id": intention.player.id, + "player_name": intention.player.name, + "objective_player_id": intention.objective_player.id, + "objective_player_name": intention.objective_player.name, + "action_type": intention.action_type } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) else: diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index 38ed066..17ec2d1 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -38,6 +38,11 @@ def create_intention_in_game(game: Game, action_type: ActionType, player: Player return intention +@db_session +def set_objective_card_in_exchange_payload(game: Game, objective_card_id: int): + game.intention.exchange_payload['objective_card_id'] = objective_card_id + + @db_session def process_intention_in_game(game_name) -> Intention: game: Game = find_game_by_name(game_name) @@ -49,9 +54,9 @@ def process_intention_in_game(game_name) -> Intention: case ActionType.EXCHANGE_OFFER: exchange_info = intention.exchange_payload - player_card = find_card_by_id(exchange_info.card_id) + player_card = find_card_by_id(exchange_info['card_id']) objective_player_card = find_card_by_id( - exchange_info.objective_card_id) + exchange_info['objective_card_id']) process_card_exchange(player, objective_player, player_card, objective_player_card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 71aa06d..aafd32f 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -49,6 +49,7 @@ class Events(str, Enum): SEDUCTION_DONE = 'seduction_done' INTERCHANGE_INTENTION = 'interchange_intention' INTERCHANGE_INTENTION_DONE = 'interchange_intention_done' + DEFENSE_CARD_PLAYED = "defense_card_played" async def send_played_card_event(game_name: str, player_id: int, card_id: int): From 82e346b593ac58caf328867e1d89d4ef49654181 Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 8 Nov 2023 22:42:46 -0300 Subject: [PATCH 180/224] =?UTF-8?q?Hotfix=20-=20Se=20agrega=20cheat=20para?= =?UTF-8?q?=20la=20carta=20an=C3=A1lisis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/websockets/services.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index ba0ceba..7c85f29 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -19,8 +19,9 @@ async def send_list_of_cheats(player_id: int): '[det | determinación | resolute]: Obtienes una carta determinación', '[olv | olvidadizo | forgetful]: Obtienes una carta olvidadizo', '[uno_dos | one_two]: Obtienes una carta uno, dos...', - '[sed | seducción | seduction]: Obtienes una carta de seducción' - '[vig | vigila_tus_espaldas | watch_your_back]: Obtienes una carta vigila tus espaldas' + '[sed | seducción | seduction]: Obtienes una carta de seducción', + '[vig | vigila_tus_espaldas | watch_your_back]: Obtienes una carta vigila tus espaldas', + '[an | análisis | analysis]: Obtienes una carta análisis' ] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) @@ -67,10 +68,13 @@ async def handle_message(data, player_id): elif message == 'det' or message == 'determinación' or message == 'resolute': apply_cheat(game_name, player_id, range(43, 48)) await send_event_cheat_used(player_id) - + elif message == 'vig' or message == 'vigila_tus_espaldas' or message == 'watch_your_back': apply_cheat(game_name, player_id, range(53, 55)) await send_event_cheat_used(player_id) + elif message == "an" or message == "análisis" or message == "analysis": + apply_cheat(game_name, player_id, range(27, 31)) + await send_event_cheat_used(player_id) async def websocket_games(player_id: int, websocket: WebSocket): From 2d9a398883954a2fcc5edc6590b7c91780200d1a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Wed, 8 Nov 2023 23:21:01 -0300 Subject: [PATCH 181/224] Hotfix - Se arregla error menor en range de carta analisis para el cheat --- app/routers/websockets/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 7c85f29..22fa002 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -73,7 +73,7 @@ async def handle_message(data, player_id): apply_cheat(game_name, player_id, range(53, 55)) await send_event_cheat_used(player_id) elif message == "an" or message == "análisis" or message == "analysis": - apply_cheat(game_name, player_id, range(27, 31)) + apply_cheat(game_name, player_id, range(27, 30)) await send_event_cheat_used(player_id) From 123f1173305668e4da595b680110621356e7067f Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Thu, 9 Nov 2023 19:48:59 -0300 Subject: [PATCH 182/224] hotfix: verificacion al jugar defensa siempre falso por comparar objeto con entero xd --- app/routers/games/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index aafd32f..363b81d 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -592,7 +592,7 @@ def verify_defense_card_can_be_played(game_name: str, defense_info: PlayDefenseI detail='No intention to conclude in the game.' ) - if game.intention.objective_player != defense_info.player_id: + if game.intention.objective_player.id != defense_info.player_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Does not correspond to the objective player for the intention.' From e4c60409102f5eb19512b3952dccd0fc27cc03d4 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Fri, 10 Nov 2023 10:35:37 -0300 Subject: [PATCH 183/224] hotfix --- app/routers/games/games.py | 4 ++-- app/routers/games/schemas.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 3790e6e..7cdcac0 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -355,8 +355,8 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation @router.patch("/{game_name}/the-thing-end-game") -async def the_thing_end_game(game_name: str, player_id: int): - utils.verify_player_is_the_thing(player_id, game_name) +async def the_thing_end_game(game_name: str, game_data: TheThingEndGameIn): + utils.verify_player_is_the_thing(game_data.player_id, game_name) services.finish_game_by_the_thing(game_name) json_msg = { diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 267b499..59b6c12 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -190,3 +190,8 @@ class OneTwoEffectIn(BaseModel): player_id: int objective_player_id: int + +class TheThingEndGameIn(BaseModel): + model_config = ConfigDict(from_attributes=True) + + player_id: int \ No newline at end of file From 5e9cfc8f50637b004421edef5ae7b938739d3d8b Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Fri, 10 Nov 2023 11:46:11 -0300 Subject: [PATCH 184/224] se agrega nombre del player objetivo del analisis --- app/routers/games/action_functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index ab95db7..a740de3 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -79,7 +79,8 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play @db_session def process_analysis_card(game: Game, player: Player, objective_player: Player): - result = [CardResponse(id=c.id, + result = {} + result['cards'] = [CardResponse(id=c.id, number=c.number, type=c.type, subtype=c.subtype, @@ -87,6 +88,7 @@ def process_analysis_card(game: Game, player: Player, objective_player: Player): description=c.description ) for c in objective_player.hand] + result['objective_player_name'] = objective_player.name return result From a90166dae2beb259fb5c3b079cad5bfeeb038a1a Mon Sep 17 00:00:00 2001 From: Ignacio Belitzky Date: Fri, 10 Nov 2023 16:00:44 -0300 Subject: [PATCH 185/224] Hotfix - Se agrega cheat para nada de barbacoas --- app/routers/websockets/services.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index 22fa002..d811e39 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -21,7 +21,8 @@ async def send_list_of_cheats(player_id: int): '[uno_dos | one_two]: Obtienes una carta uno, dos...', '[sed | seducción | seduction]: Obtienes una carta de seducción', '[vig | vigila_tus_espaldas | watch_your_back]: Obtienes una carta vigila tus espaldas', - '[an | análisis | analysis]: Obtienes una carta análisis' + '[an | análisis | analysis]: Obtienes una carta análisis', + '[nb | nada_barbacoas | no_barbecue]: Obtienes una carta nada de barbacoas' ] for message in cheat_messages: await player_connections.send_message(player_id, 'Loki', message) @@ -75,6 +76,9 @@ async def handle_message(data, player_id): elif message == "an" or message == "análisis" or message == "analysis": apply_cheat(game_name, player_id, range(27, 30)) await send_event_cheat_used(player_id) + elif message == "nb" or message == "nada_barbacoas" or message == "no_barbecue": + apply_cheat(game_name, player_id, range(84, 86)) + await send_event_cheat_used(player_id) async def websocket_games(player_id: int, websocket: WebSocket): From 0bc005c990600c5a766fd62ad840c6377040d61c Mon Sep 17 00:00:00 2001 From: nehu Date: Fri, 10 Nov 2023 16:29:39 -0300 Subject: [PATCH 186/224] defense card ya no envia nombres --- app/routers/games/games.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 7cdcac0..3921a1d 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -342,9 +342,7 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "event": utils.Events.DEFENSE_CARD_PLAYED, "card_id": defense_info.card_id, "player_id": intention.player.id, - "player_name": intention.player.name, "objective_player_id": intention.objective_player.id, - "objective_player_name": intention.objective_player.name, "action_type": intention.action_type } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) From fbf133f8d2d09d3a93cc241678dd8c6af4961175 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 10 Nov 2023 16:57:09 -0300 Subject: [PATCH 187/224] hotfix: finalizar partida puesta como asincrona innecesario no actualiza el status --- app/routers/games/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 8fdd920..c57c4b0 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -254,7 +254,7 @@ async def finish_game(name: str) -> Game: @db_session -async def finish_game_by_the_thing(name: str) -> Game: +def finish_game_by_the_thing(name: str) -> Game: game: Game = find_game_by_name(name) game.status = GameStatus.ENDED From fa7b086e840f522bab758ecfe482dab6aebfb35f Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 10 Nov 2023 18:12:29 -0300 Subject: [PATCH 188/224] se envian los eventos que restaban al procesar la intencion --- app/routers/games/action_functions.py | 45 +++++++++++++++++++-------- app/routers/games/intention.py | 2 +- app/routers/games/services.py | 3 +- app/routers/games/utils.py | 1 + app/routers/players/schemas.py | 1 + 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index a740de3..a4e4c48 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -37,6 +37,24 @@ async def send_players_whiskey_event(game: Game, player_id: int, player_name: st await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) +async def send_players_chagnge_event(game: Game, player_id: int, objective_player_id: int): + json_msg = { + "event": Events.CHANGE_DONE, + "player_id": player_id, + "objective_player_id": objective_player_id + } + await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) + + +async def send_players_exchagnge_event(game: Game, player_id: int, objective_player_id: int): + json_msg = { + "event": Events.EXCHANGE_DONE, + "player_id": player_id, + "objective_player_id": objective_player_id + } + await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) + + async def send_resolute_card_played_event(game: Game, player_id: int, option_cards: list[int]): json_msg = { "event": Events.RESOLUTE_CARD_PLAYED, @@ -81,12 +99,12 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play def process_analysis_card(game: Game, player: Player, objective_player: Player): result = {} result['cards'] = [CardResponse(id=c.id, - number=c.number, - type=c.type, - subtype=c.subtype, - name=c.name, - description=c.description - ) for c in objective_player.hand] + number=c.number, + type=c.type, + subtype=c.subtype, + name=c.name, + description=c.description + ) for c in objective_player.hand] result['objective_player_name'] = objective_player.name return result @@ -140,33 +158,34 @@ def process_watch_your_back_card(game: Game): @db_session def process_change_places_card(game: Game, player: Player, objective_player: Player): - # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition - # enviar evento de intercambio de lugar + asyncio.ensure_future(send_players_chagnge_event( + game, player.id, objective_player.id)) @db_session def process_better_run_card(game: Game, player: Player, objective_player: Player): - # Intercambio de posiciones entre los jugadores tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition - # enviar evento de cambio de lugar + asyncio.ensure_future(send_players_chagnge_event( + game, player.id, objective_player.id)) @db_session -def process_card_exchange(player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): +def process_card_exchange(game: Game, player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): player.hand.remove(player_card) player.hand.add(objective_player_card) objective_player.hand.remove(objective_player_card) objective_player.hand.add(player_card) - # enviar evento de intercambio de carta + asyncio.ensure_future(send_players_exchagnge_event( + game, player.id, objective_player.id)) @db_session @@ -177,7 +196,7 @@ def process_seduction_card(game: Game, player: Player, objective_player: Player, c for c in objective_player_hand_list if c.type != CardType.THE_THING] random_card = random.choice(eligible_cards) - process_card_exchange(player, objective_player, + process_card_exchange(game, player, objective_player, card_to_exchange, random_card) asyncio.ensure_future(send_seduction_done_event( diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index 17ec2d1..0c45152 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -58,7 +58,7 @@ def process_intention_in_game(game_name) -> Intention: objective_player_card = find_card_by_id( exchange_info['objective_card_id']) - process_card_exchange(player, objective_player, + process_card_exchange(game, player, objective_player, player_card, objective_player_card) case ActionType.CHANGE_PLACES: diff --git a/app/routers/games/services.py b/app/routers/games/services.py index c57c4b0..2a91010 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -563,7 +563,8 @@ def card_interchange_response(game_name: str, game_data: InterchangeInformationI next_player: Player = find_player_by_id(game_data.player_id) next_player_card: Card = cards_services.find_card_by_id(game_data.card_id) - process_card_exchange(player, next_player, player_card, next_player_card) + process_card_exchange(game, player, next_player, + player_card, next_player_card) update_game_turn(game_name) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 363b81d..2bbd27a 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -39,6 +39,7 @@ class Events(str, Enum): REVELATIONS_DONE = 'revelations_done' EXCHANGE_INTENTION = 'exchange_intention' EXCHANGE_DONE = 'exchange_done' + CHANGE_DONE = 'change_done' BLIND_DATE_SELECTION = 'blind_date_selection' BLIND_DATE_DONE = 'blind_date_done' OOOPS_CARD_PLAYED = 'ooops_card_played' diff --git a/app/routers/players/schemas.py b/app/routers/players/schemas.py index 4c8f941..163198f 100644 --- a/app/routers/players/schemas.py +++ b/app/routers/players/schemas.py @@ -32,6 +32,7 @@ class PlayerUpdateIn(BaseModel): class PlayerResponse(BasePlayer): id: int position: int + rol: PlayerRol class PlayerInfo(PlayerResponse): From 6856ac94e50bd4d2f1f03a4d0245c2c6f44bf6fb Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Fri, 10 Nov 2023 18:19:09 -0300 Subject: [PATCH 189/224] se quita rol que rompe los tests xd --- app/routers/players/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/routers/players/schemas.py b/app/routers/players/schemas.py index 163198f..4c8f941 100644 --- a/app/routers/players/schemas.py +++ b/app/routers/players/schemas.py @@ -32,7 +32,6 @@ class PlayerUpdateIn(BaseModel): class PlayerResponse(BasePlayer): id: int position: int - rol: PlayerRol class PlayerInfo(PlayerResponse): From c71ac7b42b6124467d57601fbdd690a3ce934676 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 15:35:20 -0300 Subject: [PATCH 190/224] Se actualiza el turno luego del cambio de lugar --- app/routers/games/action_functions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index a4e4c48..3c62cbc 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -161,6 +161,7 @@ def process_change_places_card(game: Game, player: Player, objective_player: Pla tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition + game.turn = player.position asyncio.ensure_future(send_players_chagnge_event( game, player.id, objective_player.id)) @@ -171,6 +172,7 @@ def process_better_run_card(game: Game, player: Player, objective_player: Player tempPosition = player.position player.position = objective_player.position objective_player.position = tempPosition + game.turn = player.position asyncio.ensure_future(send_players_chagnge_event( game, player.id, objective_player.id)) @@ -199,5 +201,5 @@ def process_seduction_card(game: Game, player: Player, objective_player: Player, process_card_exchange(game, player, objective_player, card_to_exchange, random_card) - asyncio.ensure_future(send_seduction_done_event( - player.id, objective_player.id)) + # asyncio.ensure_future(send_seduction_done_event( + # player.id, objective_player.id)) From 7956e28957848044a0062976eb526f136db8782a Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 16:03:50 -0300 Subject: [PATCH 191/224] =?UTF-8?q?Arreglo=20de=20envio=20de=20mensajes=20?= =?UTF-8?q?al=20jugar=20seducci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/action_functions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 3c62cbc..b70f061 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -49,8 +49,8 @@ async def send_players_chagnge_event(game: Game, player_id: int, objective_playe async def send_players_exchagnge_event(game: Game, player_id: int, objective_player_id: int): json_msg = { "event": Events.EXCHANGE_DONE, - "player_id": player_id, - "objective_player_id": objective_player_id + "player_name": get_player_name_by_id(player_id), + "objective_player_name": get_player_name_by_id(objective_player_id) } await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) @@ -186,9 +186,6 @@ def process_card_exchange(game: Game, player: Player, objective_player: Player, objective_player.hand.remove(objective_player_card) objective_player.hand.add(player_card) - asyncio.ensure_future(send_players_exchagnge_event( - game, player.id, objective_player.id)) - @db_session def process_seduction_card(game: Game, player: Player, objective_player: Player, @@ -201,5 +198,8 @@ def process_seduction_card(game: Game, player: Player, objective_player: Player, process_card_exchange(game, player, objective_player, card_to_exchange, random_card) + asyncio.ensure_future(send_players_exchagnge_event( + game, player.id, objective_player.id)) + # asyncio.ensure_future(send_seduction_done_event( # player.id, objective_player.id)) From 0ac8b2f09b24801d6547e9d4081f4c11a64142e8 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 16:34:13 -0300 Subject: [PATCH 192/224] cambio en envio de mensajes --- app/routers/games/action_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index b70f061..cd1f212 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -43,7 +43,7 @@ async def send_players_chagnge_event(game: Game, player_id: int, objective_playe "player_id": player_id, "objective_player_id": objective_player_id } - await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) + await player_connections.send_event_to_all_players_in_game(game.name, json_msg, player_id) async def send_players_exchagnge_event(game: Game, player_id: int, objective_player_id: int): @@ -52,7 +52,7 @@ async def send_players_exchagnge_event(game: Game, player_id: int, objective_pla "player_name": get_player_name_by_id(player_id), "objective_player_name": get_player_name_by_id(objective_player_id) } - await player_connections.send_event_to_other_players_in_game(game.name, json_msg, player_id) + await player_connections.send_event_to_all_players_in_game(game.name, json_msg, player_id) async def send_resolute_card_played_event(game: Game, player_id: int, option_cards: list[int]): From 6d08247366b4ec0a5610d1d92e6c43213658182c Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 16:41:13 -0300 Subject: [PATCH 193/224] fix --- app/routers/games/action_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index cd1f212..f906cb9 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -43,7 +43,7 @@ async def send_players_chagnge_event(game: Game, player_id: int, objective_playe "player_id": player_id, "objective_player_id": objective_player_id } - await player_connections.send_event_to_all_players_in_game(game.name, json_msg, player_id) + await player_connections.send_event_to_all_players_in_game(game.name, json_msg) async def send_players_exchagnge_event(game: Game, player_id: int, objective_player_id: int): @@ -52,7 +52,7 @@ async def send_players_exchagnge_event(game: Game, player_id: int, objective_pla "player_name": get_player_name_by_id(player_id), "objective_player_name": get_player_name_by_id(objective_player_id) } - await player_connections.send_event_to_all_players_in_game(game.name, json_msg, player_id) + await player_connections.send_event_to_all_players_in_game(game.name, json_msg) async def send_resolute_card_played_event(game: Game, player_id: int, option_cards: list[int]): From 930f16cd0cf74f5b2be3ddbbcdc0fbd299714cde Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 17:27:51 -0300 Subject: [PATCH 194/224] fix: cambio de turno lanzallamas --- app/routers/games/services.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 2a91010..fee56c0 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -283,9 +283,6 @@ def play_action_card(game_name: str, play_info: PlayInformation): objective_player: Player = find_player_by_id( play_info.objective_player_id) - if game.turn != 0 and objective_player.position < player.position: - game.turn = game.turn - 1 - create_intention_in_game( game, ActionType.FLAMETHROWER, player, objective_player) From 0e00476623910133227b8a58e876a5d7d07be281 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 17:29:55 -0300 Subject: [PATCH 195/224] fix: reacomodo de turno --- app/routers/games/action_functions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index f906cb9..f1d0220 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -88,6 +88,10 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play p.position -= 1 objective_player.position = -1 + # Reacomodo el turno + if game.turn != 0 and objective_player.position < player.position: + game.turn = game.turn - 1 + asyncio.ensure_future(send_players_eliminated_event(game=game, killer_id=player.id, killer_name=player.name, From 9ca81e6eded66112abb9c86968e4f1746a826c3a Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 19:34:56 -0300 Subject: [PATCH 196/224] nuevo mensaje ws y sleep para no pisar eventos --- app/routers/games/action_functions.py | 10 ++++++++++ app/routers/games/utils.py | 1 + 2 files changed, 11 insertions(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index f1d0220..27921a4 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -70,6 +70,14 @@ async def send_seduction_done_event(player_id: int, objective_player_id: int): await player_connections.send_event_to(player_id, json_msg) await player_connections.send_event_to(objective_player_id, json_msg) +async def send_suspicious_card_played_event(player_id: int, card_name: str): + json_msg = { + "event": Events.SUSPICIOUS_CARD_PLAYED, + "card_name": card_name + } + await player_connections.send_event_to(player_id, json_msg) + await asyncio.sleep(5) + @db_session def process_flamethrower_card(game: Game, player: Player, objective_player: Player): @@ -127,6 +135,8 @@ def process_suspicious_card(game: Game, player: Player, objective_player: Player description=random_card.description ) + asyncio.ensure_future(send_suspicious_card_played_event(player.id, random_card.name)) + return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 2bbd27a..adc2fb1 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -51,6 +51,7 @@ class Events(str, Enum): INTERCHANGE_INTENTION = 'interchange_intention' INTERCHANGE_INTENTION_DONE = 'interchange_intention_done' DEFENSE_CARD_PLAYED = "defense_card_played" + SUSPICIOUS_CARD_PLAYED = "suspicious_card_played" async def send_played_card_event(game_name: str, player_id: int, card_id: int): From e4d4dc42436b2964eb58161c58a8f86363b31214 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 11 Nov 2023 19:43:31 -0300 Subject: [PATCH 197/224] refactorizacion intercambio de cartas con defensa --- app/routers/games/games.py | 33 ++++++++++++++++++--------------- app/routers/games/services.py | 14 ++++++++------ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 3921a1d..d2f37dd 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -223,20 +223,20 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent utils.verify_if_interchange_can_be_done(game_name, interchange_info) objective_player_id = utils.get_id_of_next_player_in_turn(game_name) - '''exchange_intention = services.register_card_exchange_intention( - game_name, interchange_info)''' - - with db_session: - objective_player = find_player_by_id(objective_player_id) - - json_msg = { - "event": "exchange_intention", - "player_id": interchange_info.player_id, - "player_name": get_player_name_by_id(interchange_info.player_id), - "card_to_exchange": interchange_info.card_id, - "defense_cards": player_cards_to_defend_himself(ActionType.EXCHANGE_OFFER, objective_player) - } - await player_connections.send_event_to(objective_player_id, json_msg) + services.register_card_exchange_intention( + game_name, interchange_info.player_id, interchange_info.card_id, objective_player_id) + + # with db_session: + # objective_player = find_player_by_id(objective_player_id) + + # json_msg = { + # "event": "exchange_intention", + # "player_id": interchange_info.player_id, + # "player_name": get_player_name_by_id(interchange_info.player_id), + # "card_to_exchange": interchange_info.card_id, + # "defense_cards": player_cards_to_defend_himself(ActionType.EXCHANGE_OFFER, objective_player) + # } + # await player_connections.send_event_to(objective_player_id, json_msg) return {"message": "Card interchange intention terminated."} @@ -245,7 +245,10 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) - # clean_intention_in_game(game_name) + game: Game = find_game_by_name(game_name) + set_objective_card_in_exchange_payload(game, game_data.card_id) + + clean_intention_in_game(game_name) json_msg = { "event": utils.Events.EXCHANGE_DONE, diff --git a/app/routers/games/services.py b/app/routers/games/services.py index fee56c0..76dcd58 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -357,8 +357,10 @@ def play_action_card(game_name: str, play_info: PlayInformation): detail="The card to exchange cannot be The Thing" ) verify_card_in_hand(player, card_to_exchange) - process_seduction_card( - game, player, objective_player, card_to_exchange) + + exchange_payload = {"card_id": card_to_exchange.id} + create_intention_in_game( + game, ActionType.EXCHANGE_OFFER, player, objective_player, exchange_payload) player.hand.remove(card) game.discard_deck.add(card) @@ -538,11 +540,11 @@ def pass_card(play_info: PlayInformation): @db_session -def register_card_exchange_intention(game_name: str, exchange_info: IntentionExchangeInformationIn) -> Intention: +def register_card_exchange_intention(game_name: str, player_id: int, card_id: int, objective_player_id: int) -> Intention: game: Game = find_game_by_name(game_name) - player: Player = find_player_by_id(exchange_info.player_id) - objective_player = utils.get_next_player_in_turn(game) - exchange_payload = {"card_id": exchange_info.card_id} + player: Player = find_player_by_id(player_id) + objective_player = find_player_by_id(objective_player_id) + exchange_payload = {"card_id": card_id} exchange_intention = create_intention_in_game( game, ActionType.EXCHANGE_OFFER, player, objective_player, exchange_payload) From fe0aa5c0f579aa3e892cfea476b713f20ddd04fd Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 11 Nov 2023 20:51:18 -0300 Subject: [PATCH 198/224] ya ni se --- app/routers/games/games.py | 15 +++++++++++++-- app/routers/games/intention.py | 14 +++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index d2f37dd..b3c7514 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -245,8 +245,19 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) - game: Game = find_game_by_name(game_name) - set_objective_card_in_exchange_payload(game, game_data.card_id) + with db_session: + game: Game = find_game_by_name(game_name) + + intention: Intention = get_intention_in_game(game_name) + + player = find_player_by_id(intention.player.id) + objective_player = find_player_by_id(intention.objective_player.id) + + player_card = find_card_by_id(intention.exchange_payload['card_id']) + objective_player_card = find_card_by_id(game_data.card_id) + + process_card_exchange(game, player, objective_player, + player_card, objective_player_card) clean_intention_in_game(game_name) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index 0c45152..03c664f 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -51,15 +51,15 @@ def process_intention_in_game(game_name) -> Intention: objective_player = intention.objective_player match intention.action_type: - case ActionType.EXCHANGE_OFFER: - exchange_info = intention.exchange_payload + # case ActionType.EXCHANGE_OFFER: + # exchange_info = intention.exchange_payload - player_card = find_card_by_id(exchange_info['card_id']) - objective_player_card = find_card_by_id( - exchange_info['objective_card_id']) + # player_card = find_card_by_id(exchange_info['card_id']) + # objective_player_card = find_card_by_id( + # exchange_info['objective_card_id']) - process_card_exchange(game, player, objective_player, - player_card, objective_player_card) + # process_card_exchange(game, player, objective_player, + # player_card, objective_player_card) case ActionType.CHANGE_PLACES: process_change_places_card(game, player, objective_player) From b3925e1c5b955556d4486b2e68bdc95d339a6530 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 21:16:39 -0300 Subject: [PATCH 199/224] ahora anda to --- app/routers/games/action_functions.py | 19 ++++++++++--------- app/routers/websockets/services.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index f381535..cade71c 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -158,22 +158,23 @@ def process_better_run_card(game: Game, player: Player, objective_player: Player @db_session def process_card_exchange(player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): - player.hand.remove(player_card) - player.hand.add(objective_player_card) - - objective_player.hand.remove(objective_player_card) - objective_player.hand.add(player_card) - - if (player.rol == PlayerRol.THE_THING and player_card.type == CardSubtype.CONTAGION): + print(player_card.type) + if (player.rol == PlayerRol.THE_THING and player_card.subtype == CardSubtype.CONTAGION): if objective_player.rol != PlayerRol.INFECTED: objective_player.rol = PlayerRol.INFECTED - objective_player.game_last_infected = objective_player.game + objective_player.game_last_infected = objective_player.game - elif (objective_player.rol == PlayerRol.THE_THING and objective_player_card.type == CardSubtype.CONTAGION): + elif (objective_player.rol == PlayerRol.THE_THING and objective_player_card.subtype == CardSubtype.CONTAGION): if player.rol != PlayerRol.INFECTED: player.rol = PlayerRol.INFECTED player.game_last_infected = player.game + player.hand.remove(player_card) + player.hand.add(objective_player_card) + + objective_player.hand.remove(objective_player_card) + objective_player.hand.add(player_card) + # enviar evento de intercambio de carta diff --git a/app/routers/websockets/services.py b/app/routers/websockets/services.py index ba0ceba..f1906ad 100644 --- a/app/routers/websockets/services.py +++ b/app/routers/websockets/services.py @@ -67,7 +67,7 @@ async def handle_message(data, player_id): elif message == 'det' or message == 'determinación' or message == 'resolute': apply_cheat(game_name, player_id, range(43, 48)) await send_event_cheat_used(player_id) - + elif message == 'vig' or message == 'vigila_tus_espaldas' or message == 'watch_your_back': apply_cheat(game_name, player_id, range(53, 55)) await send_event_cheat_used(player_id) From 1193426c4529995aec57e07a675a0d7365fbbd50 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 21:21:18 -0300 Subject: [PATCH 200/224] hotfix --- app/routers/games/action_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 3c8af0b..4d5547a 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -195,7 +195,7 @@ def process_card_exchange(player: Player, objective_player: Player, player_card: objective_player.hand.add(player_card) asyncio.ensure_future(send_players_exchagnge_event( - game, player.id, objective_player.id)) + player.game, player.id, objective_player.id)) @db_session From d2a90dc69bb0bda98faf5bee0e25a1a72c2481ab Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sat, 11 Nov 2023 21:25:49 -0300 Subject: [PATCH 201/224] hotfix en serio --- app/routers/games/action_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 4d5547a..6f3eefc 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -177,7 +177,7 @@ def process_better_run_card(game: Game, player: Player, objective_player: Player @db_session -def process_card_exchange(player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): +def process_card_exchange(game : Game ,player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): if (player.rol == PlayerRol.THE_THING and player_card.subtype == CardSubtype.CONTAGION): if objective_player.rol != PlayerRol.INFECTED: objective_player.rol = PlayerRol.INFECTED @@ -195,7 +195,7 @@ def process_card_exchange(player: Player, objective_player: Player, player_card: objective_player.hand.add(player_card) asyncio.ensure_future(send_players_exchagnge_event( - player.game, player.id, objective_player.id)) + game, player.id, objective_player.id)) @db_session From c9dde3172f9dd6424f83289eef698633767c22ef Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Sat, 11 Nov 2023 21:28:12 -0300 Subject: [PATCH 202/224] correccion en card-interchange-response --- app/routers/games/games.py | 18 +++++++++--------- app/routers/games/schemas.py | 2 -- app/routers/games/services.py | 7 ++++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index b3c7514..9f9a97b 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -245,19 +245,19 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform utils.verify_if_interchange_response_can_be_done(game_name, game_data) services.card_interchange_response(game_name, game_data) - with db_session: - game: Game = find_game_by_name(game_name) + # with db_session: + # game: Game = find_game_by_name(game_name) - intention: Intention = get_intention_in_game(game_name) + # intention: Intention = get_intention_in_game(game_name) - player = find_player_by_id(intention.player.id) - objective_player = find_player_by_id(intention.objective_player.id) + # player = find_player_by_id(intention.player.id) + # objective_player = find_player_by_id(intention.objective_player.id) - player_card = find_card_by_id(intention.exchange_payload['card_id']) - objective_player_card = find_card_by_id(game_data.card_id) + # player_card = find_card_by_id(intention.exchange_payload['card_id']) + # objective_player_card = find_card_by_id(game_data.card_id) - process_card_exchange(game, player, objective_player, - player_card, objective_player_card) + # process_card_exchange(game, player, objective_player, + # player_card, objective_player_card) clean_intention_in_game(game_name) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 59b6c12..821bc02 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -151,8 +151,6 @@ class InterchangeInformationIn(BaseModel): player_id: int # ID jugador que recibe la intencion card_id: int # Card ID del jugador que recibe la intencion - objective_player_id: int # ID jugador que inicia la intencion - objective_card_id: int # Card ID del jugador que inicia la intencion class ResoluteExchangeIn(BaseModel): diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 76dcd58..3b4f5c3 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -14,7 +14,7 @@ from .panic_functions import * import random from app.routers.games import utils -from .intention import create_intention_in_game, ActionType +from .intention import create_intention_in_game, ActionType, get_intention_in_game def get_unstarted_games() -> List[GameResponse]: @@ -555,9 +555,10 @@ def register_card_exchange_intention(game_name: str, player_id: int, card_id: in @db_session def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): game: Game = find_game_by_name(game_name) - player: Player = find_player_by_id(game_data.objective_player_id) + intention: Intention = get_intention_in_game(game_name) + player: Player = intention.objective_player player_card: Card = cards_services.find_card_by_id( - game_data.objective_card_id) + intention.exchange_payload['card_id']) next_player: Player = find_player_by_id(game_data.player_id) next_player_card: Card = cards_services.find_card_by_id(game_data.card_id) From 2780855b6cc34835b8377e16408cfdd4516a56ac Mon Sep 17 00:00:00 2001 From: nehu Date: Sun, 12 Nov 2023 14:22:57 -0300 Subject: [PATCH 203/224] cambios en games, games/schemas, y /games/utils --- app/routers/games/games.py | 44 ++++++++++++++++++++++++------------ app/routers/games/schemas.py | 4 ++++ app/routers/games/utils.py | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 9f9a97b..c7f55ae 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -10,6 +10,8 @@ from ..cards.utils import get_card_name_by_id, get_card_type_by_id, is_flamethrower, is_whiskey from .services import finish_game from .intention import * +from .defense_functions import ActionType + router = APIRouter( @@ -242,29 +244,39 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent @router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): - utils.verify_if_interchange_response_can_be_done(game_name, game_data) - services.card_interchange_response(game_name, game_data) + #services.card_interchange_response(game_name, game_data) + intention: Intention = get_intention_in_game(game_name) + with db_session: + game: Game = find_game_by_name(game_name) + + print("\n\n\nIntention:", intention) - # with db_session: - # game: Game = find_game_by_name(game_name) + player = find_player_by_id(intention.player.id) + objective_player = find_player_by_id(intention.objective_player.id) - # intention: Intention = get_intention_in_game(game_name) + player_card = find_card_by_id(intention.exchange_payload['card_id']) + objective_player_card = find_card_by_id(game_data.card_id) - # player = find_player_by_id(intention.player.id) - # objective_player = find_player_by_id(intention.objective_player.id) + interchange_verify_data = { + "player_id": game_data.player_id, + "card_id": game_data.card_id, + "objective_player": objective_player.id, + "objective_player_card": objective_player_card.id + } + interchange_verify = InterchangeInformationVerify(**interchange_verify_data) - # player_card = find_card_by_id(intention.exchange_payload['card_id']) - # objective_player_card = find_card_by_id(game_data.card_id) + utils.verify_if_interchange_response_can_be_done(game_name, interchange_verify) + services.update_game_turn(game_name) - # process_card_exchange(game, player, objective_player, - # player_card, objective_player_card) + process_card_exchange(game, player, objective_player, + player_card, objective_player_card) clean_intention_in_game(game_name) json_msg = { "event": utils.Events.EXCHANGE_DONE, "player_name": get_player_name_by_id(game_data.player_id), - "objective_player_name": get_player_name_by_id(game_data.objective_player_id) + "objective_player_name": get_player_name_by_id(objective_player_card) } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) @@ -347,7 +359,7 @@ async def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): @router.post("/{game_name}/play-defense-card") async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): utils.verify_defense_card_can_be_played(game_name, defense_info) - + defense = False if defense_info.card_id: services.play_defense_card(game_name, defense_info) intention: Intention = get_intention_in_game(game_name) @@ -360,10 +372,12 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "action_type": intention.action_type } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + defense = True else: process_intention_in_game(game_name) - - clean_intention_in_game(game_name) + + if (defense or intention.action_type!=ActionType.EXCHANGE_OFFER): + clean_intention_in_game(game_name) @router.patch("/{game_name}/the-thing-end-game") diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 821bc02..ef8e5e2 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -152,6 +152,10 @@ class InterchangeInformationIn(BaseModel): player_id: int # ID jugador que recibe la intencion card_id: int # Card ID del jugador que recibe la intencion +class InterchangeInformationVerify(InterchangeInformationIn): + objective_player: int + objective_player_card: int + class ResoluteExchangeIn(BaseModel): model_config = ConfigDict(from_attributes=True) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 2bbd27a..04f2e1f 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -463,7 +463,7 @@ def verify_if_interchange_can_be_done(game_name: str, interchange_info: Intentio @db_session -def verify_if_interchange_response_can_be_done(game_name: str, game_data: InterchangeInformationIn): +def verify_if_interchange_response_can_be_done(game_name: str, game_data: InterchangeInformationVerify): game: Game = find_game_by_name(game_name) player: Player = find_player_by_id(game_data.player_id) player_card: Card = find_card_by_id(game_data.card_id) From 54908f20a2b2aceb53f3800a271dc97a2beb476d Mon Sep 17 00:00:00 2001 From: nehu Date: Sun, 12 Nov 2023 14:51:16 -0300 Subject: [PATCH 204/224] modificado schemas --- app/routers/games/games.py | 6 +++--- app/routers/games/schemas.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index c7f55ae..6e81b1e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -260,8 +260,8 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform interchange_verify_data = { "player_id": game_data.player_id, "card_id": game_data.card_id, - "objective_player": objective_player.id, - "objective_player_card": objective_player_card.id + "objective_player_id": objective_player.id, + "objective_card_id": objective_player_card.id } interchange_verify = InterchangeInformationVerify(**interchange_verify_data) @@ -359,10 +359,10 @@ async def card_resolute_exchange(game_name: str, game_data: ResoluteExchangeIn): @router.post("/{game_name}/play-defense-card") async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation): utils.verify_defense_card_can_be_played(game_name, defense_info) + intention: Intention = get_intention_in_game(game_name) defense = False if defense_info.card_id: services.play_defense_card(game_name, defense_info) - intention: Intention = get_intention_in_game(game_name) json_msg = { "event": utils.Events.DEFENSE_CARD_PLAYED, diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index ef8e5e2..3b036e7 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -153,8 +153,8 @@ class InterchangeInformationIn(BaseModel): card_id: int # Card ID del jugador que recibe la intencion class InterchangeInformationVerify(InterchangeInformationIn): - objective_player: int - objective_player_card: int + objective_player_id: int + objective_card_id: int class ResoluteExchangeIn(BaseModel): From 303587396c7d5a9564f39ac827c94d4beaf2d82e Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Sun, 12 Nov 2023 15:54:50 -0300 Subject: [PATCH 205/224] hotfix --- app/routers/games/games.py | 39 ++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 6e81b1e..d4abf1c 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -245,18 +245,16 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent @router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): #services.card_interchange_response(game_name, game_data) - intention: Intention = get_intention_in_game(game_name) with db_session: + intention: Intention = get_intention_in_game(game_name) game: Game = find_game_by_name(game_name) - print("\n\n\nIntention:", intention) player = find_player_by_id(intention.player.id) objective_player = find_player_by_id(intention.objective_player.id) player_card = find_card_by_id(intention.exchange_payload['card_id']) objective_player_card = find_card_by_id(game_data.card_id) - interchange_verify_data = { "player_id": game_data.player_id, "card_id": game_data.card_id, @@ -264,27 +262,44 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform "objective_card_id": objective_player_card.id } interchange_verify = InterchangeInformationVerify(**interchange_verify_data) - utils.verify_if_interchange_response_can_be_done(game_name, interchange_verify) services.update_game_turn(game_name) + + with db_session: + intention: Intention = get_intention_in_game(game_name) + game: Game = find_game_by_name(game_name) + + + player = find_player_by_id(intention.player.id) + objective_player = find_player_by_id(intention.objective_player.id) - process_card_exchange(game, player, objective_player, - player_card, objective_player_card) + player_card = find_card_by_id(intention.exchange_payload['card_id']) + objective_player_card = find_card_by_id(game_data.card_id) + + process_card_exchange(game, player, objective_player, + player_card, objective_player_card) + + print("\n\n\n LLEGUE 5 Intention:", intention.action_type) clean_intention_in_game(game_name) - json_msg = { - "event": utils.Events.EXCHANGE_DONE, - "player_name": get_player_name_by_id(game_data.player_id), - "objective_player_name": get_player_name_by_id(objective_player_card) - } - await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + print("\n\n\n LLEGUE 6 Intention:") + with db_session: + json_msg = { + "event": utils.Events.EXCHANGE_DONE, + "player_name": get_player_name_by_id(game_data.player_id), + "objective_player_name": get_player_name_by_id(objective_player.id) + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + + print("\n\n\n LLEGUE 7 Intention:") with db_session: game = find_game_by_name(game_name) player_id_turn = select( p for p in game.players if p.position == game.turn).first().id + print("\n\n\n LLEGUE 8 Intention:", intention.action_type) json_msg = { "event": utils.Events.NEW_TURN, "next_player_name": get_player_name_by_id(player_id_turn), From 2457970c221ac00b121f0f8e53f3013645474a4f Mon Sep 17 00:00:00 2001 From: nehu Date: Sun, 12 Nov 2023 17:53:58 -0300 Subject: [PATCH 206/224] nuevo turno --- app/routers/games/games.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index d4abf1c..ac7198c 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -268,7 +268,6 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform with db_session: intention: Intention = get_intention_in_game(game_name) game: Game = find_game_by_name(game_name) - player = find_player_by_id(intention.player.id) objective_player = find_player_by_id(intention.objective_player.id) @@ -279,11 +278,8 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform process_card_exchange(game, player, objective_player, player_card, objective_player_card) - print("\n\n\n LLEGUE 5 Intention:", intention.action_type) - clean_intention_in_game(game_name) - print("\n\n\n LLEGUE 6 Intention:") with db_session: json_msg = { "event": utils.Events.EXCHANGE_DONE, @@ -292,14 +288,11 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - print("\n\n\n LLEGUE 7 Intention:") - with db_session: game = find_game_by_name(game_name) player_id_turn = select( p for p in game.players if p.position == game.turn).first().id - print("\n\n\n LLEGUE 8 Intention:", intention.action_type) json_msg = { "event": utils.Events.NEW_TURN, "next_player_name": get_player_name_by_id(player_id_turn), @@ -393,6 +386,21 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation if (defense or intention.action_type!=ActionType.EXCHANGE_OFFER): clean_intention_in_game(game_name) + + if (defense and intention.action_type==ActionType.EXCHANGE_OFFER): + services.update_game_turn(game_name) + with db_session: + game = find_game_by_name(game_name) + player_id_turn = select( + p for p in game.players if p.position == game.turn).first().id + + json_msg = { + "event": utils.Events.NEW_TURN, + "next_player_name": get_player_name_by_id(player_id_turn), + "next_player_id": player_id_turn, + "round_direction": game.round_direction + } + await player_connections.send_event_to_all_players_in_game(game_name, json_msg) @router.patch("/{game_name}/the-thing-end-game") From 5218033ff6334bc7d6da8cd73d8a1587187b5b73 Mon Sep 17 00:00:00 2001 From: nehu Date: Sun, 12 Nov 2023 23:38:05 -0300 Subject: [PATCH 207/224] cambios para la defensa con Aterrador --- app/routers/games/games.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index ac7198c..59ddc6e 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -379,6 +379,9 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "objective_player_id": intention.objective_player.id, "action_type": intention.action_type } + if 73<=defense_info.card_id and defense_info.card_id <= 76: + card_name = get_card_name_by_id(intention.exchange_payload["card_id"]) + json_msg["card_to_exchange"] = card_name await player_connections.send_event_to_all_players_in_game(game_name, json_msg) defense = True else: From 44278cec36d15d70f86fa2ddb20203ba04456971 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 01:54:19 -0300 Subject: [PATCH 208/224] autopep8 y borrar comentarios --- app/routers/games/action_functions.py | 4 +-- app/routers/games/games.py | 44 +++++++++++---------------- app/routers/games/intention.py | 10 ------ app/routers/games/schemas.py | 4 ++- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 7838a5b..c871852 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -90,7 +90,7 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play # Reacomodo el turno if game.turn != 0 and objective_player.position < player.position: - game.turn = game.turn - 1 + game.turn = game.turn - 1 asyncio.ensure_future(send_players_eliminated_event(game=game, killer_id=player.id, @@ -183,7 +183,7 @@ def process_better_run_card(game: Game, player: Player, objective_player: Player @db_session -def process_card_exchange(game : Game ,player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): +def process_card_exchange(game: Game, player: Player, objective_player: Player, player_card: Card, objective_player_card: Card): if (player.rol == PlayerRol.THE_THING and player_card.subtype == CardSubtype.CONTAGION): if objective_player.rol != PlayerRol.INFECTED: objective_player.rol = PlayerRol.INFECTED diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 59ddc6e..4a33805 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -13,7 +13,6 @@ from .defense_functions import ActionType - router = APIRouter( prefix="/games", tags=["games"], @@ -227,28 +226,16 @@ async def intention_to_interchange_card(game_name: str, interchange_info: Intent services.register_card_exchange_intention( game_name, interchange_info.player_id, interchange_info.card_id, objective_player_id) - - # with db_session: - # objective_player = find_player_by_id(objective_player_id) - - # json_msg = { - # "event": "exchange_intention", - # "player_id": interchange_info.player_id, - # "player_name": get_player_name_by_id(interchange_info.player_id), - # "card_to_exchange": interchange_info.card_id, - # "defense_cards": player_cards_to_defend_himself(ActionType.EXCHANGE_OFFER, objective_player) - # } - # await player_connections.send_event_to(objective_player_id, json_msg) + return {"message": "Card interchange intention terminated."} @router.patch("/{game_name}/card-interchange-response", status_code=status.HTTP_200_OK) async def card_interchange_response(game_name: str, game_data: InterchangeInformationIn): - #services.card_interchange_response(game_name, game_data) + # services.card_interchange_response(game_name, game_data) with db_session: intention: Intention = get_intention_in_game(game_name) game: Game = find_game_by_name(game_name) - player = find_player_by_id(intention.player.id) objective_player = find_player_by_id(intention.objective_player.id) @@ -261,10 +248,12 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform "objective_player_id": objective_player.id, "objective_card_id": objective_player_card.id } - interchange_verify = InterchangeInformationVerify(**interchange_verify_data) - utils.verify_if_interchange_response_can_be_done(game_name, interchange_verify) + interchange_verify = InterchangeInformationVerify( + **interchange_verify_data) + utils.verify_if_interchange_response_can_be_done( + game_name, interchange_verify) services.update_game_turn(game_name) - + with db_session: intention: Intention = get_intention_in_game(game_name) game: Game = find_game_by_name(game_name) @@ -276,8 +265,8 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform objective_player_card = find_card_by_id(game_data.card_id) process_card_exchange(game, player, objective_player, - player_card, objective_player_card) - + player_card, objective_player_card) + clean_intention_in_game(game_name) with db_session: @@ -379,23 +368,24 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "objective_player_id": intention.objective_player.id, "action_type": intention.action_type } - if 73<=defense_info.card_id and defense_info.card_id <= 76: - card_name = get_card_name_by_id(intention.exchange_payload["card_id"]) + if 73 <= defense_info.card_id and defense_info.card_id <= 76: + card_name = get_card_name_by_id( + intention.exchange_payload["card_id"]) json_msg["card_to_exchange"] = card_name await player_connections.send_event_to_all_players_in_game(game_name, json_msg) defense = True else: process_intention_in_game(game_name) - - if (defense or intention.action_type!=ActionType.EXCHANGE_OFFER): + + if (defense or intention.action_type != ActionType.EXCHANGE_OFFER): clean_intention_in_game(game_name) - - if (defense and intention.action_type==ActionType.EXCHANGE_OFFER): + + if (defense and intention.action_type == ActionType.EXCHANGE_OFFER): services.update_game_turn(game_name) with db_session: game = find_game_by_name(game_name) player_id_turn = select( - p for p in game.players if p.position == game.turn).first().id + p for p in game.players if p.position == game.turn).first().id json_msg = { "event": utils.Events.NEW_TURN, diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index 03c664f..a1039e7 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -51,16 +51,6 @@ def process_intention_in_game(game_name) -> Intention: objective_player = intention.objective_player match intention.action_type: - # case ActionType.EXCHANGE_OFFER: - # exchange_info = intention.exchange_payload - - # player_card = find_card_by_id(exchange_info['card_id']) - # objective_player_card = find_card_by_id( - # exchange_info['objective_card_id']) - - # process_card_exchange(game, player, objective_player, - # player_card, objective_player_card) - case ActionType.CHANGE_PLACES: process_change_places_card(game, player, objective_player) diff --git a/app/routers/games/schemas.py b/app/routers/games/schemas.py index 3b036e7..3b726b2 100644 --- a/app/routers/games/schemas.py +++ b/app/routers/games/schemas.py @@ -152,6 +152,7 @@ class InterchangeInformationIn(BaseModel): player_id: int # ID jugador que recibe la intencion card_id: int # Card ID del jugador que recibe la intencion + class InterchangeInformationVerify(InterchangeInformationIn): objective_player_id: int objective_card_id: int @@ -193,7 +194,8 @@ class OneTwoEffectIn(BaseModel): player_id: int objective_player_id: int + class TheThingEndGameIn(BaseModel): model_config = ConfigDict(from_attributes=True) - player_id: int \ No newline at end of file + player_id: int From e6fcd5fd61a2683867aaff5e8ce9612910198585 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 10:56:42 -0300 Subject: [PATCH 209/224] arreglo intercambio y fin de partida --- app/routers/games/games.py | 2 +- app/routers/games/intention.py | 7 ++++--- app/routers/games/services.py | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 4a33805..e563dac 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -272,7 +272,7 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform with db_session: json_msg = { "event": utils.Events.EXCHANGE_DONE, - "player_name": get_player_name_by_id(game_data.player_id), + "player_name": get_player_name_by_id(player.id), "objective_player_name": get_player_name_by_id(objective_player.id) } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index a1039e7..8369e46 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -13,10 +13,11 @@ ) -async def send_intention_event(action_type: ActionType, objective_player: Player): +async def send_intention_event(action_type: ActionType, player: Player, objective_player: Player): json_msg = { "event": action_type, - "defense_cards": player_cards_to_defend_himself(action_type, objective_player) + "defense_cards": player_cards_to_defend_himself(action_type, objective_player), + "player_id": player.id } await player_connections.send_event_to(objective_player.id, json_msg) @@ -33,7 +34,7 @@ def create_intention_in_game(game: Game, action_type: ActionType, player: Player ) game.intention = intention - asyncio.ensure_future(send_intention_event(action_type, objective_player)) + asyncio.ensure_future(send_intention_event(action_type, player, objective_player)) return intention diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 1121e0e..f7b0cf7 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -404,7 +404,7 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu @db_session def get_game_result(name: str) -> GameResult: - game: Game = find_game_by_name(name) + game : Game = find_game_by_name(name) if game.status != GameStatus.ENDED: raise HTTPException( @@ -435,8 +435,8 @@ def get_game_result(name: str) -> GameResult: Pierden los eliminados y el último infectado.''' winners = game.players.select( lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] - losers = game.players.select( - lambda p: p.rol == PlayerRol.ELIMINATED)[:] + losers = list(game.players.select( + lambda p: p.rol == PlayerRol.ELIMINATED)[:]) losers.append(game.last_infected) # elif the_thing_declared_a_wrong_victory(game): From fafc2fbfd961183386ba0ad195af77aa7d8233a6 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 11:27:25 -0300 Subject: [PATCH 210/224] fix mensaje fin de partida --- app/routers/games/services.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index f7b0cf7..60ce89c 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -433,11 +433,12 @@ def get_game_result(name: str) -> GameResult: elif no_human_remains(game): reason = '''No queda ningún Humano en la partida. Todos fueron infectados o eliminados. Pierden los eliminados y el último infectado.''' - winners = game.players.select( - lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:] + winners = list(game.players.select( + lambda p: p.rol in [PlayerRol.THE_THING, PlayerRol.INFECTED])[:]) losers = list(game.players.select( lambda p: p.rol == PlayerRol.ELIMINATED)[:]) losers.append(game.last_infected) + winners.remove(game.last_infected) # elif the_thing_declared_a_wrong_victory(game): else: From b03e2bac20ac37d8e0e253ebc8a4283dd8983ad1 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 13:09:14 -0300 Subject: [PATCH 211/224] =?UTF-8?q?finalizaci=C3=B3n=20de=20partida=20auto?= =?UTF-8?q?=20y=20cambio=20en=20intention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/games.py | 2 ++ app/routers/games/intention.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index e563dac..f4276e0 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -376,6 +376,8 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation defense = True else: process_intention_in_game(game_name) + if is_the_game_finished(game_name): + await finish_game(game_name) if (defense or intention.action_type != ActionType.EXCHANGE_OFFER): clean_intention_in_game(game_name) diff --git a/app/routers/games/intention.py b/app/routers/games/intention.py index 8369e46..381d77e 100644 --- a/app/routers/games/intention.py +++ b/app/routers/games/intention.py @@ -25,6 +25,7 @@ async def send_intention_event(action_type: ActionType, player: Player, objectiv @db_session def create_intention_in_game(game: Game, action_type: ActionType, player: Player, objective_player: Player, exchange_payload={}) -> Intention: + clean_intention_in_game(game.name) intention = Intention( action_type=action_type, player=player, @@ -67,7 +68,10 @@ def process_intention_in_game(game_name) -> Intention: @db_session def clean_intention_in_game(game_name): game: Game = find_game_by_name(game_name) - Intention.get(game=game).delete() + intention = game.intention + # Intention.get(game=game).delete() + if intention: + intention.delete() @db_session From fa94fedf99fcd2eefd4df064d0701e928c225950 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 14:47:07 -0300 Subject: [PATCH 212/224] cambio en el lanzallamas --- app/routers/games/action_functions.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 01eaeda..87817b7 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -70,6 +70,7 @@ async def send_seduction_done_event(player_id: int, objective_player_id: int): await player_connections.send_event_to(player_id, json_msg) await player_connections.send_event_to(objective_player_id, json_msg) + async def send_suspicious_card_played_event(player_id: int, card_name: str): json_msg = { "event": Events.SUSPICIOUS_CARD_PLAYED, @@ -90,16 +91,16 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play game.discard_deck.add(c) objective_player.hand.remove(c) + # Reacomodo el turno + if game.turn != 0 and objective_player.position < player.position: + game.turn = game.turn - 1 + # Reacomodo las posiciones for p in game.players: if p.position > objective_player.position: p.position -= 1 objective_player.position = -1 - # Reacomodo el turno - if game.turn != 0 and objective_player.position < player.position: - game.turn = game.turn - 1 - asyncio.ensure_future(send_players_eliminated_event(game=game, killer_id=player.id, killer_name=player.name, @@ -135,7 +136,8 @@ def process_suspicious_card(game: Game, player: Player, objective_player: Player description=random_card.description ) - asyncio.ensure_future(send_suspicious_card_played_event(player.id, random_card.name)) + asyncio.ensure_future(send_suspicious_card_played_event( + player.id, random_card.name)) return result From 4afca19fbb7ade62c64811ddfb715b73a7ea9d51 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 14:59:03 -0300 Subject: [PATCH 213/224] =?UTF-8?q?Evento=20an=C3=A1lisis=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/action_functions.py | 13 +++++++++++++ app/routers/games/utils.py | 1 + 2 files changed, 14 insertions(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 87817b7..2c9fe8e 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -80,6 +80,16 @@ async def send_suspicious_card_played_event(player_id: int, card_name: str): await asyncio.sleep(5) +async def send_analysis_card_played_event(player_id: int, player_name: str, cards: list[CardResponse]): + json_msg = { + "event": Events.ANALYSIS_CARD_PLAYED, + "cards": cards, + "player_name": player_name + } + await player_connections.send_event_to(player_id, json_msg) + await asyncio.sleep(5) + + @db_session def process_flamethrower_card(game: Game, player: Player, objective_player: Player): objective_player.rol = PlayerRol.ELIMINATED @@ -119,6 +129,9 @@ def process_analysis_card(game: Game, player: Player, objective_player: Player): description=c.description ) for c in objective_player.hand] + asyncio.ensure_future(send_analysis_card_played_event(player.id, + player.name, list(result['cards']))) + result['objective_player_name'] = objective_player.name return result diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 5c5a49c..cc4b98b 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -52,6 +52,7 @@ class Events(str, Enum): INTERCHANGE_INTENTION_DONE = 'interchange_intention_done' DEFENSE_CARD_PLAYED = "defense_card_played" SUSPICIOUS_CARD_PLAYED = "suspicious_card_played" + ANALYSIS_CARD_PLAYED = "analysis_card_played" async def send_played_card_event(game_name: str, player_id: int, card_id: int): From ede461ef92f973667b142ba2478adbedebfce632 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 15:23:39 -0300 Subject: [PATCH 214/224] fix --- app/routers/games/action_functions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 2c9fe8e..d45d4d6 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -79,8 +79,7 @@ async def send_suspicious_card_played_event(player_id: int, card_name: str): await player_connections.send_event_to(player_id, json_msg) await asyncio.sleep(5) - -async def send_analysis_card_played_event(player_id: int, player_name: str, cards: list[CardResponse]): +async def send_analysis_card_played_event(player_id: int, player_name: str, cards: list[str]): json_msg = { "event": Events.ANALYSIS_CARD_PLAYED, "cards": cards, @@ -121,17 +120,11 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play @db_session def process_analysis_card(game: Game, player: Player, objective_player: Player): result = {} - result['cards'] = [CardResponse(id=c.id, - number=c.number, - type=c.type, - subtype=c.subtype, - name=c.name, - description=c.description - ) for c in objective_player.hand] - - asyncio.ensure_future(send_analysis_card_played_event(player.id, - player.name, list(result['cards']))) + result['cards'] = [c.name for c in objective_player.hand] + asyncio.ensure_future(send_analysis_card_played_event(player.id, + player.name, result['cards'])) + result['objective_player_name'] = objective_player.name return result From fb9f3b6cc32d10de6fb2cef715ed0ecb7e286085 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 15:38:20 -0300 Subject: [PATCH 215/224] agrego ", " a la lista --- app/routers/games/action_functions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index d45d4d6..33f93e6 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -121,11 +121,15 @@ def process_flamethrower_card(game: Game, player: Player, objective_player: Play def process_analysis_card(game: Game, player: Player, objective_player: Player): result = {} result['cards'] = [c.name for c in objective_player.hand] + result['objective_player_name'] = objective_player.name + + for i in range (len(result['cards'] - 1)): + result["cards"][i] = result["cards"][i] + ", " asyncio.ensure_future(send_analysis_card_played_event(player.id, - player.name, result['cards'])) + objective_player.name, result['cards'])) + - result['objective_player_name'] = objective_player.name return result From 67bc074a5aa1593b977726fc3a2e9fd161952665 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 15:41:46 -0300 Subject: [PATCH 216/224] fix triste --- app/routers/games/action_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 33f93e6..c574b0b 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -123,7 +123,7 @@ def process_analysis_card(game: Game, player: Player, objective_player: Player): result['cards'] = [c.name for c in objective_player.hand] result['objective_player_name'] = objective_player.name - for i in range (len(result['cards'] - 1)): + for i in range (len(result['cards']) - 1): result["cards"][i] = result["cards"][i] + ", " asyncio.ensure_future(send_analysis_card_played_event(player.id, From 073c25bcb7e3d8489e9f21c4c8e183a993ac8ecc Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 15:50:41 -0300 Subject: [PATCH 217/224] otro fix enorme --- app/routers/games/action_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index c574b0b..13f9b23 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -123,8 +123,8 @@ def process_analysis_card(game: Game, player: Player, objective_player: Player): result['cards'] = [c.name for c in objective_player.hand] result['objective_player_name'] = objective_player.name - for i in range (len(result['cards']) - 1): - result["cards"][i] = result["cards"][i] + ", " + for i in range (1 ,(len(result['cards']))): + result["cards"][i] = " " + result["cards"][i] asyncio.ensure_future(send_analysis_card_played_event(player.id, objective_player.name, result['cards'])) From 90f680a9c4fd2afe872d294d1286bc91d96ea623 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 16:12:30 -0300 Subject: [PATCH 218/224] fix eliminar partida --- app/routers/games/services.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routers/games/services.py b/app/routers/games/services.py index 60ce89c..9a287c6 100644 --- a/app/routers/games/services.py +++ b/app/routers/games/services.py @@ -14,7 +14,7 @@ from .panic_functions import * import random from app.routers.games import utils -from .intention import create_intention_in_game, ActionType, get_intention_in_game +from .intention import clean_intention_in_game, create_intention_in_game, ActionType, get_intention_in_game def get_unstarted_games() -> List[GameResponse]: @@ -109,6 +109,8 @@ def delete_game(game_name: str): detail=f"The game is not ended." ) + clean_intention_in_game(game_name) + for player in game.players: player.hand.clear() @@ -404,7 +406,7 @@ def draw_card(game_name: str, game_data: DrawInformationIn) -> DrawInformationOu @db_session def get_game_result(name: str) -> GameResult: - game : Game = find_game_by_name(name) + game: Game = find_game_by_name(name) if game.status != GameStatus.ENDED: raise HTTPException( From 817b3e0f859492956c063fabeec06b75035f7d0a Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 19:23:59 -0300 Subject: [PATCH 219/224] nuevo evento cuando se infecta --- app/routers/games/action_functions.py | 27 +++++++++++++++++++++------ app/routers/games/utils.py | 1 + 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 13f9b23..7a59dd9 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -79,6 +79,7 @@ async def send_suspicious_card_played_event(player_id: int, card_name: str): await player_connections.send_event_to(player_id, json_msg) await asyncio.sleep(5) + async def send_analysis_card_played_event(player_id: int, player_name: str, cards: list[str]): json_msg = { "event": Events.ANALYSIS_CARD_PLAYED, @@ -89,6 +90,17 @@ async def send_analysis_card_played_event(player_id: int, player_name: str, card await asyncio.sleep(5) +async def send_infected_event(infected_id: int, infected_name: str, the_thing_id: int, the_thing_name: str): + json_msg = { + "event": Events.NEW_INFECTED, + "infected_id": infected_id, + "infected_name": infected_name, + "the_thing_id": the_thing_id, + "the_thing_name": the_thing_name + } + await player_connections.send_event_to(infected_id, json_msg) + + @db_session def process_flamethrower_card(game: Game, player: Player, objective_player: Player): objective_player.rol = PlayerRol.ELIMINATED @@ -123,13 +135,12 @@ def process_analysis_card(game: Game, player: Player, objective_player: Player): result['cards'] = [c.name for c in objective_player.hand] result['objective_player_name'] = objective_player.name - for i in range (1 ,(len(result['cards']))): - result["cards"][i] = " " + result["cards"][i] + for i in range(1, (len(result['cards']))): + result["cards"][i] = " " + result["cards"][i] + + asyncio.ensure_future(send_analysis_card_played_event(player.id, + objective_player.name, result['cards'])) - asyncio.ensure_future(send_analysis_card_played_event(player.id, - objective_player.name, result['cards'])) - - return result @@ -210,11 +221,15 @@ def process_card_exchange(game: Game, player: Player, objective_player: Player, if objective_player.rol != PlayerRol.INFECTED: objective_player.rol = PlayerRol.INFECTED objective_player.game_last_infected = objective_player.game + asyncio.ensure_future(send_infected_event( + objective_player.id, objective_player.name, player.id, player.name)) elif (objective_player.rol == PlayerRol.THE_THING and objective_player_card.subtype == CardSubtype.CONTAGION): if player.rol != PlayerRol.INFECTED: player.rol = PlayerRol.INFECTED player.game_last_infected = player.game + asyncio.ensure_future(send_infected_event( + player.id, player.name, objective_player.id, objective_player.name)) player.hand.remove(player_card) player.hand.add(objective_player_card) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index cc4b98b..87dee83 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -53,6 +53,7 @@ class Events(str, Enum): DEFENSE_CARD_PLAYED = "defense_card_played" SUSPICIOUS_CARD_PLAYED = "suspicious_card_played" ANALYSIS_CARD_PLAYED = "analysis_card_played" + NEW_INFECTED = "new_infected" async def send_played_card_event(game_name: str, player_id: int, card_id: int): From 518ed2cd0bae53fbe9ccc4c93bb97cba19028eab Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 20:22:29 -0300 Subject: [PATCH 220/224] =?UTF-8?q?validaci=C3=B3n=20en=20el=20discard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/games/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routers/games/utils.py b/app/routers/games/utils.py index 87dee83..fd3def0 100644 --- a/app/routers/games/utils.py +++ b/app/routers/games/utils.py @@ -299,8 +299,7 @@ def verify_discard_can_be_done(game_name: str, game_data: DiscardInformationIn): detail="It's not the turn of the player" ) if player.rol == PlayerRol.INFECTED and card.name == '¡Infectado!': - infected_count = select(count(c) - for c in player.hand if c.name == '¡Infectado!') + infected_count = select(c for c in player.hand if c.name == '¡Infectado!').count() if infected_count <= 1: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, From 221c281ca7e5840e7490f7ddd080e9378b601576 Mon Sep 17 00:00:00 2001 From: Anelio Alvarez Date: Mon, 13 Nov 2023 20:37:31 -0300 Subject: [PATCH 221/224] tests hotfix: se corrige el mock de FakeGame agregando atributo intention --- app/tests/game_tests/test_delete_of_games.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tests/game_tests/test_delete_of_games.py b/app/tests/game_tests/test_delete_of_games.py index 6bb6e57..190007a 100644 --- a/app/tests/game_tests/test_delete_of_games.py +++ b/app/tests/game_tests/test_delete_of_games.py @@ -34,6 +34,7 @@ def __init__( self.turn = turn self.status = status self.round_direction = round_direction + self.intention = None def delete(a): pass From 4912ead18cdbce3eb7e0197c7ebcd4413841bac3 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Mon, 13 Nov 2023 20:48:58 -0300 Subject: [PATCH 222/224] cambio en el envio de mensaje infectado --- app/routers/games/action_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routers/games/action_functions.py b/app/routers/games/action_functions.py index 7a59dd9..091c728 100644 --- a/app/routers/games/action_functions.py +++ b/app/routers/games/action_functions.py @@ -99,6 +99,7 @@ async def send_infected_event(infected_id: int, infected_name: str, the_thing_id "the_thing_name": the_thing_name } await player_connections.send_event_to(infected_id, json_msg) + await player_connections.send_event_to(the_thing_id, json_msg) @db_session From dd24d3256afd879bfb04ba6e937ff2848df7d391 Mon Sep 17 00:00:00 2001 From: nehu Date: Mon, 13 Nov 2023 21:57:22 -0300 Subject: [PATCH 223/224] sleeps --- app/routers/games/games.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index f4276e0..42ca5ed 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -289,6 +289,7 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform "round_direction": game.round_direction } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) + asyncio.sleep(1) return {"message": "Card interchange terminated."} @@ -395,6 +396,7 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "next_player_id": player_id_turn, "round_direction": game.round_direction } + asyncio.sleep(1) await player_connections.send_event_to_all_players_in_game(game_name, json_msg) From 86e907fc75bee03898d9454d42332e3fa5d2eee2 Mon Sep 17 00:00:00 2001 From: ezeluduena Date: Tue, 14 Nov 2023 00:03:00 -0300 Subject: [PATCH 224/224] chau sleep --- app/routers/games/games.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routers/games/games.py b/app/routers/games/games.py index 42ca5ed..f4276e0 100644 --- a/app/routers/games/games.py +++ b/app/routers/games/games.py @@ -289,7 +289,6 @@ async def card_interchange_response(game_name: str, game_data: InterchangeInform "round_direction": game.round_direction } await player_connections.send_event_to_all_players_in_game(game_name, json_msg) - asyncio.sleep(1) return {"message": "Card interchange terminated."} @@ -396,7 +395,6 @@ async def play_defense_card(game_name: str, defense_info: PlayDefenseInformation "next_player_id": player_id_turn, "round_direction": game.round_direction } - asyncio.sleep(1) await player_connections.send_event_to_all_players_in_game(game_name, json_msg)