core: tournament

This commit is contained in:
starnakin 2024-04-16 19:13:18 +02:00
parent 14886c8ac9
commit 630ef709ab
10 changed files with 160 additions and 216 deletions

View File

@ -9,13 +9,13 @@ class Tourmanent
* @param {Number} name
* @param {Number} nb_players
* @param {Number} nb_players_by_game
* @param {Number} level
* @param {Number} round
* @param {Boolean} started
* @param {Boolean} finished
* @param {[]} levels
* @param {[]} rounds
* @param {String} state
*/
constructor(client, id, name = undefined, nb_players = undefined, nb_players_by_game = undefined, level = undefined, started = undefined, finished = undefined, levels = undefined, state = undefined)
constructor(client, id, name = undefined, nb_players = undefined, round = undefined, started = undefined, finished = undefined, rounds = undefined, state = undefined)
{
/**
* @type {Client}
@ -35,12 +35,7 @@ class Tourmanent
/**
* @type {Number}
*/
this.nb_players_by_game = nb_players_by_game;
/**
* @type {Number}
*/
this.level = level;
this.round = round;
/**
* @type {Number}
@ -55,7 +50,7 @@ class Tourmanent
/**
* @type {[]}
*/
this.levels = levels;
this.rounds = rounds;
/**
* @type {String} must be "finished", or "started", or "waiting". Any other return all elements
@ -86,13 +81,12 @@ class Tourmanent
let response_data = await response.json();
this.name = response_data.name || `${response_data.nb_players_by_game}x1, ${response_data.nb_players} players`;
this.name = response_data.name || `${response_data.nb_players} players tournament`;
this.nb_players = response_data.nb_players;
this.nb_players_by_game = response_data.nb_players_by_game;
this.level = response_data.level;
this.round = response_data.round;
this.started = response_data.started;
this.finished = response_data.finished;
this.levels = response_data.levels;
this.rounds = response_data.rounds;
this.state = response_data.state;
}
@ -112,11 +106,11 @@ class Tourmanent
this._socket.send(JSON.stringify({participate: ""}));
}
async onParticipantsUpdate(data)
async onPlayersUpdate(data)
{
oldParticipantList = this.par
oldPlayerList = this.par
await this.participantsUpdateHandler();
await this.playersUpdateHandler();
}
async onError(data)
@ -134,18 +128,18 @@ class Tourmanent
if (data?.detail === "error")
this.onError(data);
else if (data?.detail === "participants_update")
this.onParticipantsUpdate(data);
else if (data?.detail === "players_update")
this.onPlayersUpdate(data);
}
/**
* Join the tournament Websocket
* @param {CallableFunction} errorHandler
* @param {CallableFunction} participantsUpdateHandler
* @param {CallableFunction} playersUpdateHandler
* @param {CallableFunction} disconnectHandler
* @returns {?}
*/
async join(participantsUpdateHandler, errorHandler, disconnectHandler)
async join(playersUpdateHandler, errorHandler, disconnectHandler)
{
if (!await this.client.isAuthenticated())
return null;
@ -157,7 +151,7 @@ class Tourmanent
this.connected = true;
this.isParticipating = false;
this.participantsUpdateHandler = participantsUpdateHandler;
this.playersUpdateHandler = playersUpdateHandler;
this.errorHandler = errorHandler;
this.disconnectHandler = disconnectHandler;

View File

@ -30,13 +30,12 @@ class Tourmanents
/**
*
* @param {Number} nb_players
* @param {Number} nb_players_by_game
* @param {String} name
* @returns {Response}
*/
async createTournament(nb_players, nb_players_by_game, name = "")
async createTournament(nb_players, name = "")
{
let response = await this.client._post("/api/tournaments/", {nb_players: nb_players, nb_players_by_game: nb_players_by_game, name: name});
let response = await this.client._post("/api/tournaments/", {nb_players: nb_players, name: name});
return response;
}
@ -56,17 +55,16 @@ class Tourmanents
return null;
}
let tournaments = [];
let tournaments = [];``
response_data.forEach(tournament_data => {
tournaments.push(new Tourmanent(this.client,
tournament_data.name,
tournament_data.nb_players,
tournament_data.nb_players_by_game,
tournament_data.level,
tournament_data.round,
tournament_data.started,
tournament_data.finished,
tournament_data.levels,
tournament_data.rounds,
tournament_data.id,
tournament_data.state));
});

View File

@ -14,9 +14,8 @@ export default class extends AbstractAuthenticatedView
{
let name = document.getElementById("name-input").value;
let nb_players = document.getElementById("nb-players-input").value;
let nb_players_by_game = document.getElementById("nb-players-by-game-input").value;
let response = await client.tournaments.createTournament(nb_players, nb_players_by_game, name);
let response = await client.tournaments.createTournament(nb_players, name);
let response_data = await response.json();
let id = response_data.id;
@ -26,7 +25,7 @@ export default class extends AbstractAuthenticatedView
return;
}
clearIds("innerHTML", ["name", "nb_players", "nb_players_by_game"]);
clearIds("innerHTML", ["name", "nb_players"]);
fill_errors(response_data, "innerHTML");
}
@ -46,11 +45,6 @@ export default class extends AbstractAuthenticatedView
<label for='name-input' id='name-label'>${lang.get("TournamentCreateTournamentName")}</label>
<span class='text-danger' id='name'></span>
</div>
<div class='form-floating mb-2'>
<input type='number' class='form-control' min='2' max='4' value='2' id='nb-players-by-game-input' placeholder='${lang.get("TournamentCreateNbPlayerByGame")}'>
<label for='nb-players-by-game-input' id='nb-players-by-game-label'>${lang.get("TournamentCreateNbPlayerByGame")}</label>
<span class='text-danger' id='nb_players_by_game'></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>

View File

@ -1,8 +1,9 @@
from __future__ import annotations
from django.db import models
from django.db.models import QuerySet, CASCADE
from django.db.models import QuerySet
from profiles.models import ProfileModel
import time
@ -12,16 +13,15 @@ class GameModel(models.Model):
finished = models.BooleanField(default = False)
started = models.BooleanField(default = False)
winner_id = models.IntegerField(default = -1)
winner = models.ForeignKey(ProfileModel, 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, game_type: str, players_id: list[int]):
self.game_type = game_type
def create(self, players: list[ProfileModel]):
self.save()
for player_id in players_id:
GameMembersModel(game_id = self.pk, player_id = player_id).save()
for player in players:
GameMembersModel(game = self.pk, player=player).save()
return self.pk
def start(self):
@ -35,8 +35,8 @@ class GameModel(models.Model):
self.stop_timestamp = round(time.time() * 1000, 1)
self.save()
def get_players_id(self):
return [game_player.player_id for game_player in GameMembersModel.objects.filter(game_id = self.pk)]
def get_players(self) -> list[ProfileModel]:
return [game_player.player 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)
@ -55,11 +55,11 @@ class GameModel(models.Model):
return timestamp
class GameMembersModel(models.Model):
game_id = models.IntegerField()
player_id = models.IntegerField()
game = models.ForeignKey(GameModel, on_delete=CASCADE)
player = models.ForeignKey(ProfileModel, on_delete=CASCADE)
class GameGoalModel(models.Model):
game_id = models.IntegerField()
player_id = models.IntegerField()
game = models.ForeignKey(GameModel, on_delete=CASCADE)
player = models.ForeignKey(ProfileModel, on_delete=CASCADE)
timestamp = models.IntegerField()

View File

@ -6,18 +6,16 @@ from django.db.models import Q, Model, CASCADE, ForeignKey, ImageField
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from games.consumers import game_manager
def upload_to(instance, filename: str):
return f"./profiles/static/avatars/{instance.pk}{splitext(filename)[1]}"
class ProfileModel(Model):
user = ForeignKey(User, on_delete=CASCADE)
avatar = ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif")
def get_game(self) -> int:
from games.consumers import game_manager
for game in game_manager._game_list:
for player in game.get_players_connected():
if (player.user_id == self.user.pk):

View File

@ -6,27 +6,20 @@ from games.models import GameModel
import json
from .models import tournament_manager, TournamentMember, TournamentRoom, TournamentRoomManager
from .models import tournament_manager, TournamentPlayer, TournamentSpectator, TournamentRoom, TournamentRoomManager
class TournamentWebConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_name = "tournament"
self.group_name = "tournament"
def connect(self):
self.user: User = self.scope["user"]
if (self.user.is_anonymous or not self.user.is_authenticated):
return
self.channel_layer.group_add(self.group_name, self.channel_name)
self.tournament_id = int(self.scope['url_route']['kwargs']['tournament_id'])
self.room = tournament_manager.get(self.tournament_id)
self.member = TournamentMember(self.user.pk, self, self.room)
self.member: TournamentPlayer | TournamentSpectator = self.room(self.user.pk, self, self.room)
if (self.room is None):
self.member.send("Tournament not found")

View File

@ -1,155 +1,127 @@
from __future__ import annotations
from django.db import models
from channels.generic.websocket import WebsocketConsumer
from games.models import GameModel
import json
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
# Create your models here.tu
class TournamentModel(models.Model):
name = models.CharField(max_length = 100)
nb_players = models.IntegerField()
nb_players_by_game = models.IntegerField()
level = models.IntegerField()
rounds = models.IntegerField()
started = models.BooleanField(default = False)
finished = models.BooleanField(default = False)
gamemode = models.CharField(max_length = 50, default = "pong")
winner = models.ForeignKey(ProfileModel, on_delete=CASCADE)
def create_game(self, level, players_id):
game_id = GameModel().create(self.gamemode, players_id = players_id)
TournamentGamesModel(game_id = game_id, tournament_id = self.pk, tournament_level = level).save()
return game_id
def _add_player(self, player: ProfileModel) -> None:
TournamentPlayerModel(player=player, tournament=self).save()
def get_games_id_by_level(self, level):
tmp = TournamentGamesModel.objects.filter(tournament_id = self.pk, tournament_level = level)
return [instance.game_id for instance in tmp]
def start(self, players: list[ProfileModel]) -> None:
def get_games_id(self):
return [tournament_game.game_id for tournament_game in TournamentGamesModel.objects.filter(tournament_id = self.pk)]
self.started = False
def get_participants_id(self):
return [model.participant_id for model in TournamentParticipantsModel.objects.filter(tournament_id=self.pk)]
for player in players:
self._add_player(player)
def is_a_participant(self, participant_id: int):
return TournamentParticipantsModel.objects.filter(participant_id = participant_id, tournament_id = self.pk).exists()
for (player1, player2) in zip(players[0::2], players[1::2]):
self.create_game([player1, player2], round=1)
def add_participants(self, participants_id: [int]):
for participant_id in participants_id:
TournamentParticipantsModel(tournament_id = self.pk, participant_id = participant_id).save()
def start(self, participants_id: [int]):
self.started = True
self.add_participants(participants_id)
games_id = [int]
for i in range(0, len(participants_id), self.nb_players_by_game):
game_id = self.create_game(0, participants_id[i : i + self.nb_players_by_game])
games_id.append(game_id)
self.save()
return games_id
class TournamentParticipantsModel(models.Model):
tournament_id = models.IntegerField()
participant_id = models.IntegerField()
def create_game(self, players: list[ProfileModel], round: int) -> GameModel:
class TournamentGamesModel(models.Model):
if (self.started == False):
return None
tournament_id = models.IntegerField()
tournament_level = models.IntegerField()
game_id = models.IntegerField()
if (len(players) != 2):
return None
class TournamentMember(AbstractRoomMember):
from games.models import GameModel
def __init__(self, user_id: int, socket: WebsocketConsumer, room):
super().__init__(user_id, socket)
self.participate = False
self.room = room
game: GameModel = GameModel().create(players=players)
def receive(self, text_data: str = None, byte_dates: bytes = None):
TournamentGameModel(tournament=self, game=game, round=round).save()
if (text_data is None):
return
return game
data: dict = json.loads(text_data)
def start(self, players: list[ProfileModel]) -> int:
if (data.get("participate") is not None):
self.room.update_participants(self)
if (len(players) != self.nb_players):
return 1
def send_error_message(self, message: str):
self.send("error", {"error_message": message})
for player in players:
TournamentPlayerModel(tournament=self, player=player).save()
def go_to(self, url: str):
self.send("go_to", {"url": url})
return 0
def send_participating(self):
self.send("is_participant", {"is_participant": self.participate})
def get_games(self) -> list[GameModel]:
return [tournament_game.game for tournament_game in TournamentGameModel.objects.filter(tournament=self)]
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]:
return [game.get_players() for game in self.get_games_by_round(round)]
def get_winners_by_round(self, round: int) -> list[ProfileModel]:
return [game.winner for game in self.get_games_by_round(round)]
def get_players(self) -> list[TournamentPlayerModel]:
return TournamentPlayerModel.objects.filter(tournament=self.pk)
def get_state(self) -> str:
return ("waiting to start", "in progress", "finish")[self.started + self.finished]
def is_player(self, profile: ProfileModel) -> bool:
return TournamentPlayerModel.objects.filter(player=profile, tournament=self).exists()
class TournamentPlayerModel(models.Model):
player = models.ForeignKey(ProfileModel, on_delete=CASCADE)
tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE)
class TournamentGameModel(models.Model):
tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE, null=True, blank=True)
round = models.IntegerField()
game = models.ForeignKey(GameModel, on_delete=CASCADE)
class TournamentSpectator(AbstractRoomMember):
pass
class TournamentPlayer(TournamentSpectator):
pass
class TournamentRoom(AbstractRoom):
def __init__(self, room_manager, tournament_id: int):
def __init__(self, room_manager: TournamentRoomManager, tournament_id: int):
super().__init__(room_manager)
self.tournament_id = tournament_id
self.tournament = TournamentModel.objects.get(pk = tournament_id)
self.room_manager: TournamentRoomManager
self.id: int = id
self.model: TournamentModel = TournamentModel.objects.get(pk=tournament_id)
def start(self):
self.broadcast("tournament_start")
games_id = self.tournament.start(self.get_participants_id())
for i, participant in enumerate(self.get_participants()):
participant: TournamentMember
participant.go_to(f"games/{games_id[i // self.tournament.nb_players_by_game]}")
def update_participants(self, member: TournamentMember):
if (self.tournament.started):
member.send_error_message("Tournament already started")
return
member.participate = not member.participate
nb_participants = self.get_nb_participants()
self.broadcast("update_participants", {"nb_participants": nb_participants})
member.send_participating()
if (nb_participants == self.tournament.nb_players):
self.start()
def get_nb_participants(self):
if (self.tournament.started):
return self.tournament.nb_players
nb_participants = 0
for member in self._member_list:
member: TournamentMember
if (member.participate):
nb_participants += 1
return nb_participants
def get_participants(self):
return [member for member in self._member_list if member.participate]
def get_participants_id(self):
return [member.user_id for member in self._member_list if member.participate]
def append(self, member: TournamentMember):
super().append(member)
if self.tournament.started:
member.participate = self.tournament.is_a_participant(member.user_id)
member.send_participating()
self.broadcast("update_participants", {"participants": [self.get_participants_id()]})
def join(self, profile: ProfileModel, socket: WebsocketConsumer) -> TournamentPlayer | TournamentSpectator:
if (self.model.started):
if (self.model.is_player(profile)):
return TournamentPlayer(profile.pk, socket)
else:
return TournamentSpectator(profile.pk, socket)
else:
return TournamentSpectator(profile.pk, socket)
class TournamentRoomManager(AbstractRoomManager):
def get(self, tournament_id: int):
for room in self._room_list:
if (room.tournament_id == tournament_id):
return room
if (TournamentModel.objects.filter(pk = tournament_id).exists()):
room = TournamentRoom(self, tournament_id)
self.append(room)
return room
return None
pass
tournament_manager: TournamentRoomManager = TournamentRoomManager()

View File

@ -7,35 +7,35 @@ from django.contrib.auth.models import User
from .models import TournamentModel, tournament_manager
from profiles.models import ProfileModel
from profiles.serializers import ProfileSerializer
from profiles.serializers.ProfileSerializer import ProfileSerializer
from games.serializers import GameSerializer
class TournamentSerializer(serializers.ModelSerializer):
levels = serializers.SerializerMethodField(read_only=True, required=False)
rounds = 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()
players = serializers.SerializerMethodField(read_only=True, required=False)
round = serializers.ReadOnlyField()
started = serializers.ReadOnlyField()
finished = serializers.ReadOnlyField()
name = serializers.CharField(default="")
class Meta:
model = TournamentModel
fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id", "state", "participants"]
fields = ["name", "nb_players", "nb_players_by_game", "round", "started", "finished", "rounds", "id", "state", "players"]
def get_participants(self, instance: TournamentModel):
def get_players(self, instance: TournamentModel):
participants_id: list[ProfileModel]
players_id: list[ProfileModel]
if (instance.started):
participants_id = instance.get_participants_id()
players_id = instance.get_players_id()
else:
participants_id = tournament_manager.get(instance.pk).get_participants_id()
players_id = tournament_manager.get(instance.pk).get_players_id()
participants_profile: list[ProfileModel] = []
for participant_id in participants_id:
query: QuerySet = ProfileModel.objects.filter(user_id = participant_id)
players_profile: list[ProfileModel] = []
for player_id in players_id:
query: QuerySet = ProfileModel.objects.filter(user_id = player_id)
profile_data: dict
if query.exists():
profile_data = ProfileSerializer(query[0]).data
@ -43,23 +43,24 @@ class TournamentSerializer(serializers.ModelSerializer):
profile_data = {
"username": "deleted_user",
"avatar": "/static/avatars/default.avif",
"user_id": participants_id
"user_id": players_id
}
participants_profile.append(profile_data)
players_profile.append(profile_data)
return players_profile
return participants_profile
def get_state(self, instance: TournamentModel):
return ["waiting", "started", "finished"][instance.started + instance.finished]
def get_levels(self, instance: TournamentModel):
levels: list[list[int]] = []
for i in range(instance.level):
games_id: [int] = instance.get_games_id_by_level(i)
def get_rounds(self, instance: TournamentModel):
rounds: list[list[int]] = []
for i in range(instance.round):
games_id: list[int] = instance.get_games_id_by_round(i)
if (games_id == []):
break
levels.append(games_id)
return levels
rounds.append(games_id)
return rounds
def validate_nb_players(self, value: int):
if (value < 2):

View File

@ -21,14 +21,8 @@ class TournamentViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer: TournamentSerializer):
nb_players = serializer.validated_data["nb_players"]
nb_players_by_game = serializer.validated_data["nb_players_by_game"]
level = 1
number: int = nb_players
while (number != nb_players_by_game):
number = number // 2 + (number % 2)
level += 1
tournament = serializer.save(level = level)
name = serializer.validated_data["name"]
tournament = serializer.save(name=name, nb_players=nb_players, round=1)
return Response(self.serializer_class(tournament).data, status=status.HTTP_201_CREATED)

View File

@ -3,7 +3,7 @@ from .AbstractRoom import AbstractRoom
class AbstractRoomManager:
def __init__(self):
self._room_list: [AbstractRoom] = []
self._room_list: list[AbstractRoom] = []
def remove(self, room: AbstractRoom):
self._room_list.remove(room)