From e125eb16c78a4e3a179696d38b5dae8355e7c675 Mon Sep 17 00:00:00 2001 From: starnakin Date: Mon, 22 Apr 2024 11:37:08 +0200 Subject: [PATCH] core: fix: matchmaking and game --- frontend/static/js/api/Matchmaking.js | 4 +- frontend/static/js/views/MatchMakingView.js | 26 +++---- games/models.py | 18 +++-- games/objects/AGame.py | 10 ++- games/objects/APlayer.py | 3 - games/objects/ASpectator.py | 6 +- games/objects/pong/PongGame.py | 10 +-- games/objects/pong/PongPlayer.py | 12 ++- games/routine.py | 1 - games/serializers.py | 20 +---- matchmaking/consumers.py | 34 ++++---- matchmaking/models.py | 33 +++++--- matchmaking/routing.py | 2 +- transcendence/abstract/AbstractRoom.py | 78 ++++++++++--------- transcendence/abstract/AbstractRoomManager.py | 4 +- transcendence/abstract/AbstractRoomMember.py | 8 +- 16 files changed, 135 insertions(+), 134 deletions(-) diff --git a/frontend/static/js/api/Matchmaking.js b/frontend/static/js/api/Matchmaking.js index b0e2240..bcf4173 100644 --- a/frontend/static/js/api/Matchmaking.js +++ b/frontend/static/js/api/Matchmaking.js @@ -21,12 +21,12 @@ class MatchMaking * @param {Number} mode The number of players in a game * @returns {Promise} */ - async start(receive_func, disconnect_func, gamemode, mode) + async start(receive_func, disconnect_func, game_type, mode) { if (!await this.client.isAuthenticated()) return null; - let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${gamemode}/${mode}`; + let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${game_type}/${mode}`; this._socket = new WebSocket(url); diff --git a/frontend/static/js/views/MatchMakingView.js b/frontend/static/js/views/MatchMakingView.js index 2327053..b8bce29 100644 --- a/frontend/static/js/views/MatchMakingView.js +++ b/frontend/static/js/views/MatchMakingView.js @@ -19,7 +19,7 @@ export default class extends AbstractAuthenticatedView { } else { - await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), this.gamemode_input.value, this.nb_players_input.value); + await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), this.game_type_input.value, this.nb_players_input.value); this.button.innerHTML = lang.get("matchmakingStopSearch"); } @@ -34,10 +34,10 @@ export default class extends AbstractAuthenticatedView { { if (data.detail === "game_found") { - if (this.gamemode_input.value == "pong") - navigateTo(`/games/${data.gamemode}/${data.game_id}`); + if (this.game_type_input.value == "pong") + navigateTo(`/games/${data.game_type}/${data.game_id}`); else - navigateTo(`/games/${data.gamemode}/${data.game_id}`); + navigateTo(`/games/${data.game_type}/${data.game_id}`); return; } this.display_data(data); @@ -51,7 +51,7 @@ export default class extends AbstractAuthenticatedView { addEnterEvent() { - [this.nb_players_input, this.gamemode_input].forEach((input) => { + [this.nb_players_input, this.game_type_input].forEach((input) => { input.addEventListener('keydown', async ev => { @@ -74,13 +74,13 @@ export default class extends AbstractAuthenticatedView { }); } - addChangeGameModeEvent() + addChangegame_typeEvent() { let nb_players_div = document.getElementById("nb-players-div"); - this.gamemode_input.addEventListener("change", () => { + this.game_type_input.addEventListener("change", () => { - if (this.gamemode_input.value === "tictactoe") + if (this.game_type_input.value === "tictactoe") { nb_players_div.style.display = 'none'; this.nb_players_input.value = 2; @@ -96,7 +96,7 @@ export default class extends AbstractAuthenticatedView { addEvents() { this.addEnterEvent(); - this.addChangeGameModeEvent(); + this.addChangegame_typeEvent(); this.addChangeNbPlayersEvent(); } @@ -104,7 +104,7 @@ export default class extends AbstractAuthenticatedView { { this.button = document.getElementById("toggle-search"); this.nb_players_input = document.getElementById("nb-players-input"); - this.gamemode_input = document.getElementById("gamemode-input"); + this.game_type_input = document.getElementById("game-type-input"); this.button.onclick = this.toggle_search.bind(this); @@ -117,12 +117,12 @@ export default class extends AbstractAuthenticatedView {

${lang.get("matchmakingTitle")}

-
- - +
diff --git a/games/models.py b/games/models.py index 3fca337..37bfcce 100644 --- a/games/models.py +++ b/games/models.py @@ -3,25 +3,24 @@ from __future__ import annotations from django.db import models from django.db.models import QuerySet, CASCADE -from profiles.models import ProfileModel +from django.contrib.auth.models import User import time -# Create your models here. class GameModel(models.Model): finished = models.BooleanField(default = False) started = models.BooleanField(default = False) - winner = models.ForeignKey(ProfileModel, on_delete=CASCADE, null=True, blank=True) + winner = models.ForeignKey(User, on_delete=CASCADE, null=True, blank=True) start_timestamp = models.BigIntegerField(null = True, blank = True) stop_timestamp = models.BigIntegerField(null = True, blank = True) game_type = models.CharField(max_length = 60, default = "pong") - def create(self, players: list[ProfileModel]): + def create(self, players: list[User]): self.save() for player in players: - GameMembersModel(game = self.pk, player=player).save() + GameMembersModel(game = self, player=player).save() return self.pk def start(self): @@ -35,9 +34,12 @@ class GameModel(models.Model): self.stop_timestamp = round(time.time() * 1000, 1) self.save() - def get_players(self) -> list[ProfileModel]: + def get_players(self) -> list[User]: return [game_player.player for game_player in GameMembersModel.objects.filter(game = self)] + def get_players_profiles(self) -> list[User]: + return [game_player.player.profilemodel for game_player in GameMembersModel.objects.filter(game = self)] + def get_score_by_player_id(self, player_id: int) -> list[int]: query: QuerySet = GameGoalModel.objects.filter(game_id = self.pk, player_id = player_id) score_data: list[int] = [game_goal.timestamp for game_goal in query] @@ -56,10 +58,10 @@ class GameModel(models.Model): class GameMembersModel(models.Model): game = models.ForeignKey(GameModel, on_delete=CASCADE) - player = models.ForeignKey(ProfileModel, on_delete=CASCADE) + player = models.ForeignKey(User, on_delete=CASCADE) class GameGoalModel(models.Model): game = models.ForeignKey(GameModel, on_delete=CASCADE) - player = models.ForeignKey(ProfileModel, on_delete=CASCADE) + player = models.ForeignKey(User, on_delete=CASCADE) timestamp = models.IntegerField() \ No newline at end of file diff --git a/games/objects/AGame.py b/games/objects/AGame.py index b0ff08c..6839a61 100644 --- a/games/objects/AGame.py +++ b/games/objects/AGame.py @@ -6,6 +6,8 @@ from .ASpectator import ASpectator from ..models import GameModel +from django.contrib.auth.models import User + class AGame(AbstractRoom): def __init__(self, game_type: str, game_id: int, game_manager): @@ -16,23 +18,23 @@ class AGame(AbstractRoom): self.model: GameModel = GameModel.objects.get(pk = game_id, game_type = game_type) - players_id: list[int] = self.model.get_players_id() + players: list[User] = self.model.get_players() - self.players: list[APlayer] = [APlayer(player_id, None, self) for player_id in players_id] + self.players: list[APlayer] = [APlayer(player.pk, None, self) for player in players] self.spectators: list[ASpectator] = [] self.game_id: int = game_id def get_players_id(self) -> list[int]: - return [player.user_id for player in self.players] + return [player.pk for player in self.players] def get_players_connected(self) -> list[APlayer]: return [player for player in self.players if player.is_connected()] def get_player_by_user_id(self, user_id: int) -> APlayer: for player in self.players: - if (player.user_id == user_id): + if (player.user.pk == user_id): return player return None diff --git a/games/objects/APlayer.py b/games/objects/APlayer.py index 20edb67..1d4bcca 100644 --- a/games/objects/APlayer.py +++ b/games/objects/APlayer.py @@ -10,9 +10,6 @@ if TYPE_CHECKING: class APlayer(ASpectator): - def __init__(self, user_id: int, socket: WebsocketConsumer, game: AGame): - super().__init__(user_id, socket, game) - def is_connected(self) -> bool: return self.socket != None diff --git a/games/objects/ASpectator.py b/games/objects/ASpectator.py index f5a34e5..f7f2fd6 100644 --- a/games/objects/ASpectator.py +++ b/games/objects/ASpectator.py @@ -3,6 +3,8 @@ from channels.generic.websocket import WebsocketConsumer from transcendence.abstract.AbstractRoomMember import AbstractRoomMember +from django.contrib.auth.models import User + from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -10,8 +12,8 @@ if TYPE_CHECKING: class ASpectator(AbstractRoomMember): - def __init__(self, user_id: int, socket: WebsocketConsumer, game): + def __init__(self, user: User, socket: WebsocketConsumer, game): - super().__init__(user_id, socket) + super().__init__(user, socket) self.game: AGame = game \ No newline at end of file diff --git a/games/objects/pong/PongGame.py b/games/objects/pong/PongGame.py index 91f2de4..76296c7 100644 --- a/games/objects/pong/PongGame.py +++ b/games/objects/pong/PongGame.py @@ -22,7 +22,7 @@ import threading from typing import TYPE_CHECKING if TYPE_CHECKING: - pass + from profiles.models import ProfileModel class PongGame(AGame): @@ -36,7 +36,7 @@ class PongGame(AGame): radius: float = min(config.MAP_SIZE_X, config.MAP_SIZE_Y) / 2 - 10 - players_id: list[int] = self.model.get_players_id() + players: list[ProfileModel] = self.model.get_players() nb_sides = 4 @@ -58,16 +58,16 @@ class PongGame(AGame): self.walls: list[Wall] self.players: list[PongPlayer] - nb_players: int = len(players_id) + nb_players: int = len(players) if (nb_players == 2): - self.players = [PongPlayer(self, players_id[0], None, segments[0]), PongPlayer(self, players_id[1], None, segments[2])] + self.players = [PongPlayer(self, players[0], None, segments[0]), PongPlayer(self, players[1], None, segments[2])] self.walls = [Wall(segments[1].start, segments[1].stop), Wall(segments[3].start, segments[3].stop)] else: self.players = [] self.walls = [] for i in range(4): if (i < nb_players): - self.players.append(PongPlayer(self, players_id[i], None, segments[i])) + self.players.append(PongPlayer(self, players[i], None, segments[i])) else: self.walls.append(Wall(segments[i])) diff --git a/games/objects/pong/PongPlayer.py b/games/objects/pong/PongPlayer.py index 14db6e7..aeaa5de 100644 --- a/games/objects/pong/PongPlayer.py +++ b/games/objects/pong/PongPlayer.py @@ -17,9 +17,9 @@ if TYPE_CHECKING: class PongPlayer(APlayer): - def __init__(self, game: PongGame, user_id: int, socket: WebsocketConsumer, rail: Segment) -> None: + def __init__(self, game: PongGame, user: User, socket: WebsocketConsumer, rail: Segment) -> None: - super().__init__(user_id, socket, game) + super().__init__(user, socket, game) self.position: Position = Position(0.5, 0) @@ -29,8 +29,6 @@ class PongPlayer(APlayer): self.game: PongGame - self.username: str = User.objects.get(pk = self.user_id).username - def eliminate(self): self.disconnect(1000) @@ -43,7 +41,7 @@ class PongPlayer(APlayer): if (detail is None): return - + if (detail == "update_my_paddle_pos"): self.update_position(data) @@ -119,8 +117,8 @@ class PongPlayer(APlayer): def to_dict(self) -> dict: data = { - "username": self.username, - "id": self.user_id, + "username": self.user.username, + "id": self.user.pk, "position": self.position.to_dict(), "score": self.score, diff --git a/games/routine.py b/games/routine.py index 2bb3cba..6b1a0a5 100644 --- a/games/routine.py +++ b/games/routine.py @@ -270,7 +270,6 @@ async def render_ball(game: PongGame): async def render_players(game: PongGame): while True: - for player in game._updated_players: await SyncToAsync(game.broadcast)("update_player", player.to_dict(), [player]) diff --git a/games/serializers.py b/games/serializers.py index 6321d65..114c82d 100644 --- a/games/serializers.py +++ b/games/serializers.py @@ -3,7 +3,8 @@ from rest_framework import serializers from django.contrib.auth.models import User from django.db.models import QuerySet -from .models import GameModel, GameMembersModel +from .models import GameModel +from profiles.serializers.ProfileSerializer import ProfileSerializer class GameSerializer(serializers.ModelSerializer): @@ -28,19 +29,4 @@ class GameSerializer(serializers.ModelSerializer): return "waiting" def get_players(self, instance: GameModel): - players_data: list = [] - for player_id in instance.get_players_id(): - query: QuerySet = User.objects.filter(pk = player_id) - username: str = "Deleted User" - if (query.exists()): - username = query[0].username - - data: dict = { - "id": player_id, - "username": username, - "score": instance.get_score_by_player_id(player_id) - } - - players_data.append(data) - - return players_data \ No newline at end of file + return ProfileSerializer(instance.get_players_profiles(), many=True).data \ No newline at end of file diff --git a/matchmaking/consumers.py b/matchmaking/consumers.py index 80bceeb..331d391 100644 --- a/matchmaking/consumers.py +++ b/matchmaking/consumers.py @@ -6,15 +6,10 @@ from games.models import GameModel import json -from .models import Waiter, WaitingRoom, WaitingRoomManager, normal +from .models import Waiter, WaitingRoom, waiting_room_manager class MatchMaking(WebsocketConsumer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.channel_name = "matchmaking" - self.group_name = "matchmaking" - def connect(self): user: User = self.scope["user"] @@ -22,14 +17,12 @@ class MatchMaking(WebsocketConsumer): if (user.is_anonymous or not user.is_authenticated): return - self.channel_layer.group_add(self.group_name, self.channel_name) - self.mode: int = int(self.scope['url_route']['kwargs']['mode']) - self.gamemode: str = self.scope['url_route']['kwargs']['gamemode'] + self.game_type: str = self.scope['url_route']['kwargs']['game_type'] self.group_name = self.mode - waiting_room: WaitingRoom = normal.get(self.gamemode, self.mode) - waiting_room.append(Waiter(user.pk, self)) + self.waiting_room: WaitingRoom = waiting_room_manager.get(self.game_type, self.mode) + self.waiting_room.append(Waiter(user, self)) if (self.mode < 2 or self.mode > 4): data: dict = { @@ -39,23 +32,22 @@ class MatchMaking(WebsocketConsumer): self.disconnect(1000) return - if (self.gamemode not in ["tictactoe", "pong"]): + if (self.game_type not in ["tictactoe", "pong"]): data: dict = { - "detail": "The gamemode must 'pong' or 'tictactoe'.", + "detail": "The game_type must 'pong' or 'tictactoe'.", } self.send(json.dumps(data)) self.disconnect(1000) return - waiting_room.broadcast(f"{len(waiting_room)} / {waiting_room.mode}") - if (len(waiting_room) == waiting_room.mode): - game_id: int = GameModel().create(self.gamemode, waiting_room.get_users_id()) - waiting_room.broadcast("game_found", {"game_id": game_id, "gamemode": self.gamemode}) - waiting_room.clear() + self.waiting_room.broadcast(f"{len(self.waiting_room)} / {self.waiting_room.mode}") + if (len(self.waiting_room) == self.waiting_room.mode): + game_id: int = GameModel(game_type=self.game_type).create(self.waiting_room.get_members()) + self.waiting_room.broadcast("game_found", {"game_id": game_id, "game_type": self.game_type}) def disconnect(self, close_code): - super().close(close_code) - waiting_room: WaitingRoom = normal.get(self.gamemode, self.mode) + super().disconnect(close_code) + waiting_room: WaitingRoom = waiting_room_manager.get(self.game_type, self.mode) waiter: Waiter = waiting_room.get_member_by_socket(self) if (waiter is not None): - waiting_room.remove(waiter, close_code) \ No newline at end of file + waiting_room.remove(waiter) \ No newline at end of file diff --git a/matchmaking/models.py b/matchmaking/models.py index 58a0af0..ac9e014 100644 --- a/matchmaking/models.py +++ b/matchmaking/models.py @@ -13,29 +13,44 @@ class Waiter(AbstractRoomMember): class WaitingRoom(AbstractRoom): - def __init__(self, room_manager, gamemode: str, mode: int): + def __init__(self, room_manager, game_type: str, mode: int): + super().__init__(room_manager) - self.mode = mode - self.gamemode = gamemode + self._member_list: set[Waiter] + + self.mode: int = mode + self.game_type: str = game_type def append(self, waiter: Waiter): - tmp: Waiter = self.get_member_by_user_id(waiter.user_id) + + tmp: Waiter = self.get_member_by_user(waiter.user) if (tmp is not None): tmp.send("Connection close: Another connection open with the same user id.") self.remove(tmp) + waiter.socket.accept() - self._member_list.append(waiter) + + super().append(waiter) class WaitingRoomManager(AbstractRoomManager): - def get(self, gamemode: str, mode: int): + def __init__(self): + super().__init__() + self._room_list: set[WaitingRoom] + + + def get(self, game_type: str, mode: int) -> WaitingRoom: + for waiting_room in self._room_list: waiting_room: WaitingRoom - if (waiting_room.mode == mode and waiting_room.gamemode == gamemode): + if (waiting_room.mode == mode and waiting_room.game_type == game_type): return waiting_room - tmp: WaitingRoom = WaitingRoom(self, gamemode, mode) + + tmp: WaitingRoom = WaitingRoom(self, game_type, mode) + super().append(tmp) + return tmp -normal: WaitingRoomManager = WaitingRoomManager() \ No newline at end of file +waiting_room_manager: WaitingRoomManager = WaitingRoomManager() \ No newline at end of file diff --git a/matchmaking/routing.py b/matchmaking/routing.py index 2ae19b1..58d2178 100644 --- a/matchmaking/routing.py +++ b/matchmaking/routing.py @@ -2,5 +2,5 @@ from django.urls import re_path from . import consumers websocket_urlpatterns = [ - re_path(r'ws/matchmaking/(?P\w+)/(?P\d+)$', consumers.MatchMaking.as_asgi()) + re_path(r'ws/matchmaking/(?P\w+)/(?P\d+)$', consumers.MatchMaking.as_asgi()) ] diff --git a/transcendence/abstract/AbstractRoom.py b/transcendence/abstract/AbstractRoom.py index 620d037..5d568ff 100644 --- a/transcendence/abstract/AbstractRoom.py +++ b/transcendence/abstract/AbstractRoom.py @@ -1,51 +1,57 @@ +from __future__ import annotations + from channels.generic.websocket import WebsocketConsumer from .AbstractRoomMember import AbstractRoomMember +from django.contrib.auth.models import User + +from profiles.models import ProfileModel + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .AbstractRoomManager import AbstractRoomManager + class AbstractRoom: - def __init__(self, room_manager): - self._member_list: list[AbstractRoomMember] = [] - self.room_manager = room_manager + def __init__(self, room_manager: AbstractRoomManager): + self._member_list: set[AbstractRoomMember] = set() + self._room_manager: AbstractRoomManager = room_manager - def broadcast(self, detail: str, data: dict = {}): - for member in self._member_list: - member: AbstractRoomMember + def broadcast(self, detail: str, data: dict = {}, excludes: set[AbstractRoomMember] = set()) -> None: + + members: set[AbstractRoomMember] = self._member_list - excludes + + for member in members: member.send(detail, data) + + def get_member_by_socket(self, socket: WebsocketConsumer) -> AbstractRoomMember | None: + + for member in self._member_list: + if member.socket is socket: + return member + + def get_member_by_user(self, user: User) -> AbstractRoomMember: + + for member in self._member_list: + if member.user == user: + return member - def clear(self): - self._member_list.clear() + def get_members_profiles(self) -> set[ProfileModel]: + return set(member.user.profilemodel for member in self._member_list) + + def get_members(self) -> set[ProfileModel]: + return set(member.user for member in self._member_list) - def get_member_by_socket(self, socket: WebsocketConsumer): - for member in self._member_list: - member: AbstractRoomMember - if (member.socket is socket): - return member - return None + def append(self, member: AbstractRoomMember) -> None: + self._member_list.add(member) - def get_member_by_user_id(self, user_id: int): - for member in self._member_list: - member: AbstractRoomMember - if (member.user_id == user_id): - return member - return None - - - def append(self, member: AbstractRoomMember): - self._member_list.append(member) - member.accept() - - def remove(self, member: AbstractRoomMember, code: int = 1000): + def remove(self, member: AbstractRoomMember) -> None: self._member_list.remove(member) - member.socket.disconnect(code) - def empty(self): - for _ in self._member_list: - return False - return True + def get_users(self) -> set[User]: + return set(member.user for member in self._member_list) - def get_users_id(self): - return [member.user_id for member in self._member_list] - - def __len__(self): + def __len__(self) -> int: return len(self._member_list) diff --git a/transcendence/abstract/AbstractRoomManager.py b/transcendence/abstract/AbstractRoomManager.py index 5b12ef6..8c1f370 100644 --- a/transcendence/abstract/AbstractRoomManager.py +++ b/transcendence/abstract/AbstractRoomManager.py @@ -5,8 +5,8 @@ class AbstractRoomManager: def __init__(self): self._room_list: list[AbstractRoom] = [] - def remove(self, room: AbstractRoom): + def remove(self, room: AbstractRoom) -> None: self._room_list.remove(room) - def append(self, room: AbstractRoom): + def append(self, room: AbstractRoom) -> None: self._room_list.append(room) diff --git a/transcendence/abstract/AbstractRoomMember.py b/transcendence/abstract/AbstractRoomMember.py index e0e701e..570a110 100644 --- a/transcendence/abstract/AbstractRoomMember.py +++ b/transcendence/abstract/AbstractRoomMember.py @@ -1,14 +1,16 @@ from channels.generic.websocket import WebsocketConsumer +from django.contrib.auth.models import User + import json class AbstractRoomMember: - def __init__(self, user_id: int, socket: WebsocketConsumer): - self.user_id: int = user_id + def __init__(self, user: User, socket: WebsocketConsumer): + self.user: User = user self.socket: WebsocketConsumer = socket - def send(self, detail: str, data: dict = {}): + def send(self, detail: str, data: dict = {}) -> None: raw_data: dict = {"detail": detail} raw_data.update(data) self.socket.send(text_data=json.dumps(raw_data)) \ No newline at end of file