diff --git a/backend/api/play/game.py b/backend/api/play/game.py index dd1dd3f..3cbddfd 100644 --- a/backend/api/play/game.py +++ b/backend/api/play/game.py @@ -4,12 +4,14 @@ from typing import Dict import chess +from users.models import AnonymousSessionUser from ..utils import genUniqueID from .chess_board import CHESS_COLOR_NAMES, ChessBoard, CustomOutcome, CustomTermination from .game_modes import GameMode, TimeControl from .models import Game as GameModel from .models import GameTerminations, Move +from .models import Player as PlayerModel from .players import APICallbackType, GameUser, Player, Players, TimeS, UnknownPlayer, UnknownPlayerType @@ -184,12 +186,11 @@ def is_players_turn(self, user: GameUser) -> bool: def save_to_db(self, result: CustomOutcome) -> None: """Saves the game to the database.""" - white = self.players.by_color(chess.WHITE).user - black = self.players.by_color(chess.BLACK).user + whitePlayer, blackPlayer = self.get_player_models() game = GameModel( - player_white=white if white is not UnknownPlayer else None, - player_black=black if black is not UnknownPlayer else None, + player_white=whitePlayer, + player_black=blackPlayer, termination=GameTerminations.from_chess_termination(result.termination), winner_color=result.winner, time_control=self.time_control.time, @@ -199,6 +200,35 @@ def save_to_db(self, result: CustomOutcome) -> None: [Move(game=game, order=order, move=move) for order, move in enumerate(self.get_moves_list())] ) + def get_player_models(self) -> tuple[PlayerModel | None, PlayerModel | None]: + """ + Gets the PlayerModel objects of the game's players. + - Returns `None` if the player is an UnknownPlayer. + """ + whitePlayer = self.players.by_color(chess.WHITE) + blackPlayer = self.players.by_color(chess.BLACK) + + return ( + self.get_player_model(whitePlayer.user), + self.get_player_model(blackPlayer.user), + ) + + def get_player_model(self, user: GameUser | UnknownPlayerType) -> PlayerModel | None: + """Gets the PlayerModel object of the user, or None if the user is UnknownPlayer.""" + isUnknownPlayer = user is UnknownPlayer + if isUnknownPlayer: + return None + + isAnonymousUser = isinstance(user, AnonymousSessionUser) + + playerModel = PlayerModel( + user=user if not isAnonymousUser else None, + anonymousUser=user if isAnonymousUser else None, + ) + playerModel.save() + + return playerModel + def finish(self, result: CustomOutcome) -> None: """Finishes the game and saves it to the database. - Does not save games with termination of `ABORTED`.""" diff --git a/backend/api/play/models.py b/backend/api/play/models.py index 0997a57..e0e6170 100644 --- a/backend/api/play/models.py +++ b/backend/api/play/models.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import Any + import chess from django.db import models -from users.models import User +from users.models import AnonymousSessionUser, User from .chess_board import CustomTermination @@ -41,10 +43,31 @@ def from_chess_termination(termination: chess.Termination | CustomTermination) - } +class Player(models.Model): + """ + Abstraction class for the Player. + - Either has to be a regular logged-in user or an anonymous user. + """ + + user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) + anonymousUser = models.ForeignKey(AnonymousSessionUser, null=True, on_delete=models.CASCADE) + + def clean(self) -> None: + userObjects = [self.user, self.anonymousUser] + isValid = sum(item is not None for item in userObjects) == 1 + + if not isValid: + raise ValueError("Exactly one of user or anonymousUser must be set.") + + def save(self, *args: Any, **kwargs: Any) -> None: + self.clean() + super().save(*args, **kwargs) + + class Game(models.Model): game_id = models.AutoField(primary_key=True) - player_white = models.ForeignKey(User, related_name="player_white", on_delete=models.SET_NULL, null=True) - player_black = models.ForeignKey(User, related_name="player_black", on_delete=models.SET_NULL, null=True) + player_white = models.ForeignKey(Player, related_name="player_white", on_delete=models.SET_NULL, null=True) + player_black = models.ForeignKey(Player, related_name="player_black", on_delete=models.SET_NULL, null=True) termination = models.IntegerField(choices=GameTerminations.choices) winner_color = models.BooleanField(null=True) time_control = models.PositiveBigIntegerField() diff --git a/backend/tests/unit/api/play/test_game_queue.py b/backend/tests/unit/api/play/test_game_queue.py new file mode 100644 index 0000000..be3c419 --- /dev/null +++ b/backend/tests/unit/api/play/test_game_queue.py @@ -0,0 +1,42 @@ +import pytest +from api.play.chess_board import CustomOutcome +from api.play.game import ALL_ACTIVE_GAMES_MANAGER, Game +from api.play.game_modes import GameMode, TimeControl +from api.play.game_queue import GameQueueManager +from chess import Termination +from users.models import AnonymousSessionUser + + +@pytest.mark.django_db +def test_anonymous_user_game() -> None: + user1 = AnonymousSessionUser.objects.create(session_key="session1") + user2 = AnonymousSessionUser.objects.create(session_key="session2") + + gameMode = GameMode("Blitz", [TimeControl(120)]) + queueManager = GameQueueManager([gameMode]) + queue = queueManager.get_game_queue("Blitz", 120) + + assert queue is not None + + addedUser1 = False + addedUser2 = False + + def onAddUser1Callback(_: Game) -> None: + nonlocal addedUser1 + addedUser1 = True + + def onAddUser2Callback(_: Game) -> None: + nonlocal addedUser2 + addedUser2 = True + + queueManager.add_user(user1, queue, onAddUser1Callback) + queueManager.add_user(user2, queue, onAddUser2Callback) + + first_game = next(iter(ALL_ACTIVE_GAMES_MANAGER.games.values())) + + outcome = CustomOutcome(Termination.CHECKMATE, None) + first_game.finish(outcome) + gameId = first_game.game_id + assert gameId is not None + + ALL_ACTIVE_GAMES_MANAGER.remove_game(gameId) diff --git a/backend/users/models.py b/backend/users/models.py index 3fb2232..f7ae9aa 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,5 +1,5 @@ -from attr import dataclass from django.contrib.auth.models import AbstractUser +from django.db import models class User(AbstractUser): @@ -7,6 +7,5 @@ class Meta: db_table = "auth_user" -@dataclass(frozen=True) -class AnonymousSessionUser: - session_key: str +class AnonymousSessionUser(models.Model): + session_key = models.CharField(primary_key=True, max_length=255)