From a8f63fba627837b6d6a1e62dbfe4fbafb8b8f94b Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Fri, 29 Sep 2023 18:02:33 +0100 Subject: [PATCH 1/3] Add a test for start game --- ...faction_game_alter_player_game_and_more.py | 29 ++++++++ backend/rorapp/models/faction.py | 2 +- backend/rorapp/models/player.py | 2 +- backend/rorapp/models/senator.py | 2 +- backend/rorapp/models/title.py | 2 +- backend/rorapp/tests/__init__.py | 3 +- backend/rorapp/tests/start_game_tests.py | 66 +++++++++++++++++++ .../tests/{user_viewset.py => user_tests.py} | 22 +++---- 8 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 backend/rorapp/migrations/0035_alter_faction_game_alter_player_game_and_more.py create mode 100644 backend/rorapp/tests/start_game_tests.py rename backend/rorapp/tests/{user_viewset.py => user_tests.py} (57%) diff --git a/backend/rorapp/migrations/0035_alter_faction_game_alter_player_game_and_more.py b/backend/rorapp/migrations/0035_alter_faction_game_alter_player_game_and_more.py new file mode 100644 index 00000000..2820707c --- /dev/null +++ b/backend/rorapp/migrations/0035_alter_faction_game_alter_player_game_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.2 on 2023-09-29 16:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rorapp', '0034_senatoractionlog_rename_notification_actionlog_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='faction', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='factions', to='rorapp.game'), + ), + migrations.AlterField( + model_name='player', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='players', to='rorapp.game'), + ), + migrations.AlterField( + model_name='senator', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='senators', to='rorapp.game'), + ), + ] diff --git a/backend/rorapp/models/faction.py b/backend/rorapp/models/faction.py index e6aff434..d45dcc35 100644 --- a/backend/rorapp/models/faction.py +++ b/backend/rorapp/models/faction.py @@ -5,7 +5,7 @@ # Model for representing factions class Faction(models.Model): - game = models.ForeignKey(Game, on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='factions') position = models.IntegerField() player = models.ForeignKey(Player, blank=True, null=True, on_delete=models.SET_NULL) rank = models.IntegerField(blank=True, null=True) diff --git a/backend/rorapp/models/player.py b/backend/rorapp/models/player.py index 58311916..957bd7a5 100644 --- a/backend/rorapp/models/player.py +++ b/backend/rorapp/models/player.py @@ -7,7 +7,7 @@ # Model for representing game players class Player(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - game = models.ForeignKey(Game, on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='players') join_date = models.DateTimeField(default=timezone.now) # String representation of the game player, used in admin site diff --git a/backend/rorapp/models/senator.py b/backend/rorapp/models/senator.py index 2b076865..05696542 100644 --- a/backend/rorapp/models/senator.py +++ b/backend/rorapp/models/senator.py @@ -6,7 +6,7 @@ # Model for representing senators class Senator(models.Model): name = models.CharField(max_length=10) - game = models.ForeignKey(Game, on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='senators') faction = models.ForeignKey(Faction, blank=True, null=True, on_delete=models.SET_NULL) alive = models.BooleanField(default=True) code = models.IntegerField() diff --git a/backend/rorapp/models/title.py b/backend/rorapp/models/title.py index bf8decb2..ef80b004 100644 --- a/backend/rorapp/models/title.py +++ b/backend/rorapp/models/title.py @@ -6,7 +6,7 @@ # Model for representing titles class Title(models.Model): name = models.CharField(max_length=21) - senator = models.ForeignKey(Senator, on_delete=models.CASCADE) + senator = models.ForeignKey(Senator, on_delete=models.CASCADE, related_name='titles') start_step = models.ForeignKey(Step, on_delete=models.CASCADE, related_name='starting_title_set') end_step = models.ForeignKey(Step, on_delete=models.CASCADE, blank=True, null=True, related_name='ending_title_set') # Null means the title is active major_office = models.BooleanField(default=False) diff --git a/backend/rorapp/tests/__init__.py b/backend/rorapp/tests/__init__.py index 9f48164a..e8966a07 100644 --- a/backend/rorapp/tests/__init__.py +++ b/backend/rorapp/tests/__init__.py @@ -1 +1,2 @@ -from .user_viewset import UserViewSetTest +from .start_game_tests import StartGameTests +from .user_tests import UserTests diff --git a/backend/rorapp/tests/start_game_tests.py b/backend/rorapp/tests/start_game_tests.py new file mode 100644 index 00000000..570f00ae --- /dev/null +++ b/backend/rorapp/tests/start_game_tests.py @@ -0,0 +1,66 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from django.contrib.auth.models import User +from rorapp.models import Game, Player + + +class StartGameTests(TestCase): + + def setUp(self): + + # Set up data for the tests - 4 games with a variety of player counts for each + self.user1 = User.objects.create_user(username='User 1', password='Password') + self.user2 = User.objects.create_user(username='User 2', password='Password') + self.user3 = User.objects.create_user(username='User 3', password='Password') + self.user4 = User.objects.create_user(username='User 4', password='Password') + self.user5 = User.objects.create_user(username='User 5', password='Password') + self.user6 = User.objects.create_user(username='User 6', password='Password') + + self.gameA = Game.objects.create(name='Game 1', host=self.user1) + self.gameB = Game.objects.create(name='Game 2', host=self.user1) + self.gameC = Game.objects.create(name='Game 3', host=self.user1) + self.gameD = Game.objects.create(name='Game 4', host=self.user1) + + self.player1A = Player.objects.create(user=self.user1, game=self.gameA) + self.player2A = Player.objects.create(user=self.user2, game=self.gameA) + self.player3A = Player.objects.create(user=self.user3, game=self.gameA) + self.player4A = Player.objects.create(user=self.user4, game=self.gameA) + self.player5A = Player.objects.create(user=self.user5, game=self.gameA) + self.player6A = Player.objects.create(user=self.user6, game=self.gameA) + + self.player1B = Player.objects.create(user=self.user1, game=self.gameB) + self.player2B = Player.objects.create(user=self.user2, game=self.gameB) + self.player3B = Player.objects.create(user=self.user3, game=self.gameB) + self.player4B = Player.objects.create(user=self.user4, game=self.gameB) + self.player5B = Player.objects.create(user=self.user5, game=self.gameB) + + self.player1C = Player.objects.create(user=self.user1, game=self.gameC) + self.player2C = Player.objects.create(user=self.user2, game=self.gameC) + self.player3C = Player.objects.create(user=self.user3, game=self.gameC) + self.player4C = Player.objects.create(user=self.user4, game=self.gameC) + + self.player1D = Player.objects.create(user=self.user1, game=self.gameD) + self.player2D = Player.objects.create(user=self.user2, game=self.gameD) + self.player3D = Player.objects.create(user=self.user3, game=self.gameD) + + # Enables requests to API endpoints during testing + self.client = APIClient() + + def test_start_game(self): + + # Allow user 1 to make authenticated requests + self.client.force_authenticate(user=self.user1) + + for game in [self.gameA, self.gameB, self.gameC, self.gameD]: + + # Try to start game + response = self.client.post(f'/api/games/{game.id}/start-game/') + + # Check that the response is 200 OK + self.assertEqual(response.status_code, 200) + + # Check that the game has the correct number of factions + self.assertEqual(game.factions.count(), game.players.count()) + + # Check that the game has the correct number of senators + self.assertEqual(game.senators.count(), game.factions.count() * 3) diff --git a/backend/rorapp/tests/user_viewset.py b/backend/rorapp/tests/user_tests.py similarity index 57% rename from backend/rorapp/tests/user_viewset.py rename to backend/rorapp/tests/user_tests.py index 1ef9b318..4b442c9e 100644 --- a/backend/rorapp/tests/user_viewset.py +++ b/backend/rorapp/tests/user_tests.py @@ -4,19 +4,20 @@ from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken -class UserViewSetTest(TestCase): +class UserTests(TestCase): - # Set up data for the tests def setUp(self): + # Set up data for the tests # Use `create_user()` instead of `create()` to automatically hash the password - self.user1 = User.objects.create_user(username='testuser1', password='testpass1') - self.user2 = User.objects.create_user(username='testuser2', password='testpass2') + self.user1 = User.objects.create_user(username='User 1', password='Password 1') + self.user2 = User.objects.create_user(username='User 2', password='Password 2') # Enables requests to API endpoints during testing self.client = APIClient() def test_get_all_users(self): + # Get a token for the test user refresh = RefreshToken.for_user(self.user1) self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') @@ -27,14 +28,9 @@ def test_get_all_users(self): # Check that the response is 200 OK self.assertEqual(response.status_code, 200) - # Define the expected JSON response - expected_json = json.dumps([ - {'id': 1, 'username': self.user1.username}, - {'id': 2, 'username': self.user2.username} - ]) - # Convert the response content to JSON - response_json = response.content.decode('utf-8') + response_json = json.loads(response.content.decode('utf-8')) - # Check that the response JSON matches the expected JSON - self.assertEqual(json.loads(response_json), json.loads(expected_json)) + # Check that the response JSON matches expectations + self.assertEqual(response_json[0]['username'], self.user1.username) + self.assertEqual(response_json[1]['username'], self.user2.username) From b33220f4bce2c910233e4345b22451ccf2514370 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Fri, 29 Sep 2023 18:12:52 +0100 Subject: [PATCH 2/3] Improve the start game test Improve the start game test by making it check that one Temporary Rome Consul has been assigned. --- backend/rorapp/tests/start_game_tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/rorapp/tests/start_game_tests.py b/backend/rorapp/tests/start_game_tests.py index 570f00ae..da649949 100644 --- a/backend/rorapp/tests/start_game_tests.py +++ b/backend/rorapp/tests/start_game_tests.py @@ -1,7 +1,7 @@ from django.test import TestCase from rest_framework.test import APIClient from django.contrib.auth.models import User -from rorapp.models import Game, Player +from rorapp.models import Game, Player, Title class StartGameTests(TestCase): @@ -59,8 +59,10 @@ def test_start_game(self): # Check that the response is 200 OK self.assertEqual(response.status_code, 200) - # Check that the game has the correct number of factions + # Check that the game has the correct number of factions and senators self.assertEqual(game.factions.count(), game.players.count()) - - # Check that the game has the correct number of senators self.assertEqual(game.senators.count(), game.factions.count() * 3) + + # Check that a Temporary Rome Consul has been assigned + temp_rome_consuls = Title.objects.filter(senator__game=game, name='Temporary Rome Consul') + self.assertEqual(temp_rome_consuls.count(), 1) From fa237fef5daeb6cc9705d0c195d91c1558750a91 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Fri, 29 Sep 2023 20:20:00 +0100 Subject: [PATCH 3/3] Improve the start game test Improve the start game test to cover a wider range of scenarios and refactor the start game function to comply better with the single responsibility principle. --- backend/rorapp/functions/__init__.py | 1 + backend/rorapp/functions/face_mortality.py | 1 + .../rorapp/functions/select_faction_leader.py | 1 + backend/rorapp/functions/start_game.py | 224 ++++++++++++++++++ backend/rorapp/tests/start_game_tests.py | 138 ++++++++--- backend/rorapp/views/start_game.py | 166 +------------ 6 files changed, 334 insertions(+), 197 deletions(-) create mode 100644 backend/rorapp/functions/start_game.py diff --git a/backend/rorapp/functions/__init__.py b/backend/rorapp/functions/__init__.py index fdee8161..3c7bda7c 100644 --- a/backend/rorapp/functions/__init__.py +++ b/backend/rorapp/functions/__init__.py @@ -3,3 +3,4 @@ from .face_mortality import face_mortality from .rank_senators_and_factions import rank_senators_and_factions from .select_faction_leader import select_faction_leader +from .start_game import start_game diff --git a/backend/rorapp/functions/face_mortality.py b/backend/rorapp/functions/face_mortality.py index 3d936917..8a74ce5a 100644 --- a/backend/rorapp/functions/face_mortality.py +++ b/backend/rorapp/functions/face_mortality.py @@ -15,6 +15,7 @@ def face_mortality(game, faction, potential_action, step): Ready up for facing mortality. :return: a response with a message and a status code + :rtype: rest_framework.response.Response ''' messages_to_send = [] diff --git a/backend/rorapp/functions/select_faction_leader.py b/backend/rorapp/functions/select_faction_leader.py index 2bc2e5c7..0c8d8004 100644 --- a/backend/rorapp/functions/select_faction_leader.py +++ b/backend/rorapp/functions/select_faction_leader.py @@ -19,6 +19,7 @@ def select_faction_leader(game, faction, potential_action, step, data): :param data: the data, expects a `leader_id` for the senator selected as the faction leader :return: a response with a message and a status code + :rtype: rest_framework.response.Response ''' # Try to get the senator diff --git a/backend/rorapp/functions/start_game.py b/backend/rorapp/functions/start_game.py new file mode 100644 index 00000000..705da31f --- /dev/null +++ b/backend/rorapp/functions/start_game.py @@ -0,0 +1,224 @@ +import os +import json +import random +from django.conf import settings +from django.utils import timezone +from rest_framework.response import Response +from rest_framework.exceptions import NotFound, PermissionDenied +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync +from rorapp.functions import rank_senators_and_factions +from rorapp.models import Game, Player, Faction, Senator, Title, Turn, Phase, Step, PotentialAction, ActionLog, SenatorActionLog +from rorapp.serializers import GameDetailSerializer, TurnSerializer, PhaseSerializer, StepSerializer + + +def start_game(game_id, user, seed=None): + ''' + Start and setup an early republic scenario game. + + :param game: the game id + :param faction: the user starting the game + :param seed: seed for controlling "random" operations when testing + + :return: a response with a message and a status code + :rtype: rest_framework.response.Response + ''' + + try: + game, players = validate_game_start(game_id, user) + game, turn, phase, step = setup_game(game, players, seed) + return send_websocket_messages(game, turn, phase, step) + except (NotFound, PermissionDenied) as e: + return Response({"message": str(e)}, status=e.status_code) + + +def validate_game_start(game_id, user): + try: + game = Game.objects.get(id=game_id) + except Game.DoesNotExist: + raise NotFound("Game not found") + + if game.host.id != user.id: + raise PermissionDenied("Only the host can start the game") + + if Step.objects.filter(phase__turn__game__id=game.id).count() > 0: + raise PermissionDenied("Game has already started") + + players = Player.objects.filter(game__id=game.id) + if players.count() < 3: + raise PermissionDenied("Game must have at least 3 players to start") + + return game, players + + +def setup_game(game, players, seed): + factions = create_factions(game, players, seed) + senators = create_senators(game, players, seed) + assign_senators_to_factions(senators, factions) + set_game_as_started(game) + + turn, phase, step = create_turn_phase_step(game) + temp_rome_consul_title = assign_temp_rome_consul(senators, step, seed) + + create_action_logs(temp_rome_consul_title, step) + rank_senators_and_factions(game.id) + create_potential_actions(factions, step) + + return game, turn, phase, step + + +def create_factions(game, players, seed): + factions = [] + position = 1 + random.seed() if seed is None else random.seed(seed) + list_of_players = list(players) + random.shuffle(list_of_players) + + for player in list_of_players: + faction = Faction(game=game, position=position, player=player) + faction.save() # Save factions to DB + factions.append(faction) + position += 1 + return factions + + +def create_senators(game, factions, seed): + candidate_senators = load_candidate_senators(game) + + required_senator_count = len(factions) * 3 + + random.seed() if seed is None else random.seed(seed) + random.shuffle(candidate_senators) + + # Discard some candidates, leaving only the required number of senators + return candidate_senators[:required_senator_count] + + +def load_candidate_senators(game): + senator_json_path = os.path.join(settings.BASE_DIR, 'rorapp', 'presets', 'senator.json') + senators = [] + with open(senator_json_path, 'r') as file: + senators_dict = json.load(file) + + for senator_name, senator_data in senators_dict.items(): + if senator_data['scenario'] == 1: + senator = Senator( + name=senator_name, + game=game, + code=senator_data['code'], + military=senator_data['military'], + oratory=senator_data['oratory'], + loyalty=senator_data['loyalty'], + influence=senator_data['influence'] + ) + senators.append(senator) + return senators + + +def assign_senators_to_factions(senators, factions): + senator_iterator = iter(senators) + for faction in factions: + for _ in range(3): + senator = next(senator_iterator) + senator.faction = faction + senator.save() # Save senators to DB + + +def set_game_as_started(game): + game.start_date = timezone.now() + game.save() # Update game to DB + + +def create_turn_phase_step(game): + turn = Turn(index=1, game=game) + turn.save() + phase = Phase(name="Faction", index=0, turn=turn) + phase.save() + step = Step(index=0, phase=phase) + step.save() + return turn, phase, step + + +def assign_temp_rome_consul(senators, step, seed): + random.seed() if seed is None else random.seed(seed) + random.shuffle(senators) + + temp_rome_consul_title = Title( + name="Temporary Rome Consul", + senator=senators[0], + start_step=step, + major_office=True + ) + temp_rome_consul_title.save() + return temp_rome_consul_title + + +def create_action_logs(temp_rome_consul_title, step): + action_log = ActionLog( + index=0, + step=step, + type="temporary_rome_consul", + faction=temp_rome_consul_title.senator.faction, + data={"senator": temp_rome_consul_title.senator.id} + ) + action_log.save() + + senator_action_log = SenatorActionLog( + senator=temp_rome_consul_title.senator, + action_log=action_log + ) + senator_action_log.save() + + +def create_potential_actions(factions, step): + for faction in factions: + action = PotentialAction( + step=step, + faction=faction, + type="select_faction_leader", + required=True, + parameters=None + ) + action.save() + + +def send_websocket_messages(game, turn, phase, step): + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + f"game_{game.id}", + { + "type": "game_update", + "messages": [ + { + "operation": "update", + "instance": { + "class": "game", + "data": GameDetailSerializer(game).data + } + }, + { + "operation": "create", + "instance": { + "class": "turn", + "data": TurnSerializer(turn).data + } + }, + { + "operation": "create", + "instance": { + "class": "phase", + "data": PhaseSerializer(phase).data + } + }, + { + "operation": "create", + "instance": { + "class": "step", + "data": StepSerializer(step).data + } + } + ] + } + ) + + return Response({"message": "Game started"}, status=200) diff --git a/backend/rorapp/tests/start_game_tests.py b/backend/rorapp/tests/start_game_tests.py index da649949..69f25576 100644 --- a/backend/rorapp/tests/start_game_tests.py +++ b/backend/rorapp/tests/start_game_tests.py @@ -1,14 +1,14 @@ from django.test import TestCase from rest_framework.test import APIClient from django.contrib.auth.models import User -from rorapp.models import Game, Player, Title +from rorapp.models import Game, Player, Faction, Senator, Title +from rorapp.functions import start_game as start_game_direct class StartGameTests(TestCase): def setUp(self): - - # Set up data for the tests - 4 games with a variety of player counts for each + # Set up data for the tests - 6 users self.user1 = User.objects.create_user(username='User 1', password='Password') self.user2 = User.objects.create_user(username='User 2', password='Password') self.user3 = User.objects.create_user(username='User 3', password='Password') @@ -16,42 +16,41 @@ def setUp(self): self.user5 = User.objects.create_user(username='User 5', password='Password') self.user6 = User.objects.create_user(username='User 6', password='Password') - self.gameA = Game.objects.create(name='Game 1', host=self.user1) - self.gameB = Game.objects.create(name='Game 2', host=self.user1) - self.gameC = Game.objects.create(name='Game 3', host=self.user1) - self.gameD = Game.objects.create(name='Game 4', host=self.user1) - - self.player1A = Player.objects.create(user=self.user1, game=self.gameA) - self.player2A = Player.objects.create(user=self.user2, game=self.gameA) - self.player3A = Player.objects.create(user=self.user3, game=self.gameA) - self.player4A = Player.objects.create(user=self.user4, game=self.gameA) - self.player5A = Player.objects.create(user=self.user5, game=self.gameA) - self.player6A = Player.objects.create(user=self.user6, game=self.gameA) - - self.player1B = Player.objects.create(user=self.user1, game=self.gameB) - self.player2B = Player.objects.create(user=self.user2, game=self.gameB) - self.player3B = Player.objects.create(user=self.user3, game=self.gameB) - self.player4B = Player.objects.create(user=self.user4, game=self.gameB) - self.player5B = Player.objects.create(user=self.user5, game=self.gameB) - - self.player1C = Player.objects.create(user=self.user1, game=self.gameC) - self.player2C = Player.objects.create(user=self.user2, game=self.gameC) - self.player3C = Player.objects.create(user=self.user3, game=self.gameC) - self.player4C = Player.objects.create(user=self.user4, game=self.gameC) - - self.player1D = Player.objects.create(user=self.user1, game=self.gameD) - self.player2D = Player.objects.create(user=self.user2, game=self.gameD) - self.player3D = Player.objects.create(user=self.user3, game=self.gameD) - # Enables requests to API endpoints during testing self.client = APIClient() - def test_start_game(self): - - # Allow user 1 to make authenticated requests + def test_start_game_api(self): self.client.force_authenticate(user=self.user1) + + # Create 4 games and add players + sixPlayerGame = Game.objects.create(name='Game 1', host=self.user1) + fivePlayerGame = Game.objects.create(name='Game 2', host=self.user1) + fourPlayerGame = Game.objects.create(name='Game 3', host=self.user1) + threePlayerGame = Game.objects.create(name='Game 4', host=self.user1) + + Player.objects.create(user=self.user1, game=sixPlayerGame) + Player.objects.create(user=self.user2, game=sixPlayerGame) + Player.objects.create(user=self.user3, game=sixPlayerGame) + Player.objects.create(user=self.user4, game=sixPlayerGame) + Player.objects.create(user=self.user5, game=sixPlayerGame) + Player.objects.create(user=self.user6, game=sixPlayerGame) + + Player.objects.create(user=self.user1, game=fivePlayerGame) + Player.objects.create(user=self.user2, game=fivePlayerGame) + Player.objects.create(user=self.user3, game=fivePlayerGame) + Player.objects.create(user=self.user4, game=fivePlayerGame) + Player.objects.create(user=self.user5, game=fivePlayerGame) + + Player.objects.create(user=self.user1, game=fourPlayerGame) + Player.objects.create(user=self.user2, game=fourPlayerGame) + Player.objects.create(user=self.user3, game=fourPlayerGame) + Player.objects.create(user=self.user4, game=fourPlayerGame) - for game in [self.gameA, self.gameB, self.gameC, self.gameD]: + Player.objects.create(user=self.user1, game=threePlayerGame) + Player.objects.create(user=self.user2, game=threePlayerGame) + Player.objects.create(user=self.user3, game=threePlayerGame) + + for game in [sixPlayerGame, fivePlayerGame, fourPlayerGame, threePlayerGame]: # Try to start game response = self.client.post(f'/api/games/{game.id}/start-game/') @@ -66,3 +65,74 @@ def test_start_game(self): # Check that a Temporary Rome Consul has been assigned temp_rome_consuls = Title.objects.filter(senator__game=game, name='Temporary Rome Consul') self.assertEqual(temp_rome_consuls.count(), 1) + + def test_start_game_ranks(self): + + # Create a game with 6 players + game = Game.objects.create(name='Game 1', host=self.user1) + Player.objects.create(user=self.user1, game=game) + Player.objects.create(user=self.user2, game=game) + Player.objects.create(user=self.user3, game=game) + Player.objects.create(user=self.user4, game=game) + Player.objects.create(user=self.user5, game=game) + Player.objects.create(user=self.user6, game=game) + + # Allow user 1 to make authenticated requests + self.client.force_authenticate(user=self.user1) + + # Start game A with a seed + start_game_direct(game.id, self.user1, seed=1) + + # Check that the senators have been ranked correctly + senators = Senator.objects.filter(game=game).order_by('rank') + self.assertEqual(senators[0].name, "Papirius") + self.assertEqual(senators[0].rank, 0) + self.assertEqual(senators[1].name, "Cornelius") + self.assertEqual(senators[1].rank, 1) + self.assertEqual(senators[2].name, "Fabius") + self.assertEqual(senators[2].rank, 2) + + # Check that the Temporary Rome Consul is the highest ranked senator + temp_rome_consul = Title.objects.get(senator__game=game) + self.assertEqual(temp_rome_consul.senator.name, "Papirius") + + # Check that the factions have been ranked correctly + factions = Faction.objects.filter(game=game).order_by('rank') + self.assertEqual(factions[0].player.user.username, "User 1") + self.assertEqual(factions[0].rank, 0) + self.assertEqual(factions[1].player.user.username, "User 5") + self.assertEqual(factions[1].rank, 1) + self.assertEqual(factions[2].player.user.username, "User 2") + self.assertEqual(factions[2].rank, 2) + + # Check that the Temporary Rome Consul is in the highest ranked faction + self.assertEqual(temp_rome_consul.senator.faction, factions[0]) + + def test_start_game_api_errors(self): + self.client.force_authenticate(user=self.user1) + + # Try to start a non-existent game + response = self.client.post('/api/games/9999/start-game/') + self.assertEqual(response.status_code, 404) + self.assertEqual(response.data['message'], "Game not found") + + # Try to start a game as a non-host + otherUsersGame = Game.objects.create(name='Other Users Game', host=self.user2) + response = self.client.post(f'/api/games/{otherUsersGame.id}/start-game/') + self.assertEqual(response.status_code, 403) + self.assertEqual(response.data['message'], "Only the host can start the game") + + # Try to start the game with less players than required + thisUsersGame = Game.objects.create(name='This Users Game', host=self.user1) + Player.objects.create(user=self.user1, game=thisUsersGame) + response = self.client.post(f'/api/games/{thisUsersGame.id}/start-game/') + self.assertEqual(response.status_code, 403) + self.assertEqual(response.data['message'], "Game must have at least 3 players to start") + + # Try to start a game that has already started + Player.objects.create(user=self.user2, game=thisUsersGame) + Player.objects.create(user=self.user3, game=thisUsersGame) + start_game_direct(thisUsersGame.id, self.user1, seed=1) + response = self.client.post(f'/api/games/{thisUsersGame.id}/start-game/') + self.assertEqual(response.status_code, 403) + self.assertEqual(response.data['message'], "Game has already started") diff --git a/backend/rorapp/views/start_game.py b/backend/rorapp/views/start_game.py index 05d2e8a6..3d47f55d 100644 --- a/backend/rorapp/views/start_game.py +++ b/backend/rorapp/views/start_game.py @@ -1,17 +1,7 @@ -import os -import json -import random -from django.conf import settings -from django.utils import timezone from django.db import transaction from rest_framework import viewsets from rest_framework.decorators import action -from rest_framework.response import Response -from channels.layers import get_channel_layer -from asgiref.sync import async_to_sync -from rorapp.functions import rank_senators_and_factions -from rorapp.models import Game, Player, Faction, Senator, Title, Turn, Phase, Step, PotentialAction, ActionLog, SenatorActionLog -from rorapp.serializers import GameDetailSerializer, TurnSerializer, PhaseSerializer, StepSerializer +from rorapp.functions import start_game as start_game_direct class StartGameViewSet(viewsets.ViewSet): @@ -23,155 +13,5 @@ class StartGameViewSet(viewsets.ViewSet): @transaction.atomic def start_game(self, request, game_id=None): - # ENSURE THAT GAME CAN BE STARTED - - # Try to get the game - try: - game = Game.objects.get(id=game_id) - except Game.DoesNotExist: - return Response({"message": "Game not found"}, status=404) - - # Check if the user is not the game host - if game.host.id != request.user.id: - return Response({"message": "Only the host can start the game"}, status=403) - - # Check if the game has already started - if Step.objects.filter(phase__turn__game__id=game.id).count() > 0: - return Response({"message": "Game has already started"}, status=403) - - # Check if the game has less than 3 players - players = Player.objects.filter(game__id=game.id) - if players.count() < 3: - return Response({"message": "Game must have at least 3 players to start"}, status=403) - - # START AND SETUP THE GAME - - # Create and save factions - factions = [] - position = 1 - for player in players.order_by('?'): - faction = Faction(game=game, position=position, player=player) - faction.save() # Save factions to DB - factions.append(faction) - position += 1 - - # Read senator data - senator_json_path = os.path.join(settings.BASE_DIR, 'rorapp', 'presets', 'senator.json') - with open(senator_json_path, 'r') as file: - senators_dict = json.load(file) - - # Build a list of senators - senators = [] - for senator_name, senator_data in senators_dict.items(): - if senator_data['scenario'] == 1: - senator = Senator( - name=senator_name, - game=game, - code=senator_data['code'], - military=senator_data['military'], - oratory=senator_data['oratory'], - loyalty=senator_data['loyalty'], - influence=senator_data['influence'] - ) - senators.append(senator) - - # Shuffle the list - required_senator_count = len(factions) * 3 - random.shuffle(senators) - - # Discard some, leaving only the required number of senators - senators = senators[:required_senator_count] - - # Assign senators to factions - senator_iterator = iter(senators) - for faction in factions: - for _ in range(3): - senator = next(senator_iterator) - senator.faction = faction - senator.save() # Save senators to DB - - # Start the game - game.start_date = timezone.now() - game.save() # Update game to DB - - # Create turn, phase and step - turn = Turn(index=1, game=game) - turn.save() - phase = Phase(name="Faction", index=0, turn=turn) - phase.save() - step = Step(index=0, phase=phase) - step.save() - - # Assign temporary rome consul - random.shuffle(senators) - temp_rome_consul_title = Title(name="Temporary Rome Consul", senator=senators[0], start_step=step, major_office=True) - temp_rome_consul_title.save() - - # Create action log and senator action log for temporary rome consul - action_log = ActionLog( - index=0, - step=step, - type="temporary_rome_consul", - faction=temp_rome_consul_title.senator.faction, - data={"senator": temp_rome_consul_title.senator.id} - ) - action_log.save() - senator_action_log = SenatorActionLog( - senator=temp_rome_consul_title.senator, - action_log=action_log - ) - senator_action_log.save() - - # Update senator ranks - rank_senators_and_factions(game.id) - - # Create potential actions - for faction in factions: - action = PotentialAction( - step=step, faction=faction, type="select_faction_leader", - required=True, parameters=None - ) - action.save() - - # COMMUNICATE CHANGES TO PLAYERS AND SPECTATORS - - # Send WebSocket messages - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - f"game_{game.id}", - { - "type": "game_update", - "messages": [ - { - "operation": "update", - "instance": { - "class": "game", - "data": GameDetailSerializer(game).data - } - }, - { - "operation": "create", - "instance": { - "class": "turn", - "data": TurnSerializer(turn).data - } - }, - { - "operation": "create", - "instance": { - "class": "phase", - "data": PhaseSerializer(phase).data - } - }, - { - "operation": "create", - "instance": { - "class": "step", - "data": StepSerializer(step).data - } - } - ] - } - ) - - return Response({"message": "Game started"}, status=200) + # This viewset method wraps the start_game function in a transaction + return start_game_direct(game_id, request.user)