tournament: add: les crampte

This commit is contained in:
starnakin 2024-04-22 17:03:45 +02:00
parent 1d8c2c633a
commit aafc61d40a
6 changed files with 191 additions and 152 deletions

View File

@ -43,13 +43,32 @@ class Tourmanent extends AExchangeable
*/ */
this.finished; this.finished;
/** /**
* @type {"finished" | "started" | "waiting"} must be "finished", or "started", or "waiting". Any other return all elements * @type {"finished" | "started" | "waiting"} must be "finished", or "started", or "waiting". Any other return all elements
*/ */
this.state; 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<?>} * @returns {Promise<?>}
@ -72,18 +91,35 @@ class Tourmanent extends AExchangeable
return; return;
this.connected = false; this.connected = false;
this._socket.close(); this._socket.close();
this.disconnectHandler(event); this._disconnectHandler(event);
} }
/** /**
* @param {Object} data * @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); await this.errorHandler(data);
} }
@ -92,14 +128,26 @@ class Tourmanent extends AExchangeable
* *
* @param {MessageEvent} event * @param {MessageEvent} event
*/ */
onReceive(event) async onReceive(event)
{ {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.detail === "error") switch (data.detail) {
this.onError(data); case "error":
else if (["del_participant", "add_participant"].includes(data.detail)) await this._receiveError(data)
this._receiveParticipantUpdate(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} delParticipantHandler called when a participants leave the tournament
* @param {CallableFunction} disconnectHandler * @param {CallableFunction} disconnectHandler
* @param {CallableFunction} goToHandler called when the next game will start * @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()) if (!await this.client.isAuthenticated())
return null; return null;
@ -123,10 +173,13 @@ class Tourmanent extends AExchangeable
this.connected = true; this.connected = true;
this.isParticipating = false; this.isParticipating = false;
this.participantsUpdateHandler = participantsUpdateHandler; this._startHandler = startHandler;
this.errorHandler = errorHandler; this._finishHandler = finishHandler;
this.disconnectHandler = disconnectHandler; this._addParticipantHandler = addParticipantHandler;
this.goToHandler = goToHandler; this._delParticipantHandler = delParticipantHandler;
this._errorHandler = errorHandler;
this._disconnectHandler = disconnectHandler;
this._goToHandler = goToHandler;
this._socket.onmessage = this.onReceive.bind(this); this._socket.onmessage = this.onReceive.bind(this);

View File

@ -16,10 +16,9 @@ export default class extends AbstractAuthenticatedView
if (name.length == 0) if (name.length == 0)
name = lang.get("TournamentCreateTournamentName"); name = lang.get("TournamentCreateTournamentName");
console.log(name); let nb_participant = document.getElementById("nb-participant-input").value;
let nb_players = document.getElementById("nb-players-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 response_data = await response.json();
let id = response_data.id; let id = response_data.id;
@ -29,7 +28,7 @@ export default class extends AbstractAuthenticatedView
return; return;
} }
clearIds("innerHTML", ["name", "nb_players"]); clearIds("innerHTML", ["name", "nb_participants"]);
fill_errors(response_data, "innerHTML"); fill_errors(response_data, "innerHTML");
} }
@ -50,9 +49,16 @@ export default class extends AbstractAuthenticatedView
<span class='text-danger' id='name'></span> <span class='text-danger' id='name'></span>
</div> </div>
<div class='form-floating mb-2'> <div class='form-floating mb-2'>
<input type='number' class='form-control' min='2' value='4' id='nb-players-input' placeholder='${lang.get("TournamentCreateNbPlayer")}'> <select class='form-control' id="nb-participant-input">
<label for='nb-players-input' id='nb-players-label'>${lang.get("TournamentCreateNbPlayer")}</label> <option value="2">2</option>
<span class='text-danger' id='nb_players'></span> <option value="4">4</option>
<option value="8">8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
</select>
<label for='nb-participant-input' id='nb-participant-label'>${lang.get("TournamentCreateNbPlayer")}</label>
<span class='text-danger' id='nb_participants'></span>
</div> </div>
<div class='d-flex'> <div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='create-button'>${lang.get("TournamentCreateButton")}</button> <button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='create-button'>${lang.get("TournamentCreateButton")}</button>

View File

@ -3,6 +3,10 @@ import { Tourmanent } from "../../api/tournament/Tournament.js";
import {client, navigateTo} from "../../index.js"; import {client, navigateTo} from "../../index.js";
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
const TEXT_CONVENTION = {
"error": "[ERROR]"
}
export default class extends AbstractAuthenticatedView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
@ -13,95 +17,49 @@ export default class extends AbstractAuthenticatedView
pressButton() pressButton()
{ {
this.tournament.toggle_participation(); this.tournament.setParticipation(!this.tournament.isParticipating);
this.updateParticipating()
} }
createGraph() updateParticipating()
{ {
console.log(this.tournament); document.getElementById("button").value = this.tournament.isParticipating ? `Leave ${this.tournament.name}` : `Join ${this.tournament.name}`;
let tournament_tree = document.createElement("div"); document.getElementById("display").innerText = this.tournament.isParticipating ? "You are a particpant" : "You are not a participant";
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 = "&nbsp;";
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 = "&nbsp;";
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)
{
} }
async onDisconnect(event) async onDisconnect(event)
{ {
} }
async onError(data) 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() async postInit()
@ -114,20 +72,27 @@ export default class extends AbstractAuthenticatedView
if (this.tournament === null) if (this.tournament === null)
return 404; 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"); let button = document.getElementById("button");
button.onclick = this.pressButton.bind(this); button.onclick = this.pressButton.bind(this);
document.getElementById("name").innerText = this.tournament.name; 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("round").innerText = this.tournament.round;
document.getElementById("state").innerText = this.tournament.state; document.getElementById("state").innerText = this.tournament.state;
if (this.tournament.started === false) if (this.tournament.started === false)
button.disabled = false; button.disabled = false;
this.createGraph();
this.chat = document.getElementById("chat");
}
addChatMessage(message)
{
this.chat.innerText += message;
} }
async getHtml() async getHtml()
@ -150,13 +115,19 @@ export default class extends AbstractAuthenticatedView
<td id="nb_participants">Loading...</td> <td id="nb_participants">Loading...</td>
</tr> </tr>
<tr> <tr>
<td>status</td> <td>Expected number of participants</td>
<td id="expected_nb_participants">Loading...</td>
</tr>
<tr>
<td>state</td>
<td id="state">Loading...</td> <td id="state">Loading...</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<input type="button" id="button" value="Join tournament" disabled> <input type="button" id="button" value="Join tournament" disabled>
<span id="display"></span> <span id="display"></span>
<textarea id="chat" rows="4" cols="50" readonly>
</textarea>
`; `;
} }
} }

View File

@ -6,7 +6,7 @@ from django.contrib.auth.models import User
from django.db.models import QuerySet from django.db.models import QuerySet
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from games.models import GameModel
from profiles.models import ProfileModel from profiles.models import ProfileModel
from profiles.serializers.ProfileSerializer import ProfileSerializer from profiles.serializers.ProfileSerializer import ProfileSerializer
from .models import TournamentModel from .models import TournamentModel
@ -32,26 +32,27 @@ class TournamentMember:
self.send("error", data_to_send) 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: def _receive_participating(self, data: dict) -> None:
is_participating: bool | None = data.get("is_participating") is_participating: bool | None = data.get("is_participating")
if (is_participating is None): if (is_participating is None):
self.send_error(_("Missing is_participating statement.")) self.send_error(_("Missing is_participating statement."))
return return
self._room.set_participation()
self._room.set_participation(self, is_participating)
def receive(self, data: dict): def receive(self, data: dict):
if self.is_participating == False:
return
detail: str | None = data.get("detail") detail: str | None = data.get("detail")
if (detail is None): if (detail is None):
return return
match(detail): match(detail):
case "update_particapating": case "update_participating":
self._receive_participating() self._receive_participating(data)
case _: case _:
print("bozo_send") print("bozo_send")
@ -80,16 +81,35 @@ class TournamentRoom:
def __init__(self, room_manager: TournamentRoomManager, tournament: TournamentModel): def __init__(self, room_manager: TournamentRoomManager, tournament: TournamentModel):
self._room_manager: TournamentRoomManager = room_manager self._room_manager: TournamentRoomManager = room_manager
self._member_list: list[TournamentMember] = [] self._member_list: set[TournamentMember] = set()
self._model: TournamentModel = tournament self._model: TournamentModel = tournament
def join(self, socket: TournamentWebConsumer) -> TournamentMember: def join(self, socket: TournamentWebConsumer) -> TournamentMember:
member: TournamentMember = TournamentMember(socket, self) member: TournamentMember = TournamentMember(socket, self)
self._member_list.append(member) self._member_list.add(member)
return 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: def leave(self, member: TournamentMember) -> None:
# Delete room if nobody connected, no cringe memory leak # Delete room if nobody connected, no cringe memory leak
@ -101,9 +121,12 @@ class TournamentRoom:
self.set_participation(member, False) self.set_participation(member, False)
def broadcast(self, detail: str, data: dict, excludes: list[TournamentMember] = []) -> None: def everybody_is_here(self):
return len(self.get_participants()) == self._model.nb_participants
member_list: list[TournamentMember] = [member for member in self._member_list if member not in excludes] def broadcast(self, detail: str, data: dict, excludes: set[TournamentMember] = set()) -> None:
member_list: list[TournamentMember] = self._member_list - excludes
for member in member_list: for member in member_list:
member.send(detail, data) member.send(detail, data)
@ -120,12 +143,15 @@ class TournamentRoom:
return return
if (is_participating == True): 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: 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 member.is_participating = is_participating
if self.everybody_is_here():
self.start()
tournament_manager: TournamentRoomManager = TournamentRoomManager() tournament_manager: TournamentRoomManager = TournamentRoomManager()
class TournamentWebConsumer(WebsocketConsumer): class TournamentWebConsumer(WebsocketConsumer):

View File

@ -1,21 +1,12 @@
from __future__ import annotations 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 games.models import GameModel
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import CASCADE from django.db.models import CASCADE
from channels.generic.websocket import WebsocketConsumer
from django.db import models from django.db import models
import json
# Create your models here.tu
class TournamentModel(models.Model): class TournamentModel(models.Model):
name = models.CharField(max_length = 100) name = models.CharField(max_length = 100)
@ -23,12 +14,14 @@ class TournamentModel(models.Model):
round = models.IntegerField() round = models.IntegerField()
started = models.BooleanField(default = False) started = models.BooleanField(default = False)
finished = 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() 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 self.started = True
@ -36,11 +29,14 @@ class TournamentModel(models.Model):
self._register_participant(player) self._register_participant(player)
for (participant1, participant2) in zip(participants[0::2], participants[1::2]): 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() 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): if (self.started == False):
return None return None
@ -62,10 +58,10 @@ class TournamentModel(models.Model):
def get_games_by_round(self, round: int) -> list[GameModel]: 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)] 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)] 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)] return [game.winner for game in self.get_games_by_round(round)]
def get_participants(self) -> list[TournamentParticipantModel]: def get_participants(self) -> list[TournamentParticipantModel]:
@ -74,13 +70,12 @@ class TournamentModel(models.Model):
def get_state(self) -> str: def get_state(self) -> str:
return ("waiting to start", "in progress", "finish")[self.started + self.finished] 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() return TournamentParticipantModel.objects.filter(participant=profile, tournament=self).exists()
class TournamentParticipantModel(models.Model): 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) tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE)
#prout à encore frappé
class TournamentGameModel(models.Model): class TournamentGameModel(models.Model):

View File

@ -1,15 +1,13 @@
from rest_framework import serializers from rest_framework import serializers
from django.db.models.query import QuerySet
from django.contrib.auth.models import User
from .models import TournamentModel from .models import TournamentModel
from profiles.models import ProfileModel from profiles.models import ProfileModel
from profiles.serializers.ProfileSerializer import ProfileSerializer from profiles.serializers.ProfileSerializer import ProfileSerializer
from games.serializers import GameSerializer from games.serializers import GameSerializer
nb_participants = [2 ** i for i in range(2, 6)]
class TournamentSerializer(serializers.ModelSerializer): class TournamentSerializer(serializers.ModelSerializer):
state = serializers.SerializerMethodField(read_only=True, required=False) state = serializers.SerializerMethodField(read_only=True, required=False)
@ -30,16 +28,6 @@ class TournamentSerializer(serializers.ModelSerializer):
return ["waiting", "started", "finished"][instance.started + instance.finished] return ["waiting", "started", "finished"][instance.started + instance.finished]
def validate_nb_participants(self, value: int): def validate_nb_participants(self, value: int):
if (value < 2): if (value not in nb_participants):
raise serializers.ValidationError("The numbers of participants must be greather than 2.") raise serializers.ValidationError(f"The numbers of participants must be {str(nb_participants)}.")
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.")
return value return value