Skip to content

Commit b4033da

Browse files
authored
feat: allow anonymous users to play (#18)
* feat(backend): allow anonymous users to play * test(unit): add tests for anonymous users game
1 parent 97ea447 commit b4033da

4 files changed

Lines changed: 105 additions & 11 deletions

File tree

backend/api/play/game.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from typing import Dict
55

66
import chess
7+
from users.models import AnonymousSessionUser
78

89
from ..utils import genUniqueID
910
from .chess_board import CHESS_COLOR_NAMES, ChessBoard, CustomOutcome, CustomTermination
1011
from .game_modes import GameMode, TimeControl
1112
from .models import Game as GameModel
1213
from .models import GameTerminations, Move
14+
from .models import Player as PlayerModel
1315
from .players import APICallbackType, GameUser, Player, Players, TimeS, UnknownPlayer, UnknownPlayerType
1416

1517

@@ -184,12 +186,11 @@ def is_players_turn(self, user: GameUser) -> bool:
184186

185187
def save_to_db(self, result: CustomOutcome) -> None:
186188
"""Saves the game to the database."""
187-
white = self.players.by_color(chess.WHITE).user
188-
black = self.players.by_color(chess.BLACK).user
189+
whitePlayer, blackPlayer = self.get_player_models()
189190

190191
game = GameModel(
191-
player_white=white if white is not UnknownPlayer else None,
192-
player_black=black if black is not UnknownPlayer else None,
192+
player_white=whitePlayer,
193+
player_black=blackPlayer,
193194
termination=GameTerminations.from_chess_termination(result.termination),
194195
winner_color=result.winner,
195196
time_control=self.time_control.time,
@@ -199,6 +200,35 @@ def save_to_db(self, result: CustomOutcome) -> None:
199200
[Move(game=game, order=order, move=move) for order, move in enumerate(self.get_moves_list())]
200201
)
201202

203+
def get_player_models(self) -> tuple[PlayerModel | None, PlayerModel | None]:
204+
"""
205+
Gets the PlayerModel objects of the game's players.
206+
- Returns `None` if the player is an UnknownPlayer.
207+
"""
208+
whitePlayer = self.players.by_color(chess.WHITE)
209+
blackPlayer = self.players.by_color(chess.BLACK)
210+
211+
return (
212+
self.get_player_model(whitePlayer.user),
213+
self.get_player_model(blackPlayer.user),
214+
)
215+
216+
def get_player_model(self, user: GameUser | UnknownPlayerType) -> PlayerModel | None:
217+
"""Gets the PlayerModel object of the user, or None if the user is UnknownPlayer."""
218+
isUnknownPlayer = user is UnknownPlayer
219+
if isUnknownPlayer:
220+
return None
221+
222+
isAnonymousUser = isinstance(user, AnonymousSessionUser)
223+
224+
playerModel = PlayerModel(
225+
user=user if not isAnonymousUser else None,
226+
anonymousUser=user if isAnonymousUser else None,
227+
)
228+
playerModel.save()
229+
230+
return playerModel
231+
202232
def finish(self, result: CustomOutcome) -> None:
203233
"""Finishes the game and saves it to the database.
204234
- Does not save games with termination of `ABORTED`."""

backend/api/play/models.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

3+
from typing import Any
4+
35
import chess
46
from django.db import models
5-
from users.models import User
7+
from users.models import AnonymousSessionUser, User
68

79
from .chess_board import CustomTermination
810

@@ -41,10 +43,31 @@ def from_chess_termination(termination: chess.Termination | CustomTermination) -
4143
}
4244

4345

46+
class Player(models.Model):
47+
"""
48+
Abstraction class for the Player.
49+
- Either has to be a regular logged-in user or an anonymous user.
50+
"""
51+
52+
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
53+
anonymousUser = models.ForeignKey(AnonymousSessionUser, null=True, on_delete=models.CASCADE)
54+
55+
def clean(self) -> None:
56+
userObjects = [self.user, self.anonymousUser]
57+
isValid = sum(item is not None for item in userObjects) == 1
58+
59+
if not isValid:
60+
raise ValueError("Exactly one of user or anonymousUser must be set.")
61+
62+
def save(self, *args: Any, **kwargs: Any) -> None:
63+
self.clean()
64+
super().save(*args, **kwargs)
65+
66+
4467
class Game(models.Model):
4568
game_id = models.AutoField(primary_key=True)
46-
player_white = models.ForeignKey(User, related_name="player_white", on_delete=models.SET_NULL, null=True)
47-
player_black = models.ForeignKey(User, related_name="player_black", on_delete=models.SET_NULL, null=True)
69+
player_white = models.ForeignKey(Player, related_name="player_white", on_delete=models.SET_NULL, null=True)
70+
player_black = models.ForeignKey(Player, related_name="player_black", on_delete=models.SET_NULL, null=True)
4871
termination = models.IntegerField(choices=GameTerminations.choices)
4972
winner_color = models.BooleanField(null=True)
5073
time_control = models.PositiveBigIntegerField()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
from api.play.chess_board import CustomOutcome
3+
from api.play.game import ALL_ACTIVE_GAMES_MANAGER, Game
4+
from api.play.game_modes import GameMode, TimeControl
5+
from api.play.game_queue import GameQueueManager
6+
from chess import Termination
7+
from users.models import AnonymousSessionUser
8+
9+
10+
@pytest.mark.django_db
11+
def test_anonymous_user_game() -> None:
12+
user1 = AnonymousSessionUser.objects.create(session_key="session1")
13+
user2 = AnonymousSessionUser.objects.create(session_key="session2")
14+
15+
gameMode = GameMode("Blitz", [TimeControl(120)])
16+
queueManager = GameQueueManager([gameMode])
17+
queue = queueManager.get_game_queue("Blitz", 120)
18+
19+
assert queue is not None
20+
21+
addedUser1 = False
22+
addedUser2 = False
23+
24+
def onAddUser1Callback(_: Game) -> None:
25+
nonlocal addedUser1
26+
addedUser1 = True
27+
28+
def onAddUser2Callback(_: Game) -> None:
29+
nonlocal addedUser2
30+
addedUser2 = True
31+
32+
queueManager.add_user(user1, queue, onAddUser1Callback)
33+
queueManager.add_user(user2, queue, onAddUser2Callback)
34+
35+
first_game = next(iter(ALL_ACTIVE_GAMES_MANAGER.games.values()))
36+
37+
outcome = CustomOutcome(Termination.CHECKMATE, None)
38+
first_game.finish(outcome)
39+
gameId = first_game.game_id
40+
assert gameId is not None
41+
42+
ALL_ACTIVE_GAMES_MANAGER.remove_game(gameId)

backend/users/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from attr import dataclass
21
from django.contrib.auth.models import AbstractUser
2+
from django.db import models
33

44

55
class User(AbstractUser):
66
class Meta:
77
db_table = "auth_user"
88

99

10-
@dataclass(frozen=True)
11-
class AnonymousSessionUser:
12-
session_key: str
10+
class AnonymousSessionUser(models.Model):
11+
session_key = models.CharField(primary_key=True, max_length=255)

0 commit comments

Comments
 (0)