From aafc61d40aecb0bf70e19f6566be3b5f67168e41 Mon Sep 17 00:00:00 2001 From: starnakin Date: Mon, 22 Apr 2024 17:03:45 +0200 Subject: [PATCH] tournament: add: les crampte --- .../static/js/api/tournament/Tournament.js | 85 ++++++++--- .../views/tournament/TournamentCreateView.js | 20 ++- .../js/views/tournament/TournamentPageView.js | 133 +++++++----------- tournament/consumers.py | 52 +++++-- tournament/models.py | 33 ++--- tournament/serializers.py | 20 +-- 6 files changed, 191 insertions(+), 152 deletions(-) diff --git a/frontend/static/js/api/tournament/Tournament.js b/frontend/static/js/api/tournament/Tournament.js index 634ee11..caee57c 100644 --- a/frontend/static/js/api/tournament/Tournament.js +++ b/frontend/static/js/api/tournament/Tournament.js @@ -42,14 +42,33 @@ class Tourmanent extends AExchangeable * @type {Number} */ this.finished; - /** * @type {"finished" | "started" | "waiting"} must be "finished", or "started", or "waiting". Any other return all elements */ this.state; + + /** + * @type {Boolean} the client is a participant of the tournament + */ + this.is_participating; } + /** + * @param {Boolean} newParticipation + */ + async setParticipation(newParticipation) + { + if (this.isParticipating == newParticipation) + return; + + this.isParticipating = newParticipation; + + this._socket.send(JSON.stringify({"detail": "update_participating", + "is_participating": newParticipation}) + ); + + } /** * * @returns {Promise} @@ -72,18 +91,35 @@ class Tourmanent extends AExchangeable return; this.connected = false; this._socket.close(); - this.disconnectHandler(event); + this._disconnectHandler(event); } /** * @param {Object} data */ - async _receiveParticipantUpdate(data) + async _receiveAddParticipant(data) { - this.par + const participant = new Profile(this.client, undefined, data.participant.user_id); + participant.import(data.participant) + + this.participantList.push(participant); + + await this._addParticipantHandler(this.participantList.length) } - async onError(data) + /** + * @param {Object} data + */ + async _receiveDelParticipant(data) + { + const index = this.participantList.indexOf((profile) => profile.id === data.profile.user_id) + + this.participantList.splice(index, 1); + + await this._delParticipantHandler(this.participantList.length); + } + + async _receiveError(data) { await this.errorHandler(data); } @@ -92,14 +128,26 @@ class Tourmanent extends AExchangeable * * @param {MessageEvent} event */ - onReceive(event) + async onReceive(event) { const data = JSON.parse(event.data); - if (data.detail === "error") - this.onError(data); - else if (["del_participant", "add_participant"].includes(data.detail)) - this._receiveParticipantUpdate(data); + switch (data.detail) { + case "error": + await this._receiveError(data) + break; + + case "add_participant": + await this._receiveAddParticipant(data); + break; + + case "del_participant": + await this._receiveDelParticipant(data); + break; + + default: + break; + } } /** @@ -109,9 +157,11 @@ class Tourmanent extends AExchangeable * @param {CallableFunction} delParticipantHandler called when a participants leave the tournament * @param {CallableFunction} disconnectHandler * @param {CallableFunction} goToHandler called when the next game will start - * @returns {?} + * @param {CallableFunction} startHandler called when tournament start + * @param {CallableFunction} finishHandler called when tournament finish + * @returns {Promise} */ - async join(participantsUpdateHandler, errorHandler, goToHandler, disconnectHandler) + async join(addParticipantHandler, delParticipantHandler, startHandler, finishHandler, errorHandler, goToHandler, disconnectHandler) { if (!await this.client.isAuthenticated()) return null; @@ -123,10 +173,13 @@ class Tourmanent extends AExchangeable this.connected = true; this.isParticipating = false; - this.participantsUpdateHandler = participantsUpdateHandler; - this.errorHandler = errorHandler; - this.disconnectHandler = disconnectHandler; - this.goToHandler = goToHandler; + this._startHandler = startHandler; + this._finishHandler = finishHandler; + this._addParticipantHandler = addParticipantHandler; + this._delParticipantHandler = delParticipantHandler; + this._errorHandler = errorHandler; + this._disconnectHandler = disconnectHandler; + this._goToHandler = goToHandler; this._socket.onmessage = this.onReceive.bind(this); diff --git a/frontend/static/js/views/tournament/TournamentCreateView.js b/frontend/static/js/views/tournament/TournamentCreateView.js index 6565520..24752f1 100644 --- a/frontend/static/js/views/tournament/TournamentCreateView.js +++ b/frontend/static/js/views/tournament/TournamentCreateView.js @@ -16,10 +16,9 @@ export default class extends AbstractAuthenticatedView if (name.length == 0) name = lang.get("TournamentCreateTournamentName"); - console.log(name); - let nb_players = document.getElementById("nb-players-input").value; + let nb_participant = document.getElementById("nb-participant-input").value; - let response = await client.tournaments.createTournament(nb_players, name); + let response = await client.tournaments.createTournament(nb_participant, name); let response_data = await response.json(); let id = response_data.id; @@ -29,7 +28,7 @@ export default class extends AbstractAuthenticatedView return; } - clearIds("innerHTML", ["name", "nb_players"]); + clearIds("innerHTML", ["name", "nb_participants"]); fill_errors(response_data, "innerHTML"); } @@ -50,9 +49,16 @@ export default class extends AbstractAuthenticatedView
- - - + + +
diff --git a/frontend/static/js/views/tournament/TournamentPageView.js b/frontend/static/js/views/tournament/TournamentPageView.js index a43c84e..e7a5833 100644 --- a/frontend/static/js/views/tournament/TournamentPageView.js +++ b/frontend/static/js/views/tournament/TournamentPageView.js @@ -3,6 +3,10 @@ import { Tourmanent } from "../../api/tournament/Tournament.js"; import {client, navigateTo} from "../../index.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; +const TEXT_CONVENTION = { + "error": "[ERROR]" +} + export default class extends AbstractAuthenticatedView { constructor(params) @@ -13,95 +17,49 @@ export default class extends AbstractAuthenticatedView pressButton() { - this.tournament.toggle_participation(); + this.tournament.setParticipation(!this.tournament.isParticipating); + this.updateParticipating() } - createGraph() + updateParticipating() { - 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.round.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.round[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.round[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.round[round_id][participant_i + 1]}`; - - current_round.appendChild(game_bottom); - } - } - - } - - async receive(data) - { - if (data.detail === "update_participants") - document.getElementById("nb_participants").innerText = `${data.participants} / ${this.tournament.nb_participants}`; - if (data.detail === "go_to") - navigateTo(data.url); - if (data.detail === "is_participant") - this.updateParticipating(data.is_participant); - if (data.detail === "error") - document.getElementById("display").innerText = data.error_message; - } - - async updateParticipating(state) - { - document.getElementById("button").value = state ? `Leave ${this.tournament.name}` : `Join ${this.tournament.name}`; - document.getElementById("display").innerText = state ? "You are a particpant" : "You are not a participant"; - } - - /** - * - * @param {[Profile]} oldParticipantsList - * @param {[Profile]} currentParticipantsList - */ - async onParticipantsUpdate(oldParticipantsList, currentParticipantsList) - { - + document.getElementById("button").value = this.tournament.isParticipating ? `Leave ${this.tournament.name}` : `Join ${this.tournament.name}`; + document.getElementById("display").innerText = this.tournament.isParticipating ? "You are a particpant" : "You are not a participant"; } async onDisconnect(event) { + } async onError(data) { + this.addChatMessage(`${TEXT_CONVENTION} data.error_message`); + } + async onFinish() + { + document.getElementById("state").innerText = "finished" + } + + async onStart() + { + document.getElementById("state").innerText = "started" + } + + async onGoTo(data) + { + await navigateTo(`/games/pong/${data.game_id}`) + } + + async onAddParticipant(nb_participants) + { + document.getElementById("nb_participants").innerText = nb_participants; + } + + async onDelParticipant(nb_participants) + { + document.getElementById("nb_participants").innerText = nb_participants; } async postInit() @@ -114,20 +72,27 @@ export default class extends AbstractAuthenticatedView if (this.tournament === null) return 404; - this.tournament.join(this.onParticipantsUpdate.bind(this), this.onError.bind(this), this.onDisconnect.bind(this)); + this.tournament.join(this.onAddParticipant, this.onDelParticipant, this.onStart, this.onFinish, this.onError, this.onGoTo, this.onDisconnect); let button = document.getElementById("button"); button.onclick = this.pressButton.bind(this); document.getElementById("name").innerText = this.tournament.name; - document.getElementById("nb_participants").innerText = this.tournament.nb_participants; + document.getElementById("nb_participants").innerText = this.tournament.participantList.length; + document.getElementById("expected_nb_participants").innerText = this.tournament.nb_participants; document.getElementById("round").innerText = this.tournament.round; document.getElementById("state").innerText = this.tournament.state; if (this.tournament.started === false) button.disabled = false; - this.createGraph(); + + this.chat = document.getElementById("chat"); + } + + addChatMessage(message) + { + this.chat.innerText += message; } async getHtml() @@ -150,13 +115,19 @@ export default class extends AbstractAuthenticatedView Loading... - status + Expected number of participants + Loading... + + + state Loading... + `; } } diff --git a/tournament/consumers.py b/tournament/consumers.py index cb99d9c..78aa59f 100644 --- a/tournament/consumers.py +++ b/tournament/consumers.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from django.db.models import QuerySet from django.utils.translation import gettext as _ - +from games.models import GameModel from profiles.models import ProfileModel from profiles.serializers.ProfileSerializer import ProfileSerializer from .models import TournamentModel @@ -31,6 +31,9 @@ class TournamentMember: data_to_send.update(data) self.send("error", data_to_send) + + def send_goto(self, game: GameModel): + self.send("go_to", {"game_id": game.pk}) def _receive_participating(self, data: dict) -> None: @@ -38,20 +41,18 @@ class TournamentMember: if (is_participating is None): self.send_error(_("Missing is_participating statement.")) return - self._room.set_participation() + + self._room.set_participation(self, is_participating) def receive(self, data: dict): - if self.is_participating == False: - return - detail: str | None = data.get("detail") if (detail is None): return match(detail): - case "update_particapating": - self._receive_participating() + case "update_participating": + self._receive_participating(data) case _: print("bozo_send") @@ -80,16 +81,35 @@ class TournamentRoom: def __init__(self, room_manager: TournamentRoomManager, tournament: TournamentModel): self._room_manager: TournamentRoomManager = room_manager - self._member_list: list[TournamentMember] = [] + self._member_list: set[TournamentMember] = set() self._model: TournamentModel = tournament def join(self, socket: TournamentWebConsumer) -> TournamentMember: member: TournamentMember = TournamentMember(socket, self) - self._member_list.append(member) + self._member_list.add(member) return member + + def get_participants_profiles(self) -> list[ProfileModel]: + return [participant._socket.user.profilemodel for participant in self.get_participants()] + def start(self) -> None: + + games: list[GameModel] = self._model.start() + + self.broadcast("start") + + for game in games: + for player in game.get_players(): + participant: TournamentMember = self.get_participant_by_profile(player) + participant.send_goto(game) + + def get_participant_by_profile(self, profile: ProfileModel): + for participant in self.get_participants(): + if (participant._socket.user.profilemodel == profile): + return participant + def leave(self, member: TournamentMember) -> None: # Delete room if nobody connected, no cringe memory leak @@ -100,10 +120,13 @@ class TournamentRoom: self._member_list.remove(member) self.set_participation(member, False) + + def everybody_is_here(self): + return len(self.get_participants()) == self._model.nb_participants - def broadcast(self, detail: str, data: dict, excludes: list[TournamentMember] = []) -> None: + def broadcast(self, detail: str, data: dict, excludes: set[TournamentMember] = set()) -> None: - member_list: list[TournamentMember] = [member for member in self._member_list if member not in excludes] + member_list: list[TournamentMember] = self._member_list - excludes for member in member_list: member.send(detail, data) @@ -120,11 +143,14 @@ class TournamentRoom: return if (is_participating == True): - self.broadcast("add_participant", {"profile", ProfileSerializer(member._socket.user.profilemodel).data}) + self.broadcast("add_participant", {"participant": ProfileSerializer(member._socket.user.profilemodel).data}) else: - self.broadcast("del_participant", {"profile", ProfileSerializer(member._socket.user.profilemodel).data}) + self.broadcast("del_participant", {"participant": ProfileSerializer(member._socket.user.profilemodel).data}) member.is_participating = is_participating + + if self.everybody_is_here(): + self.start() tournament_manager: TournamentRoomManager = TournamentRoomManager() diff --git a/tournament/models.py b/tournament/models.py index 176fdfc..4f0fe28 100644 --- a/tournament/models.py +++ b/tournament/models.py @@ -1,21 +1,12 @@ from __future__ import annotations -from transcendence.abstract.AbstractRoomMember import AbstractRoomMember -from transcendence.abstract.AbstractRoom import AbstractRoom -from transcendence.abstract.AbstractRoomManager import AbstractRoomManager - -from profiles.models import ProfileModel from games.models import GameModel from django.contrib.auth.models import User from django.db.models import CASCADE -from channels.generic.websocket import WebsocketConsumer from django.db import models -import json - -# Create your models here.tu class TournamentModel(models.Model): name = models.CharField(max_length = 100) @@ -23,12 +14,14 @@ class TournamentModel(models.Model): round = models.IntegerField() started = models.BooleanField(default = False) finished = models.BooleanField(default = False) - winner = models.ForeignKey(ProfileModel, on_delete=CASCADE, blank=True, null=True) + winner = models.ForeignKey(User, on_delete=CASCADE, blank=True, null=True) - def _register_participant(self, participant: ProfileModel) -> None: + def _register_participant(self, participant: User) -> None: TournamentParticipantModel(participant=participant, tournament=self).save() - def start(self, participants: list[ProfileModel]) -> None: + def start(self, participants: list[User]) -> None: + + games: list[GameModel] = [] self.started = True @@ -36,11 +29,14 @@ class TournamentModel(models.Model): self._register_participant(player) for (participant1, participant2) in zip(participants[0::2], participants[1::2]): - self.create_game([participant1, participant2], round=1) + game: GameModel = self.create_game([participant1, participant2], round=1) + games.append(game) self.save() - def create_game(self, participants: list[ProfileModel], round: int) -> GameModel: + return games + + def create_game(self, participants: list[User], round: int) -> GameModel: if (self.started == False): return None @@ -62,10 +58,10 @@ class TournamentModel(models.Model): def get_games_by_round(self, round: int) -> list[GameModel]: return [tournament_game.game for tournament_game in TournamentGameModel.objects.filter(tournament=self, round=round)] - def get_players_by_round(self, round: int) -> list[ProfileModel]: + def get_players_by_round(self, round: int) -> list[User]: return [game.get_players() for game in self.get_games_by_round(round)] - def get_winners_by_round(self, round: int) -> list[ProfileModel]: + def get_winners_by_round(self, round: int) -> list[User]: return [game.winner for game in self.get_games_by_round(round)] def get_participants(self) -> list[TournamentParticipantModel]: @@ -74,13 +70,12 @@ class TournamentModel(models.Model): def get_state(self) -> str: return ("waiting to start", "in progress", "finish")[self.started + self.finished] - def is_participanting(self, profile: ProfileModel) -> bool: + def is_participanting(self, profile: User) -> bool: return TournamentParticipantModel.objects.filter(participant=profile, tournament=self).exists() class TournamentParticipantModel(models.Model): - participant = models.ForeignKey(ProfileModel, on_delete=CASCADE) + participant = models.ForeignKey(User, on_delete=CASCADE) tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE) - #prout à encore frappé class TournamentGameModel(models.Model): diff --git a/tournament/serializers.py b/tournament/serializers.py index 18b4f13..abf2cfb 100644 --- a/tournament/serializers.py +++ b/tournament/serializers.py @@ -1,15 +1,13 @@ from rest_framework import serializers -from django.db.models.query import QuerySet - -from django.contrib.auth.models import User - from .models import TournamentModel from profiles.models import ProfileModel from profiles.serializers.ProfileSerializer import ProfileSerializer from games.serializers import GameSerializer +nb_participants = [2 ** i for i in range(2, 6)] + class TournamentSerializer(serializers.ModelSerializer): state = serializers.SerializerMethodField(read_only=True, required=False) @@ -30,16 +28,6 @@ class TournamentSerializer(serializers.ModelSerializer): return ["waiting", "started", "finished"][instance.started + instance.finished] def validate_nb_participants(self, value: int): - if (value < 2): - raise serializers.ValidationError("The numbers of participants must be greather than 2.") - return value - - def validate_nb_participants_by_game(self, value: int): - if (value < 2): - raise serializers.ValidationError("The numbers of participants by game must be greather than 2.") - nb_participants: str = self.initial_data.get("nb_participants") - if (nb_participants is not None and nb_participants.isnumeric()): - nb_participants: int = int(nb_participants) - if (value > nb_participants): - raise serializers.ValidationError("The numbers of participants by game must be smaller than the numbers of participants.") + if (value not in nb_participants): + raise serializers.ValidationError(f"The numbers of participants must be {str(nb_participants)}.") return value \ No newline at end of file