From 09e7476127d8399c548ecbc49ddac5478df7eb5d Mon Sep 17 00:00:00 2001 From: starnakin Date: Mon, 25 Mar 2024 13:21:37 +0100 Subject: [PATCH 1/2] add: tournament: graph de bz --- frontend/static/css/TournamentPage.css | 51 ++++++++++++ .../static/js/api/tournament/Tournament.js | 45 +++++++--- .../js/views/tournament/TournamentPageView.js | 83 +++++++++++++++++-- tournament/models.py | 4 +- tournament/serializers.py | 39 ++++++++- 5 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 frontend/static/css/TournamentPage.css diff --git a/frontend/static/css/TournamentPage.css b/frontend/static/css/TournamentPage.css new file mode 100644 index 0000000..d9c826b --- /dev/null +++ b/frontend/static/css/TournamentPage.css @@ -0,0 +1,51 @@ +#tournament-tree { + display:flex; + flex-direction:row; +} + +.round { + display:flex; + flex-direction:column; + justify-content:center; + width:200px; + list-style:none; + padding:0; +} + +.round .spacer{ flex-grow:1; } +.round .spacer:first-child, +.round .spacer:last-child{ flex-grow:.5; } + +.round .game-spacer{ + flex-grow:1; +} + +body{ + font-family:sans-serif; + font-size:small; + padding:10px; + line-height:1.4em; +} + +li.game{ +padding-left:20px; +} + +li.game.winner{ + font-weight:bold; +} +li.game span{ + float:right; + margin-right:5px; +} + +li.game-top{ border-bottom:1px solid #aaa; } + +li.game-spacer{ + border-right:1px solid #aaa; + min-height:40px; +} + +li.game-bottom{ + border-top:1px solid #aaa; +} \ No newline at end of file diff --git a/frontend/static/js/api/tournament/Tournament.js b/frontend/static/js/api/tournament/Tournament.js index e5e4682..2d727ce 100644 --- a/frontend/static/js/api/tournament/Tournament.js +++ b/frontend/static/js/api/tournament/Tournament.js @@ -102,7 +102,7 @@ class Tourmanent return; this.connected = false; this._socket.close(); - this.disconnect_func(event); + this.disconnectHandler(event); } toggle_participation() @@ -112,13 +112,40 @@ class Tourmanent this._socket.send(JSON.stringify({participate: ""})); } + async onParticipantsUpdate(data) + { + oldParticipantList = this.par + + await this.participantsUpdateHandler(); + } + + async onError(data) + { + await this.errorHandler(data); + } + + /** + * + * @param {MessageEvent} event + */ + onReceive(event) + { + const data = JSON.parse(event.data); + + if (data?.detail === "error") + this.onError(data); + else if (data?.detail === "participants_update") + this.onParticipantsUpdate(data); + } + /** * Join the tournament Websocket - * @param {CallableFunction} receive_func - * @param {CallableFunction} disconnect_func + * @param {CallableFunction} errorHandler + * @param {CallableFunction} participantsUpdateHandler + * @param {CallableFunction} disconnectHandler * @returns {?} */ - async join(receive_func, disconnect_func) + async join(participantsUpdateHandler, errorHandler, disconnectHandler) { if (!await this.client.isAuthenticated()) return null; @@ -130,13 +157,11 @@ class Tourmanent this.connected = true; this.isParticipating = false; - this.receive_func = receive_func; - this.disconnect_func = disconnect_func; + this.participantsUpdateHandler = participantsUpdateHandler; + this.errorHandler = errorHandler; + this.disconnectHandler = disconnectHandler; - this._socket.onmessage = function (event) { - const data = JSON.parse(event.data); - receive_func(data); - }; + this._socket.onmessage = this.onReceive.bind(this); this._socket.onclose = this.leave.bind(this); } diff --git a/frontend/static/js/views/tournament/TournamentPageView.js b/frontend/static/js/views/tournament/TournamentPageView.js index d36fd3d..20a52a1 100644 --- a/frontend/static/js/views/tournament/TournamentPageView.js +++ b/frontend/static/js/views/tournament/TournamentPageView.js @@ -1,3 +1,5 @@ +import { Profile } from "../../api/Profile.js"; +import { Tourmanent } from "../../api/tournament/Tournament.js"; import {client, navigateTo} from "../../index.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; @@ -14,10 +16,61 @@ export default class extends AbstractAuthenticatedView this.tournament.toggle_participation(); } + createGraph() + { + console.log(this.tournament); + let tournament_tree = document.createElement("div"); + + tournament_tree.id = "tournament-tree"; + + document.getElementById("app").appendChild(tournament_tree); + + for (let round_id = 0; round_id < this.tournament.levels.length; round_id++) + { + let current_round = document.createElement("ul"); + + tournament_tree.appendChild(current_round); + + current_round.className = `round round-${round_id}`; + + for (let participant_i = 0; participant_i < this.tournament.levels[round_id].length; participant_i += 2) + { + let spacer = document.createElement("li"); + + spacer.className = "spacer"; + spacer.innerText = " "; + + current_round.appendChild(spacer); + + let game_top = document.createElement("li"); + + game_top.className = "game game-top"; + game_top.innerText = `${this.tournament.levels[round_id][participant_i]}`; + + current_round.appendChild(game_top); + + let game_spacer = document.createElement("li"); + + spacer.className = "game game-spacer"; + spacer.innerText = " "; + + current_round.appendChild(game_spacer); + + let game_bottom = document.createElement("li"); + + game_bottom.className = "game game-bottom"; + game_bottom.innerText = `${this.tournament.levels[round_id][participant_i + 1]}`; + + current_round.appendChild(game_bottom); + } + } + + } + async receive(data) { - if (data.detail === "nb_participants" || data.detail === "update_participants") - document.getElementById("nb_participants").innerText = `${data.nb_participants} / ${this.tournament.nb_players}`; + if (data.detail === "update_participants") + document.getElementById("nb_participants").innerText = `${data.participants} / ${this.tournament.nb_players}`; if (data.detail === "go_to") navigateTo(data.url); if (data.detail === "is_participant") @@ -32,36 +85,56 @@ export default class extends AbstractAuthenticatedView document.getElementById("display").innerText = state ? "You are a particpant" : "You are not a participant"; } - async ondisconnect(event) + /** + * + * @param {[Profile]} oldParticipantsList + * @param {[Profile]} currentParticipantsList + */ + async onParticipantsUpdate(oldParticipantsList, currentParticipantsList) { + + } + + async onDisconnect(event) + { + } + + async onError(data) + { + } async postInit() { + /** + * @type {Tourmanent} + */ this.tournament = await client.tournaments.getTournament(this.id); if (this.tournament === null) return 404; - this.tournament.join(this.receive.bind(this), this.ondisconnect.bind(this)); + this.tournament.join(this.onParticipantsUpdate.bind(this), this.onError.bind(this), this.onDisconnect.bind(this)); let button = document.getElementById("button"); button.onclick = this.pressButton.bind(this); document.getElementById("name").innerText = this.tournament.name; - document.getElementById("nb_players").innerText = this.tournament.nb_players; + document.getElementById("nb_participants").innerText = this.tournament.nb_players; document.getElementById("nb_players_by_game").innerText = this.tournament.nb_players_by_game; document.getElementById("level").innerText = this.tournament.level; document.getElementById("state").innerText = this.tournament.state; if (this.tournament.started === false) button.disabled = false; + this.createGraph(); } async getHtml() { return ` + diff --git a/tournament/models.py b/tournament/models.py index f664e1e..87eb9da 100644 --- a/tournament/models.py +++ b/tournament/models.py @@ -32,7 +32,7 @@ class TournamentModel(models.Model): def get_games_id(self): return [tournament_game.game_id for tournament_game in TournamentGamesModel.objects.filter(tournament_id = self.pk)] - def get_players_id(self): + def get_participants_id(self): return [model.participant_id for model in TournamentParticipantsModel.objects.filter(tournament_id=self.pk)] def is_a_participant(self, participant_id: int): @@ -134,7 +134,7 @@ class TournamentRoom(AbstractRoom): if self.tournament.started: member.participate = self.tournament.is_a_participant(member.user_id) member.send_participating() - member.send("nb_participants", {"nb_participants": self.get_nb_participants()}) + self.broadcast("update_participants", {"participants": [self.get_participants_id()]}) class TournamentRoomManager(AbstractRoomManager): diff --git a/tournament/serializers.py b/tournament/serializers.py index 9308e08..d90df00 100644 --- a/tournament/serializers.py +++ b/tournament/serializers.py @@ -1,11 +1,20 @@ from rest_framework import serializers -from .models import TournamentModel + +from django.db.models.query import QuerySet + +from django.contrib.auth.models import User + +from .models import TournamentModel, tournament_manager + +from profiles.models import ProfileModel +from profiles.serializers import ProfileSerializer from games.serializers import GameSerializer class TournamentSerializer(serializers.ModelSerializer): levels = serializers.SerializerMethodField(read_only=True, required=False) state = serializers.SerializerMethodField(read_only=True, required=False) + participants = serializers.SerializerMethodField(read_only=True, required=False) level = serializers.ReadOnlyField() started = serializers.ReadOnlyField() finished = serializers.ReadOnlyField() @@ -13,7 +22,32 @@ class TournamentSerializer(serializers.ModelSerializer): class Meta: model = TournamentModel - fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id", "state"] + fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id", "state", "participants"] + + def get_participants(self, instance: TournamentModel): + + participants_id: list[ProfileModel] + + if (instance.started): + participants_id = instance.get_participants_id() + else: + participants_id = tournament_manager.get(instance.pk).get_participants_id() + + participants_profile: list[ProfileModel] = [] + for participant_id in participants_id: + query: QuerySet = ProfileModel.objects.filter(user_id = participant_id) + profile_data: dict + if query.exists(): + profile_data = ProfileSerializer(query[0]).data + else: + profile_data = { + "username": "deleted_user", + "avatar": "/static/avatars/default.avif", + "user_id": participants_id + } + participants_profile.append(profile_data) + + return participants_profile def get_state(self, instance: TournamentModel): return ["waiting", "started", "finished"][instance.started + instance.finished] @@ -31,6 +65,7 @@ class TournamentSerializer(serializers.ModelSerializer): if (value < 2): raise serializers.ValidationError("The numbers of players must be greather than 2.") return value + def validate_nb_players_by_game(self, value: int): if (value < 2): raise serializers.ValidationError("The numbers of players by game must be greather than 2.") From c232f03c17f0ec20e3b08ece88382b616fbf9b7d Mon Sep 17 00:00:00 2001 From: starnakin Date: Mon, 25 Mar 2024 14:53:15 +0100 Subject: [PATCH 2/2] matchmaking support multigame --- frontend/static/js/api/Matchmaking.js | 10 +-- frontend/static/js/views/MatchMakingView.js | 88 +++++++++++++-------- matchmaking/consumers.py | 16 +++- matchmaking/models.py | 10 ++- matchmaking/routing.py | 2 +- 5 files changed, 79 insertions(+), 47 deletions(-) diff --git a/frontend/static/js/api/Matchmaking.js b/frontend/static/js/api/Matchmaking.js index 1bbec59..b0e2240 100644 --- a/frontend/static/js/api/Matchmaking.js +++ b/frontend/static/js/api/Matchmaking.js @@ -21,13 +21,12 @@ class MatchMaking * @param {Number} mode The number of players in a game * @returns {Promise} */ - async start(receive_func, disconnect_func, mode, gamemode) + async start(receive_func, disconnect_func, gamemode, mode) { if (!await this.client.isAuthenticated()) return null; - this.gamemode = gamemode - let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`; + let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${gamemode}/${mode}`; this._socket = new WebSocket(url); @@ -50,10 +49,7 @@ class MatchMaking this.disconnect_func(event); } - /** - * @returns {Promise} - */ - async stop() + stop() { if (this._socket) this._socket.close(); diff --git a/frontend/static/js/views/MatchMakingView.js b/frontend/static/js/views/MatchMakingView.js index 6ca775e..6c45c43 100644 --- a/frontend/static/js/views/MatchMakingView.js +++ b/frontend/static/js/views/MatchMakingView.js @@ -19,8 +19,7 @@ export default class extends AbstractAuthenticatedView { } else { - let nb_players = this.input.value; - await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players, this.gamemode); + await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), this.gamemode_input.value, this.nb_players_input.value); this.button.innerHTML = lang.get("matchmakingStopSearch"); } @@ -29,6 +28,7 @@ export default class extends AbstractAuthenticatedView { ondisconnect(event) { this.button.innerHTML = lang.get("matchmakingStartSearch"); + clearIds("innerText", ["detail"]); } onreceive(data) @@ -36,9 +36,9 @@ export default class extends AbstractAuthenticatedView { if (data.detail === "game_found") { if (this.gamemode.value == "pong") - navigateTo(`/games/${data.game_id}`); + navigateTo(`/games/${data.gamemode}/${data.game_id}`); else - navigateTo(`/games/${this.gamemode.value}/${data.game_id}`); + navigateTo(`/games/${data.gamemode}/${data.game_id}`); return; } this.display_data(data); @@ -50,59 +50,83 @@ export default class extends AbstractAuthenticatedView { fill_errors(data, "innerText"); } - async postInit() + addEnterEvent() { - this.button = document.getElementById("toggle-search"); - this.input = document.getElementById("nb-players-input"); - this.gamemode = document.getElementById("game-choice"); + console.log(this.nb_players_input, this.gamemode_input); - let container = document.getElementById("nb-players-container"); - let gameChoice = document.getElementById("game-choice"); + [this.nb_players_input, this.gamemode_input].forEach((input) => { - this.button.onclick = this.toggle_search.bind(this); + input.addEventListener('keydown', async ev => { - this.input.addEventListener('keydown', async ev => { + if (ev.key !== 'Enter') + return; - if (ev.key !== 'Enter') - return; - - await this.toggle_search.bind(this); + await this.toggle_search.bind(this); + }); }); + } - gameChoice.addEventListener("change", function() - { - if (this.value === "tictactoe") - { - container.style.display = 'none'; - document.getElementById("nb-players-input").value = 2; - } - else - container.style.display = 'block'; - }) - + addChangeNbPlayersEvent() + { let update = () => { this.button.disabled = (this.input.value < 2 || this.input.value > 4); }; ["change", "oninput"].forEach((event_name) => { - this.input.addEventListener(event_name, update); + this.nb_players_input.addEventListener(event_name, update); }); } + addChangeGameModeEvent() + { + let nb_players_div = document.getElementById("nb-players-div"); + + this.gamemode_input.addEventListener("change", () => { + + if (this.gamemode_input.value === "tictactoe") + { + nb_players_div.style.display = 'none'; + this.nb_players_input.value = 2; + } + else + nb_players_div.style.display = 'block'; + + client.matchmaking.stop(); + }); + } + + addEvents() + { + this.addEnterEvent(); + this.addChangeGameModeEvent(); + this.addChangeNbPlayersEvent(); + } + + async postInit() + { + this.button = document.getElementById("toggle-search"); + this.nb_players_input = document.getElementById("nb-players-input"); + this.gamemode_input = document.getElementById("gamemode-input"); + + this.button.onclick = this.toggle_search.bind(this); + + this.addEvents() + } + async getHtml() { return /* HTML */ `

${lang.get("matchmakingTitle")}

-
- - +
-
+
diff --git a/matchmaking/consumers.py b/matchmaking/consumers.py index e6375f8..f66a9db 100644 --- a/matchmaking/consumers.py +++ b/matchmaking/consumers.py @@ -18,17 +18,27 @@ class MatchMaking(WebsocketConsumer): def connect(self): user: User = self.scope["user"] + 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.group_name = self.mode - - waiting_room: WaitingRoom = normal.get(self.mode) + + waiting_room: WaitingRoom = normal.get(self.gamemode, self.mode) waiting_room.append(Waiter(user.pk, self)) + if (self.mode < 2 or self.mode > 4): + data: dict = { + "detail": "The mode must be > 1 and < 4.", + } + self.send(json.dumps(data)) + self.disconnect(1000) + return + if (self.mode < 2 or self.mode > 4): data: dict = { "detail": "The mode must be > 1 and < 4.", @@ -45,7 +55,7 @@ class MatchMaking(WebsocketConsumer): def disconnect(self, close_code): super().close(close_code) - waiting_room: WaitingRoom = normal.get(self.mode) + waiting_room: WaitingRoom = normal.get(self.gamemode, 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 diff --git a/matchmaking/models.py b/matchmaking/models.py index 2c6b3d6..f1261f0 100644 --- a/matchmaking/models.py +++ b/matchmaking/models.py @@ -13,9 +13,10 @@ class Waiter(AbstractRoomMember): class WaitingRoom(AbstractRoom): - def __init__(self, room_manager,mode): + def __init__(self, room_manager, gamemode: str, mode: int): super().__init__(room_manager) self.mode = mode + self.gamemode = gamemode def append(self, waiter: Waiter): tmp: Waiter = self.get_member_by_user_id(waiter.user_id) @@ -27,12 +28,13 @@ class WaitingRoom(AbstractRoom): class WaitingRoomManager(AbstractRoomManager): - def get(self, mode: int): + def get(self, gamemode: str, mode: int): + for waiting_room in self._room_list: waiting_room: WaitingRoom - if (waiting_room.mode == mode): + if (waiting_room.mode == mode and waiting_room.gamemode == gamemode): return waiting_room - tmp: WaitingRoom = WaitingRoom(self, mode) + tmp: WaitingRoom = WaitingRoom(self, gamemode, mode) super().append(tmp) return tmp diff --git a/matchmaking/routing.py b/matchmaking/routing.py index 22af33e..2ae19b1 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\d+)$', consumers.MatchMaking.as_asgi()) + re_path(r'ws/matchmaking/(?P\w+)/(?P\d+)$', consumers.MatchMaking.as_asgi()) ]