Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions backend/api/play/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand All @@ -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`."""
Expand Down
29 changes: 26 additions & 3 deletions backend/api/play/models.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
Expand Down
42 changes: 42 additions & 0 deletions backend/tests/unit/api/play/test_game_queue.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 3 additions & 4 deletions backend/users/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from attr import dataclass
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
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)