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

@ -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);

View File

@ -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
<span class='text-danger' id='name'></span>
</div>
<div class='form-floating mb-2'>
<input type='number' class='form-control' min='2' value='4' id='nb-players-input' placeholder='${lang.get("TournamentCreateNbPlayer")}'>
<label for='nb-players-input' id='nb-players-label'>${lang.get("TournamentCreateNbPlayer")}</label>
<span class='text-danger' id='nb_players'></span>
<select class='form-control' id="nb-participant-input">
<option value="2">2</option>
<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 class='d-flex'>
<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 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 = "&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)
{
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
<td id="nb_participants">Loading...</td>
</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>
</tr>
</tbody>
</table>
<input type="button" id="button" value="Join tournament" disabled>
<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.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()

View File

@ -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):

View File

@ -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