52 Commits

Author SHA1 Message Date
072944c503 game: add: config 2023-12-28 15:21:02 +01:00
5db03a3c69 tournament: fix 2023-12-28 15:20:25 +01:00
231622a2c2 game: init: consumer 2023-12-28 12:02:39 +01:00
f11af8336a game: init: class 2023-12-28 12:01:52 +01:00
2e8eebc788 game: allow any to get game data 2023-12-28 11:34:18 +01:00
bfc58e74a9 game: add: retrive view 2023-12-28 11:33:06 +01:00
4739afbaf0 block: fix: .exists() 2023-12-28 11:32:49 +01:00
9f7b813292 chat: fix and opti 2023-12-27 16:14:39 +01:00
cfb14be945 Merge branch 'Chatte' into server 2023-12-27 09:59:19 +01:00
35a2f273f9 fix 2023-12-26 21:35:08 +01:00
98ec43e7b9 game: core: rename member to player 2023-12-26 21:32:20 +01:00
9bb6a32c70 tournament: add: TOURNAMENT CAN START 2023-12-26 18:24:23 +01:00
44fa122585 clean: tournament: remove debug print 2023-12-26 14:35:45 +01:00
0d926e78c1 fix: chat url 2023-12-26 14:33:08 +01:00
efbcd10fb0 fix: websocket url 2023-12-26 14:28:04 +01:00
52c7d9eafb add: websocket protocol now depends on http's 2023-12-26 01:03:09 +01:00
8ba55d5be2 tournament: add: player can join tournament now 2023-12-24 16:58:36 +01:00
2932c2af1f abstract room: append: fix: use right var 2023-12-24 15:37:31 +01:00
397344ee45 matchmaking: add: clear message if normal closure 2023-12-23 18:47:16 +01:00
88aca3a54b fix: de golmon 2023-12-23 18:36:16 +01:00
58f6214ad0 fix: de golmon 2023-12-23 18:34:18 +01:00
1d751b00ed Merge remote-tracking branch 'refs/remotes/origin/server' into server 2023-12-23 18:33:25 +01:00
699abc9d1b core: use abstract room, member, roommanager 2023-12-23 18:30:45 +01:00
4e21347199 rename the project 2023-12-23 18:12:13 +01:00
0db73216b3 rename the project 2023-12-23 18:11:30 +01:00
f9bf3a566e matchmaking: add: players counter in room 2023-12-23 16:58:03 +01:00
39ea33cc8e fix: go to url when game found 2023-12-23 16:56:04 +01:00
8529963f97 change matchmaking to authenfied view 2023-12-23 16:49:43 +01:00
c02c49215d fix: matchmaking support start stop, multi connection 2023-12-23 16:47:22 +01:00
6c39a13aca core: recreation of matchmaking, add: matchmaking support multiple modes 2023-12-23 12:54:33 +01:00
278e2cbe54 add: tournament create view 2023-12-22 14:26:06 +01:00
587980d637 add: tournament list view 2023-12-21 11:35:47 +01:00
bcf6868150 fix: de golmon 2023-12-21 10:41:39 +01:00
4eaa35584d fix: return 404 when tournament not found 2023-12-21 00:23:10 +01:00
6295c5120f add: tournament page 2023-12-21 00:21:18 +01:00
8e0514514b fix: check if profile existe befort render profile page 2023-12-21 00:05:54 +01:00
0626faae7f core: edit renderView engine
to display 404 error on not found page
2023-12-21 00:04:48 +01:00
5ba432e9fd add: default value to tournament.name 2023-12-20 22:49:48 +01:00
789a83cb41 add: test to tournament 2023-12-20 22:49:29 +01:00
35cd728b3c core: move tournament url 2023-12-20 22:49:18 +01:00
be43e45947 fix: profiles test 2023-12-20 22:37:44 +01:00
9bdd07d59e change tounament: viewset, serializer, models
simply the code and use django feat
2023-12-20 21:58:52 +01:00
9523ac4554 change url patern to tournament 2023-12-20 21:57:16 +01:00
56cfd563d7 fix: model, limite length of
tournament.name and winner_id can be null now
2023-12-20 21:11:37 +01:00
288252ab37 remove: useless function 2023-12-20 21:10:24 +01:00
adf0c5ceed core: change tournament api url 2023-12-20 21:10:12 +01:00
1032a8fd98 core: recreation of tournament view by using
queryset
2023-12-20 21:09:31 +01:00
9714918de9 Merge remote-tracking branch 'refs/remotes/origin/server' into server 2023-12-20 19:15:59 +01:00
bfcfabb35a init: tournament 2023-12-20 19:15:47 +01:00
8ed9deab2d add: attribute game state to gamemodel 2023-12-20 19:11:56 +01:00
4f0b2250bd fix 2023-12-20 19:10:26 +01:00
0e946b3bb8 fix: gameModel.create is not cringe now 2023-12-18 23:18:39 +01:00
61 changed files with 1436 additions and 186 deletions

View File

@ -25,6 +25,7 @@ pip install -r requirements.txt
python manage.py makemigrations games python manage.py makemigrations games
python manage.py makemigrations profiles python manage.py makemigrations profiles
python manage.py makemigrations chat python manage.py makemigrations chat
python manage.py makemigrations tournament
python manage.py migrate python manage.py migrate
``` ```
- Start the developpement server - Start the developpement server

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import ChannelModel, MemberModel, MessageModel from .models import ChatChannelModel, ChatMemberModel, ChatMessageModel
admin.site.register(ChannelModel) admin.site.register(ChatChannelModel)
admin.site.register(MemberModel) admin.site.register(ChatMemberModel)
admin.site.register(MessageModel) admin.site.register(ChatMessageModel)

View File

@ -1,21 +1,25 @@
import json
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from .models import MemberModel, MessageModel
from .models import ChatMemberModel, ChatMessageModel
from profiles.models import BlockModel from profiles.models import BlockModel
import time import time
import json
class ChatConsumer(WebsocketConsumer): class ChatConsumer(WebsocketConsumer):
def connect(self): def connect(self):
channel_id : str = self.scope['path'].split('/')[3]
self.room_group_name = 'chat' + channel_id
user = self.scope["user"] user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
return return
if MemberModel.objects.filter(member_id=user.pk, channel_id=int(channel_id)).count() != 1: channel_id : int = int(self.scope['url_route']['kwargs']['chat_id'])
self.room_group_name = f'chat{channel_id}'
if ChatMemberModel.objects.filter(member_id=user.pk, channel_id=int(channel_id)).count() != 1:
return return
if (self.channel_layer == None): if (self.channel_layer == None):
@ -30,27 +34,33 @@ class ChatConsumer(WebsocketConsumer):
def receive(self, text_data=None, bytes_data=None): def receive(self, text_data=None, bytes_data=None):
if text_data == None: if text_data == None:
return return
text_data_json = json.loads(text_data)
message = text_data_json['message']
receivers_id = text_data_json['receivers_id']
print(text_data)
channel_id : int = int(self.scope['path'].split('/')[3])
user = self.scope["user"] user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
return return
if MemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1: text_data_json: dict = json.loads(text_data)
message = text_data_json.get('message')
if (message is None):
return
receivers_id = text_data_json.get('receivers_id')
if (receivers_id is None):
return
channel_id : int = int(self.scope['url_route']['kwargs']['chat_id'])
if ChatMemberModel.objects.filter(member_id = user.pk, channel_id = channel_id).count() != 1:
return return
if (self.channel_layer == None): if (self.channel_layer == None):
return return
message_time : int = int(time.time() * 1000) message_time: int = int(time.time() * 1000)
if (len(receivers_id) == 1 and if (len(receivers_id) == 1 and
BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or
@ -68,22 +78,17 @@ class ChatConsumer(WebsocketConsumer):
} }
) )
new_message = MessageModel() new_message = ChatMessageModel(channel_id = channel_id, author_id = user.pk, content = message, time = message_time).save()
new_message.channel_id = channel_id
new_message.author_id = user.pk
new_message.content = message
new_message.time = message_time
new_message.save()
def chat_message(self, event): def chat_message(self, event):
channel_id : int = int(self.scope['path'].split('/')[3])
user = self.scope["user"] user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
return return
if MemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1: channel_id : int = int(self.scope['url_route']['kwargs']['chat_id'])
if ChatMemberModel.objects.filter(member_id = user.pk, channel_id = channel_id).count() != 1:
return return
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({

View File

@ -4,21 +4,28 @@ from django.contrib.auth.models import User
from django.contrib import admin from django.contrib import admin
# Create your models here. # Create your models here.
class ChannelModel(models.Model): class ChatChannelModel(models.Model):
pass
def create(self, users_id: [int]):
self.save()
for user_id in users_id:
ChatMemberModel(channel_id = self.pk, member_id = user_id).save()
class MemberModel(models.Model): def get_members_id(self):
return [member_channel.member_id for member_channel in ChatMemberModel.objects.filter(channel_id = self.pk)]
class ChatMemberModel(models.Model):
member_id = IntegerField(primary_key=False) member_id = IntegerField(primary_key=False)
channel_id = IntegerField(primary_key=False) channel_id = IntegerField(primary_key=False)
def __str__(self): def __str__(self):
return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id) return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id)
class MessageModel(models.Model): class ChatMessageModel(models.Model):
channel_id = IntegerField(primary_key=False) channel_id = IntegerField(primary_key=False)
author_id = IntegerField(primary_key=False) author_id = IntegerField(primary_key=False)
content = models.CharField(max_length=255) content = models.CharField(max_length=255)
time = IntegerField(primary_key=False) time = IntegerField(primary_key=False)
def __str__(self): def __str__(self):
return "author_id: " + str(self.author_id) + ", channel_id: " + str(self.channel_id) + ", content: " + self.content return "author_id: " + str(self.author_id) + ", channel_id: " + str(self.channel_id) + ", content: " + self.content

View File

@ -2,5 +2,5 @@ from django.urls import re_path
from . import consumers from . import consumers
websocket_urlpatterns = [ websocket_urlpatterns = [
re_path(r'ws/chat/(?P<chat_id>\d+)/$', consumers.ChatConsumer.as_asgi()) re_path(r'ws/chat/(?P<chat_id>\d+)$', consumers.ChatConsumer.as_asgi())
] ]

View File

@ -1,8 +1,30 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ChannelModel
class ChannelSerializer(serializers.ModelSerializer): from profiles.models import ProfileModel
from .models import ChatChannelModel, ChatMessageModel
class ChatChannelSerializer(serializers.ModelSerializer):
members_id = serializers.ListField(child = serializers.IntegerField())
class Meta: class Meta:
model = ChannelModel model = ChatChannelModel
fields = [] fields = ["members_id", "pk"]
def validate_members_id(self, value):
members_id: [int] = value
if len(members_id) < 2:
raise serializers.ValidationError('Not enought members to create the channel')
if len(set(members_id)) != len(members_id):
raise serializers.ValidationError('Same member')
for member_id in members_id:
if not ProfileModel.objects.filter(pk = member_id).exists():
raise serializers.ValidationError(f"The profile {member_id} doesn't exists.")
return members_id
class ChatMessageSerializer(serializers.Serializer):
class Meta:
model = ChatMessageModel
fields = ["channel_id", "author_id", "content", "time"]

View File

@ -1,3 +1,30 @@
from django.test import TestCase from django.test import TestCase
from django.test.client import Client
from django.http import HttpResponse, HttpRequest
from django.contrib.auth.models import User
# Create your tests here. # Create your tests here.
class ChatTest(TestCase):
def setUp(self):
self.client = Client()
self.username='bozo1'
self.password='password'
self.user: User = User.objects.create_user(username=self.username, password=self.password)
self.dest: User = User.objects.create_user(username="bozo2", password=self.password)
self.url = "/api/chat/"
def test_create_chat(self):
self.client.login(username=self.username, password=self.password)
response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]})
response_dict: dict = eval(response.content)
self.assertDictEqual(response_dict, {})
def test_create_chat_unlogged(self):
response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]})
response_dict: dict = eval(response.content)
self.assertDictEqual(response_dict, {'detail': 'Authentication credentials were not provided.'})

View File

@ -5,6 +5,5 @@ from django.conf.urls.static import static
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("<int:pk>", views.ChatView.as_view(), name="chat_page"), path("", views.ChannelView.as_view(), name="chats_page"),
path("", views.ChatsView.as_view(), name="chats_page"),
] ]

View File

@ -1,79 +1,45 @@
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import authentication, permissions, status from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from .models import ChannelModel, MemberModel, MessageModel
from django.http import HttpRequest
from django.contrib.auth import login
from django.db.models import QuerySet
from django.core import serializers from django.core import serializers
class ChatView(APIView):
from .models import ChatChannelModel, ChatMemberModel, ChatMessageModel
from .serializers import ChatChannelSerializer, ChatMessageSerializer
class ChannelView(APIView):
queryset = ChatChannelModel.objects
serializer_class = ChatChannelSerializer
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def get(self, request, pk):
if (ChannelModel.objects.filter(pk=pk)):
return Response({'channel_id': pk}, status=status.HTTP_200_OK)
else:
return Response("Channel doesn't exist", status=status.HTTP_404_NOT_FOUND)
def delete(self, request, pk):
ChannelModel.objects.filter(pk=pk).delete()
MessageModel.objects.filter(pk=pk).delete()
MemberModel.objects.filter(pk=pk).delete()
return Response({'channel_id': pk}, status=status.HTTP_200_OK)
class ChatsView(APIView):
def post(self, request): def post(self, request):
data: dict = request.data serializer = self.serializer_class(data = request.data)
users_id = request.data.get("users_id", [])
if len(users_id) < 2: serializer.is_valid(raise_exception=True)
return Response('Not enought members to create the channel', status=status.HTTP_400_BAD_REQUEST)
if users_id[0] == users_id[1]:
return Response('Same member', status=status.HTTP_400_BAD_REQUEST)
for user_id1 in users_id: data: dict = serializer.validated_data
for member1 in MemberModel.objects.filter(member_id=user_id1):
for user_id2 in users_id:
if user_id1 == user_id2:
continue
for member2 in MemberModel.objects.filter(member_id=user_id2):
if (member1.channel_id == member2.channel_id):
messages = MessageModel.objects.filter(channel_id=member1.channel_id).order_by("time")
messages = serializers.serialize("json", messages)
return Response({'channel_id': member1.channel_id, 'messages':messages}, status=status.HTTP_200_OK)
new_channel = ChannelModel()
new_channel.save()
for user_id in users_id: members_id = data.get("members_id")
if self.request.user.pk not in members_id:
return Response({"detail": "You are trying to create a chat group without you."}, status = status.HTTP_400_BAD_REQUEST)
new_member = MemberModel() for member_channel in ChatMemberModel.objects.filter(member_id = members_id[0]):
new_member.channel_id = new_channel.pk channel_id: int = member_channel.channel_id
new_member.member_id = user_id if not ChatChannelModel.objects.filter(pk = channel_id).exists():
new_member.save() continue
channel: ChatChannelModel = ChatChannelModel.objects.get(pk = channel_id)
if set(channel.get_members_id()) == set(members_id):
messages = ChatMessageModel.objects.filter(channel_id = channel_id).order_by("time")
messages = serializers.serialize("json", messages)
return Response({'channel_id': channel_id, 'messages': messages}, status=status.HTTP_200_OK)
return Response({'channel_id': new_channel.pk}, status=status.HTTP_201_CREATED) new_channel_id = ChatChannelModel().create(members_id)
return Response({'channel_id': new_channel_id}, status=status.HTTP_201_CREATED)
def delete(self, request):
data: dict = request.data
users_id = request.data.get("users_id", [])
#print(list(MemberModel.objects.all()))
for user_id1 in users_id:
for member1 in MemberModel.objects.filter(member_id=user_id1):
for user_id2 in users_id:
if user_id1 == user_id2:
break
for member2 in MemberModel.objects.filter(member_id=user_id2):
if (member1.channel_id == member2.channel_id):
MessageModel.objects.filter(channel_id=member1.channel_id).delete()
member1.delete()
member2.delete()
ChannelModel.objects.get(pk=member1.channel_id).delete()
return Response("Channel removed", status=status.HTTP_200_OK)
return Response("Channel doesn't exist", status=status.HTTP_404_NOT_FOUND)

View File

@ -33,7 +33,7 @@ class Account
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{ {
this.client._logged = false; this.client._update_logged(false);
return null; return null;
} }
if (response_data == "user deleted") if (response_data == "user deleted")

View File

@ -14,7 +14,7 @@ class Channel {
// reload = function to use when we receive a message // reload = function to use when we receive a message
async connect(reload) { async connect(reload) {
let url = `ws://${window.location.host}/ws/chat/${this.channel_id}/`; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/chat/${this.channel_id}`;
this.chatSocket = new WebSocket(url); this.chatSocket = new WebSocket(url);
this.chatSocket.onmessage = (event) =>{ this.chatSocket.onmessage = (event) =>{
@ -35,7 +35,9 @@ class Channel {
this.chatSocket.close(); this.chatSocket.close();
} }
updateMessages(messages) { updateMessages(messages)
{
console.log(messages);
messages = JSON.parse(messages); messages = JSON.parse(messages);
let new_messages = []; let new_messages = [];

View File

@ -6,18 +6,18 @@ class Channels {
this.client = client; this.client = client;
} }
async createChannel(users_id, reload) { async createChannel(members_id, reload) {
let null_id = false; let null_id = false;
users_id.forEach(user_id => { members_id.forEach(member_id => {
if (user_id == null) if (member_id == null)
null_id = true; null_id = true;
}); });
if (null_id) if (null_id)
return console.log(users_id, "createChannel error, null id;"); return console.log(members_id, "createChannel error, null id;");
let response = await this.client._post("/api/chat/", { let response = await this.client._post("/api/chat/", {
users_id:users_id members_id:members_id
}); });
let data = await response.json(); let data = await response.json();
@ -28,12 +28,13 @@ class Channels {
let messages = undefined; let messages = undefined;
if (exit_code == 200) if (exit_code == 200)
messages = data.messages; messages = data.messages;
return new Channel(this.client, data.channel_id, users_id, messages, reload);
return new Channel(this.client, data.channel_id, members_id, messages, reload);
} }
async deleteChannel(users_id) { async deleteChannel(members_id) {
let response = await this.client._delete("/api/chat/", { let response = await this.client._delete("/api/chat/", {
users_id:users_id members_id:members_id
}); });
let data = await response.json(); let data = await response.json();

View File

@ -3,6 +3,8 @@ import { MatchMaking } from "./matchmaking.js";
import { Profiles } from "./profiles.js"; import { Profiles } from "./profiles.js";
import { Channels } from './chat/channels.js'; import { Channels } from './chat/channels.js';
import { MyProfile } from "./MyProfile.js"; import { MyProfile } from "./MyProfile.js";
import { navigateTo } from "../index.js"
import { Tourmanents } from "./tournament/tournaments.js";
function getCookie(name) function getCookie(name)
{ {
@ -22,6 +24,7 @@ class Client
this.account = new Account(this); this.account = new Account(this);
this.profiles = new Profiles(this); this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this); this.matchmaking = new MatchMaking(this);
this.tournaments = new Tourmanents(this);
this._logged = undefined; this._logged = undefined;
this.channels = new Channels(this); this.channels = new Channels(this);
@ -102,6 +105,10 @@ class Client
this.me = new MyProfile(this); this.me = new MyProfile(this);
await this.me.init(); await this.me.init();
} }
if (this.logged && !state)
{
navigateTo("/login");
}
this.logged = state; this.logged = state;
} }

View File

@ -8,30 +8,45 @@ class MatchMaking
constructor(client) constructor(client)
{ {
/** /**
* @type {client} * @type {Client}
*/ */
this.client = client this.client = client
this.searching = false;
} }
async start(func) async start(receive_func, disconnect_func, mode)
{ {
if (!await this.client.isAuthentificate()) if (!await this.client.isAuthentificate())
return null; return null;
let url = `wss://${window.location.host}/ws/matchmaking/`; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`;
this._socket = new WebSocket(url);
this._chatSocket = new WebSocket(url); this.searching = true;
this._chatSocket.onmessage = function (event) { this.receive_func = receive_func;
this.disconnect_func = disconnect_func;
this._socket.onmessage = function (event) {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
func(data.game_id) receive_func(data);
}; };
this._socket.onclose = this.onclose.bind(this);
}
onclose(event)
{
this.stop();
this.disconnect_func(event);
} }
async stop() async stop()
{ {
this._chatSocket.close() this.searching = false;
this._socket.close()
} }
} }
export {MatchMaking} export {MatchMaking}

View File

@ -20,6 +20,10 @@ class Profile
async init(user_id) async init(user_id)
{ {
let response = await this.client._get(`/api/profiles/${user_id}`); let response = await this.client._get(`/api/profiles/${user_id}`);
if (response.status === 404)
return 1;
let response_data = await response.json(); let response_data = await response.json();
this.user_id = response_data.user_id; this.user_id = response_data.user_id;

View File

@ -28,7 +28,8 @@ class Profiles
async getProfile(user_id) async getProfile(user_id)
{ {
let profile = new Profile(this.client); let profile = new Profile(this.client);
await profile.init(user_id); if (await profile.init(user_id))
return null;
return profile; return profile;
} }

View File

@ -0,0 +1,98 @@
import { Client } from "../client.js";
class Tourmanent
{
/**
* @param {Client} client
*/
constructor(client, name = undefined, nb_players = undefined, nb_players_by_game = undefined, level = undefined, started = undefined, finished = undefined, levels = undefined, id = undefined)
{
/**
* @type {Client}
*/
this.client = client;
this.name = name || `${nb_players_by_game}x1, ${nb_players} players`;
this.nb_players = nb_players;
this.nb_players_by_game = nb_players_by_game;
this.level = level;
this.started = started;
this.finished = finished;
this.levels = levels;
this.state = this.get_state();
this.id = id
this.connected = false;
}
get_state()
{
if (this.finished)
return "finished";
if (this.started)
return "started";
else
return "waiting";
}
async init(id)
{
let response = await this.client._get(`/api/tournaments/${id}`);
if (response.status === 404)
return 1;
let response_data = await response.json();
this.name = response_data.name || `${response_data.nb_players_by_game}x1, ${response_data.nb_players} players`;
this.nb_players = response_data.nb_players;
this.nb_players_by_game = response_data.nb_players_by_game;
this.level = response_data.level;
this.started = response_data.started;
this.finished = response_data.finished;
this.levels = response_data.levels;
this.id = response_data.id
this.state = this.get_state();
}
leave(event)
{
if (this.connected == false)
return
this.connected = false;
this._socket.close()
this.disconnect_func(event);
}
toggle_participation()
{
if (!this.connected)
return
this._socket.send(JSON.stringify({participate: ""}));
}
async join(receive_func, disconnect_func)
{
if (!await this.client.isAuthentificate())
return null;
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/tournaments/${this.id}`;
this._socket = new WebSocket(url);
this.connected = true;
this.isParticipating = false;
this.receive_func = receive_func;
this.disconnect_func = disconnect_func;
this._socket.onmessage = function (event) {
const data = JSON.parse(event.data);
receive_func(data);
};
this._socket.onclose = this.leave.bind(this);
}
}
export { Tourmanent }

View File

@ -0,0 +1,78 @@
import { Client } from "../client.js";
import { Tourmanent } from "./tournament.js";
class Tourmanents
{
/**
* @param {Client} client
*/
constructor(client)
{
/**
* @type {Client}
*/
this.client = client
}
async getTournament(id)
{
let tournament = new Tourmanent(this.client);
if (await tournament.init(id))
return null;
return tournament;
}
async createTournament(nb_players, nb_players_by_game, name = "")
{
let response = await this.client._post("/api/tournaments/", {nb_players: nb_players, nb_players_by_game: nb_players_by_game, name: name});
if (response.status === 403)
{
this.client._update_logged(false);
return null;
}
let response_data = await response.json();
return response_data;
}
/**
* @param {string} state must be "finished", or "started", or "waiting". Any other return all elements
*/
async search(state)
{
let response = await this.client._get(`/api/tournaments/search/${state}`);
let response_data = await response.json()
if (response.status === 404)
{
this.client._update_logged(false);
return null;
}
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.started,
tournament_data.finished,
tournament_data.levels,
tournament_data.id));
});
return tournaments;
}
async all()
{
return await this.search("");
}
}
export { Tourmanents }

View File

@ -14,6 +14,9 @@ import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import MeView from "./views/MeView.js";
import ProfilePageView from "./views/ProfilePageView.js"; import ProfilePageView from "./views/ProfilePageView.js";
import MatchMakingView from "./views/MatchMakingView.js"; import MatchMakingView from "./views/MatchMakingView.js";
import TournamentPageView from "./views/TournamentPageView.js";
import TournamentsView from "./views/TournamentsListView.js";
import TournamentCreateView from "./views/TournamentCreateView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.protocol + "//" + location.host)
@ -35,10 +38,26 @@ const navigateTo = async (uri) => {
history.pushState(null, null, uri); history.pushState(null, null, uri);
}; };
async function renderView(view)
{
let content = await view.getHtml();
if (content == null)
return 1;
view.setTitle();
document.querySelector("#app").innerHTML = content
if (await view.postInit())
renderView(new PageNotFoundView());
}
const router = async (uri) => { const router = async (uri) => {
const routes = [ const routes = [
{ path: "/", view: Dashboard }, { path: "/", view: Dashboard },
{ path: "/profiles/:id", view: ProfilePageView }, { path: "/profiles/:id", view: ProfilePageView },
{ path: "/tournaments/create", view: TournamentCreateView },
{ path: "/tournaments/:id", view: TournamentPageView },
{ path: "/tournaments/", view: TournamentsView },
{ path: "/login", view: LoginView }, { path: "/login", view: LoginView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
{ path: "/register", view: RegisterView }, { path: "/register", view: RegisterView },
@ -80,14 +99,7 @@ const router = async (uri) => {
lastView = view; lastView = view;
await client.isAuthentificate(); await client.isAuthentificate();
let content = await view.getHtml(); renderView(view);
if (content == null)
return 1;
view.setTitle();
document.querySelector("#app").innerHTML = content
await view.postInit();
return 0; return 0;
}; };

View File

@ -1,24 +1,64 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../index.js";
import AbstractView from "./abstracts/AbstractView.js"; import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js";
function game_found(game_id) export default class extends AbstractAuthentifiedView {
{ constructor(params)
navigateTo(`/games/${game_id}`) {
} super(params, "Matchmaking");
}
export default class extends AbstractView { async press_button()
constructor(params) { {
super(params, "Dashboard"); if (client.matchmaking.searching)
{
client.matchmaking.stop();
document.getElementById("button").value = "Find a game"
}
else
{
let nb_players = document.getElementById("nb_players-input").value
await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players);
document.getElementById("button").value = "Stop matchmaking"
}
}
ondisconnect(event)
{
if (event.code === 1000)
clear("innerText", ["detail"])
document.getElementById("button").value = "Find a game"
}
onreceive(data)
{
if (data.detail === "game_found")
{
navigateTo(`/games/${data.game_id}`);
return;
}
this.display_data(data)
}
display_data(data)
{
clear("innerText", ["detail"]);
fill_errors(data, "innerText");
} }
async postInit() async postInit()
{ {
await client.matchmaking.start(game_found) document.getElementById("button").onclick = this.press_button.bind(this)
} }
async getHtml() { async getHtml() {
return ` return `
<h1>finding<h1> <h1>Select mode</h1>
<input type="number" value="2" id="nb_players-input">
<input type="button" value="Find a game" id="button">
<span id="detail"></span>
`; `;
} }

View File

@ -9,6 +9,11 @@ export default class extends AbstractView {
async postInit() async postInit()
{ {
let profile = await client.profiles.getProfile(this.user_id);
if (profile === null)
return 1;
this.profile = await client.profiles.getProfile(this.user_id); this.profile = await client.profiles.getProfile(this.user_id);
this.info = document.getElementById("info"); this.info = document.getElementById("info");

View File

@ -0,0 +1,52 @@
import {client, navigateTo} from "../index.js";
import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView
{
constructor(params)
{
super(params, "Create tournament");
this.id = params.id;
}
async create()
{
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_data = await client.tournaments.createTournament(nb_players, nb_players_by_game, name);
if (response_data === null)
return;
let id = response_data["id"]
if (id !== undefined)
{
navigateTo(`/tournaments/${id}`);
return;
}
clear("innerHTML", ["name", "nb_players", "nb_players_by_game"]);
fill_errors(response_data, "innerHTML");
}
async postInit()
{
document.getElementById("create-button").onclick = this.create;
}
async getHtml()
{
return `
<input type="text" id="name-input" placeholder="Tournament name">
<span id="name"></span>
<input type="number" id="nb_players-input" placeholder="Number of players in tournament">
<span id="nb_players"></span>
<input type="number" id="nb_players_by_game-input" placeholder="Number of players by game">
<span id="nb_players_by_game"></span>
<input type="button" id="create-button" value="Create tournament">
`
}
}

View File

@ -0,0 +1,98 @@
import {client, navigateTo} from "../index.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView
{
constructor(params)
{
super(params, "Tournament");
this.id = params.id;
}
pressButton()
{
this.tournament.toggle_participation();
}
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 === "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";
}
async ondisconnect(event)
{
}
async postInit()
{
this.tournament = await client.tournaments.getTournament(this.id);
if (this.tournament === null)
return 1;
this.tournament.join(this.receive.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_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.state === "waiting")
button.disabled = false;
}
async getHtml()
{
return `
<table>
<thead>
<tr>
<th id="name">Loading...</th>
</tr>
</thead>
<tbody>
<tr>
<td>Number of players</td>
<td id="nb_players">Loading...</td>
</tr>
<tr>
<td>Number of players by game</td>
<td id="nb_players_by_game">Loading...</td>
</tr>
<tr>
<td>Number of round</td>
<td id="level">Loading...</td>
</tr>
<tr>
<td>Number of player</td>
<td id="nb_participants">Loading...</td>
</tr>
<tr>
<td>status</td>
<td id="state">Loading...</td>
</tr>
</tbody>
</table>
<input type="button" id="button" value="Join tournament" disabled>
<span id="display"></span>
`
}
}

View File

@ -0,0 +1,133 @@
import {client} from "../index.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView
{
constructor(params)
{
super(params, "Tournament");
this.id = params.id;
}
async external_search()
{
let state = document.getElementById("state-select").value;
this.tournaments = await client.tournaments.search(state);
}
add_nb_player_by_game_selector()
{
let nb_players_by_game_list = new Set()
this.tournaments.forEach(tournament => {
nb_players_by_game_list.add(tournament.nb_players_by_game);
});
let select = document.getElementById("nb-players-by-game-select");
let new_children = []
const opt = document.createElement("option");
opt.value = "all";
opt.text = "All";
new_children.push(opt);
nb_players_by_game_list.forEach(nb_players_by_game => {
const opt = document.createElement("option");
opt.value = nb_players_by_game;
opt.text = nb_players_by_game;
new_children.push(opt);
})
select.replaceChildren(...new_children);
}
internal_search()
{
let nb_players_by_game = document.getElementById("nb-players-by-game-select").value;
this.display_tournaments = [];
this.tournaments.forEach(tournament => {
if (nb_players_by_game === "all" || nb_players_by_game == tournament.nb_players_by_game)
this.display_tournaments.push(tournament);
});
}
display_result()
{
const tournaments_list = document.getElementById("tournaments-list");
const new_children = []
this.display_tournaments.forEach(tournament => {
let tr = document.createElement("tr");
// name
let td = document.createElement("td");
td.innerText = tournament.name;
tr.appendChild(td);
// state
td = document.createElement("td");
td.innerText = tournament.state;
tr.appendChild(td);
// nb_players
td = document.createElement("td");
td.innerText = tournament.nb_players;
tr.appendChild(td);
// nb_players_by_game
td = document.createElement("td");
td.innerText = tournament.nb_players_by_game;
tr.appendChild(td);
new_children.push(tr);
});
tournaments_list.replaceChildren(...new_children);
}
async update_query()
{
this.internal_search();
this.display_result();
}
async update_search()
{
await this.external_search();
this.add_nb_player_by_game_selector();
this.update_query();
}
async postInit()
{
await this.update_search()
document.getElementById("state-select").onchange = this.update_search.bind(this);
document.getElementById("nb-players-by-game-select").onchange = this.update_query.bind(this);
}
async getHtml()
{
return `
<select id="state-select">
<option value="waiting">Waiting</option>
<option value="started">Started</option>
<option value="finished">Finished</option>
<option value="all">All</option>
</select>
<select id="nb-players-by-game-select">
</select>
<table>
<thead>
<td>Name</td>
<td>Status</td>
<td>Max numbers of players</td>
<td>Max numbers of players by game</td>
</thead>
<tbody id="tournaments-list">
</tbody>
</table>
`
}
}

9
games/config.py Normal file
View File

@ -0,0 +1,9 @@
PADDLE_SPEED_MAX = 1
PADDLE_SIZE_HEIGHT = 10
PADDLE_SIZE_WIDTH = 100
PADDLE_RAIL = PADDLE_SIZE_WIDTH * 5
BALL_SPEED_INC = 1
BALL_SPEED_START = 1
BALL_SIZE = 4

42
games/consumers.py Normal file
View File

@ -0,0 +1,42 @@
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import User
from games.models import GameModel
import json
from .models import game_room_manager
class GameWebSocket(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_name = "games"
self.group_name = "games"
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.game_id = int(self.scope['url_route']['kwargs']['game_id'])
self.room = game_room_manager.get(self.game_id)
if (self.room is None):
self.member.send("Tournament not found")
self.disconnect(1017)
self.room.append(self.member)
def receive(self, text_data: str = None, bytes_data: bytes = None):
self.member.receive(text_data, bytes_data)
def disconnect(self, close_code):
member = self.room.get_member_by_socket(self)
if (member is not None):
self.room.remove(self.member, close_code)

View File

@ -1,14 +1,27 @@
from django.db import models from django.db import models
from .objects.GameRoomManager import GameRoomManager
from channels.generic.websocket import AsyncWebsocketConsumer
# Create your models here. # Create your models here.
class GameModel(models.Model): class GameModel(models.Model):
finished = models.BooleanField(default = False)
started = models.BooleanField(default = False)
winner_id = models.IntegerField(default = -1)
def create(self, users_id: [int]): def create(self, players_id: [int]):
self.save() self.save()
for user_id in users_id: for player_id in players_id:
GameMembersModel(game_id=self.pk, member_id=user_id) GameMembersModel(game_id = self.pk, player_id = player_id).save()
return self.pk return self.pk
def get_players_id(self):
return [game_member.member_id for game_member in GameMembersModel.objects.filter(game_id = self.pk)]
class GameMembersModel(models.Model): class GameMembersModel(models.Model):
game_id = models.IntegerField() game_id = models.IntegerField()
member_id = models.IntegerField() player_id = models.IntegerField()
game_room_manager: GameRoomManager = GameRoomManager()

View File

@ -0,0 +1,17 @@
from channels.generic.websocket import AsyncWebsocketConsumer
from transcendence.abstract.AbstractRoomMember import AbstractRoomMember
class GameMember(AbstractRoomMember):
def __init__(self, user_id: int, socket: AsyncWebsocketConsumer):
super().__init__(user_id, socket)
self.is_a_player = False
def receive(self, data: dict):
if (not self.is_a_player):
self.send("You are not a player.")
return
def send_ball(self, ball):
pass

View File

@ -0,0 +1,9 @@
from transcendence.abstract.AbstractRoom import AbstractRoom
from .GameRoomManager import GameRoomManager
class GameRoom(AbstractRoom):
def __init__(self, game_room_manager: GameRoomManager, game_id: int):
super().__init__(game_room_manager)
self.game_id = game_id

View File

@ -0,0 +1,18 @@
from transcendence.abstract.AbstractRoomManager import AbstractRoomManager
from ..models import GameModel
class GameRoomManager(AbstractRoomManager):
def get(self, game_id: int):
for room in self._room_list:
if (room.game_id == game_id):
return room
if (GameModel.objects.filter(pk = tournament_id).exists()):
room = TournamentRoom(self, game_id)
self.append(room)
return room
return None

6
games/routing.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/tournaments/(?P<tournament_id>\d+)$', consumers.TournamentWebConsumer.as_asgi())
]

25
games/serializers.py Normal file
View File

@ -0,0 +1,25 @@
from rest_framework import serializers
from .models import GameModel, GameMembersModel
class GameSerializer(serializers.ModelSerializer):
players_id = serializers.SerializerMethodField()
winner_id = serializers.ReadOnlyField()
state = serializers.SerializerMethodField()
started = serializers.ReadOnlyField()
finished = serializers.ReadOnlyField()
class Meta:
model = GameModel
fields = ["id", "winner_id", "state", "started", "finished", "players_id"]
def get_state(self, instance: GameModel):
if (instance.finished):
return "finished"
if (instance.started):
return "started"
return "waiting"
def get_players_id(self, instance: GameModel):
players_id = [player_game.player_id for player_game in GameMembersModel.objects.filter(game_id = instance.pk)]
return players_id

11
games/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, re_path
from django.conf import settings
from django.conf.urls.static import static
from .viewset import GameViewSet
from .views import GameConfigView
urlpatterns = [
path("<int:pk>", GameViewSet.as_view({"get": "retrieve"}), name="game_page"),
path("", GameConfigView.as_view(), name = "game_config")
]

View File

@ -1,3 +1,23 @@
from django.shortcuts import render from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
# Create your views here. from django.http import HttpRequest
from . import config
class GameConfigView(APIView):
permission_classes = (permissions.AllowAny,)
def get(self, request):
config_data = {
"BALL_SIZE": config.BALL_SIZE,
"PADDLE_SPEED_MAX": config.PADDLE_SPEED_MAX,
"PADDLE_SIZE_HEIGHT": config.PADDLE_SIZE_HEIGHT,
"PADDLE_SIZE_WIDTH": config.PADDLE_SIZE_WIDTH,
"PADDLE_RAIL": config.PADDLE_RAIL,
"BALL_SPEED_INC": config.BALL_SPEED_INC,
"BALL_SPEED_START": config.BALL_SPEED_START
}
return Response(config_data, status = status.HTTP_200_OK)

27
games/viewset.py Normal file
View File

@ -0,0 +1,27 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication
from django.http import HttpRequest
from django.db.models import QuerySet
from .models import GameModel
from .serializers import GameSerializer
# Create your views here.
class GameViewSet(viewsets.ModelViewSet):
queryset = GameModel.objects
serializer_class = GameSerializer
permission_classes = (permissions.AllowAny,)
authentication_classes = (SessionAuthentication,)
def retrieve(self, request: HttpRequest, pk):
if (not self.queryset.filter(pk = pk).exists()):
return Response({"detail": "Game not found."}, status=status.HTTP_404_NOT_FOUND)
game = self.queryset.get(pk=pk)
return Response(self.serializer_class(game).data, status=status.HTTP_200_OK)

View File

@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'transcendence.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@ -6,8 +6,7 @@ from games.models import GameModel
import json import json
queue_id: [int] = [] from .models import Waiter, WaitingRoom, WaitingRoomManager, normal
queue_ws: [WebsocketConsumer] = []
class MatchMaking(WebsocketConsumer): class MatchMaking(WebsocketConsumer):
@ -24,25 +23,19 @@ class MatchMaking(WebsocketConsumer):
self.channel_layer.group_add(self.group_name, self.channel_name) self.channel_layer.group_add(self.group_name, self.channel_name)
self.accept() self.mode = int(self.scope['url_route']['kwargs']['mode'])
self.group_name = self.mode
global queue_id, queue_ws waiting_room: WaitingRoom = normal.get(self.mode)
queue_id.append(user.pk) waiting_room.append(Waiter(user.pk, self))
queue_ws.append(self) waiting_room.broadcast(f"{len(waiting_room)} / {waiting_room.mode}")
if (len(waiting_room) == waiting_room.mode):
game_id: int = GameModel().create(waiting_room.get_users_id())
waiting_room.broadcast("game_found", {"game_id": game_id})
waiting_room.clear()
if len(set(queue_id)) == 2:
game_id: int = GameModel().create(set(queue_id))
event = {"game_id": game_id}
for ws in queue_ws:
ws.send(text_data=json.dumps({'game_id': game_id}))
queue_id.clear()
queue_ws.clear()
def disconnect(self, close_code): def disconnect(self, close_code):
user: User = self.scope["user"] waiting_room: WaitingRoom = normal.get(self.mode)
global queue_id, queue_ws waiter: Waiter = waiting_room.get_member_by_socket(self)
if (user.pk in queue_id): if (waiter is not None):
queue_ws.pop(queue_id.index(user.pk)) waiting_room.remove(waiter, 1016)
queue_id.remove(user.pk)
self.channel_layer.group_discard(self.group_name, self.channel_name)

View File

@ -1,3 +1,39 @@
from django.db import models from django.db import models
from channels.generic.websocket import WebsocketConsumer
import json
from transcendence.abstract.AbstractRoom import AbstractRoom
from transcendence.abstract.AbstractRoomManager import AbstractRoomManager
from transcendence.abstract.AbstractRoomMember import AbstractRoomMember
# Create your models here. # Create your models here.
class Waiter(AbstractRoomMember):
pass
class WaitingRoom(AbstractRoom):
def __init__(self, room_manager,mode):
super().__init__(room_manager)
self.mode = mode
def append(self, waiter: Waiter):
tmp: Waiter = self.get_member_by_user_id(waiter.user_id)
if (tmp is not None):
tmp.send("Connection close: Another connection open with the same user id.")
self.remove(tmp)
waiter.accept()
self._member_list.append(waiter)
class WaitingRoomManager(AbstractRoomManager):
def get(self, mode: int):
for waiting_room in self._room_list:
waiting_room: WaitingRoom
if (waiting_room.mode == mode):
return waiting_room
tmp: WaitingRoom = WaitingRoom(self, mode)
super().append(tmp)
return tmp
normal: WaitingRoomManager = WaitingRoomManager()

View File

@ -2,5 +2,5 @@ from django.urls import re_path
from . import consumers from . import consumers
websocket_urlpatterns = [ websocket_urlpatterns = [
re_path(r'ws/matchmaking/', consumers.MatchMaking.as_asgi()) re_path(r'ws/matchmaking/(?P<mode>\d+)$', consumers.MatchMaking.as_asgi())
] ]

View File

@ -7,8 +7,8 @@ class ProfileTest(TestCase):
def setUp(self): def setUp(self):
self.user: User = User.objects.create(username='bozo', password='password') self.user: User = User.objects.create(username='bozo', password='password')
self.user.save() self.user.save()
self.expected_response = {"name": "bozo", self.expected_response = {'avatar_url': '/static/avatars/default.avif', 'user_id': 1, 'username': 'bozo'}
"title": ""}
self.url = "/api/profiles/" self.url = "/api/profiles/"
def test_profile_create_on_user_created(self): def test_profile_create_on_user_created(self):

View File

@ -12,7 +12,7 @@ class BlockView(APIView):
def get(self, request, pk): def get(self, request, pk):
block = BlockModel.objects.filter(pk=pk) block = BlockModel.objects.filter(pk=pk)
if (block): if (block.exists()):
return Response(serializers.serialize("json", block), status=status.HTTP_200_OK) return Response(serializers.serialize("json", block), status=status.HTTP_200_OK)
else: else:
return Response("Not Found", status=status.HTTP_404_NOT_FOUND) return Response("Not Found", status=status.HTTP_404_NOT_FOUND)

View File

@ -31,9 +31,6 @@ class ProfileViewSet(viewsets.ModelViewSet):
profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:] profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:]
return Response(serializer.data) return Response(serializer.data)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class MyProfileViewSet(viewsets.ModelViewSet): class MyProfileViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)

3
tournament/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
tournament/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TournamentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tournament'

43
tournament/consumers.py Normal file
View File

@ -0,0 +1,43 @@
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from games.models import GameModel
import json
from .models import tournament_manager, TournamentMember, 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)
if (self.room is None):
self.member.send("Tournament not found")
self.disconnect(1017)
self.room.append(self.member)
def receive(self, text_data: str = None, bytes_data: bytes = None):
self.member.receive(text_data, bytes_data)
def disconnect(self, close_code):
member = self.room.get_member_by_socket(self)
if (member is not None):
self.room.remove(self.member, close_code)

154
tournament/models.py Normal file
View File

@ -0,0 +1,154 @@
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
# 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()
started = models.BooleanField(default = False)
finished = models.BooleanField(default = False)
def create_game(self, level, players_id):
game_id = GameModel().create(players_id = players_id)
TournamentGamesModel(game_id = game_id, tournament_id = self.pk, tournament_level = level).save()
return game_id
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 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):
return [model.participant_id for model in TournamentParticipantsModel.objects.filter(tournament_id=self.pk)]
def is_a_participant(self, participant_id: int):
return TournamentParticipantsModel.objects.filter(participant_id = participant_id, tournament_id = self.pk).exists()
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()
class TournamentGamesModel(models.Model):
tournament_id = models.IntegerField()
tournament_level = models.IntegerField()
game_id = models.IntegerField()
class TournamentMember(AbstractRoomMember):
def __init__(self, user_id: int, socket: WebsocketConsumer, room):
super().__init__(user_id, socket)
self.participate = False
self.room = room
def receive(self, text_data: str = None, byte_dates: bytes = None):
if (text_data is None):
return
data: dict = json.loads(text_data)
if (data.get("participate") is not None):
self.room.update_participants(self)
def send_error_message(self, message: str):
self.send("error", {"error_message": message})
def go_to(self, url: str):
self.send("go_to", {"url": url})
def send_participating(self):
self.send("is_participant", {"is_participant": self.participate})
class TournamentRoom(AbstractRoom):
def __init__(self, room_manager, tournament_id: int):
super().__init__(room_manager)
self.tournament_id = tournament_id
self.tournament = 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()
member.send("nb_participants", {"nb_participants": self.get_nb_participants()})
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
tournament_manager: TournamentRoomManager = TournamentRoomManager()

6
tournament/routing.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/games/(?P<game_id>\d+)$', consumers.TournamentWebConsumer.as_asgi())
]

38
tournament/serializers.py Normal file
View File

@ -0,0 +1,38 @@
from rest_framework import serializers
from .models import TournamentModel
from games.serializers import GameSerializer
class TournamentSerializer(serializers.ModelSerializer):
levels = serializers.SerializerMethodField(read_only=True, required=False)
level = 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"]
def get_levels(self, instance: TournamentModel):
levels: [[int]] = []
for i in range(instance.level):
games_id: [int] = instance.get_games_id_by_level(i)
if (games_id == []):
break
levels.append(games_id)
return levels
def validate_nb_players(self, value: int):
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.")
nb_players: str = self.initial_data.get("nb_players")
if (nb_players is not None and nb_players.isnumeric()):
nb_players: int = int(nb_players)
if (value > nb_players):
raise serializers.ValidationError("The numbers of players by game must be smaller than the numbers of players.")
return value

44
tournament/test.py Normal file
View File

@ -0,0 +1,44 @@
from django.test import TestCase
# Create your tests here.
from django.test.client import Client
from django.http import HttpResponse
from django.contrib.auth.models import User
import json
import uuid
class CreateTest(TestCase):
def setUp(self):
self.client = Client()
self.url = "/api/tournaments/"
self.username = str(uuid.uuid4())
self.password = str(uuid.uuid4())
self.nb_players_by_game = 2
self.nb_players = 8
user: User = User.objects.create_user(username=self.username, password=self.password)
self.client.login(username=self.username, password=self.password)
def test_normal(self):
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": self.nb_players_by_game, "nb_players": self.nb_players})
response_data: dict = json.loads(response.content)
self.assertDictContainsSubset({"name": ""}, response_data)
def test_too_small_nb_players_by_game(self):
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": 1, "nb_players": self.nb_players})
response_data = json.loads(response.content)
self.assertDictEqual(response_data, {'nb_players_by_game': ['The numbers of players by game must be greather than 2.']})
def test_too_small_nb_players(self):
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": self.nb_players_by_game, "nb_players": 1})
response_data = json.loads(response.content)
self.assertDictEqual(response_data, {'nb_players': ['The numbers of players must be greather than 2.'], 'nb_players_by_game': ['The numbers of players by game must be smaller than the numbers of players.']})
def test_nb_players_smaller_nb_players_by_game(self):
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": 5, "nb_players": 3})
response_data = json.loads(response.content)
self.assertDictEqual(response_data, {'nb_players_by_game': ['The numbers of players by game must be smaller than the numbers of players.']})

3
tournament/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
tournament/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, re_path
from django.conf import settings
from django.conf.urls.static import static
from .viewset import TournamentViewSet
urlpatterns = [
path("<int:pk>", TournamentViewSet.as_view({"get": "retrieve"}), name="tournament_page"),
path("", TournamentViewSet.as_view({"post": "create"}), name="tournament_page"),
re_path(r"search/(?P<state>\w*)", TournamentViewSet.as_view({"get": "list", }), name="tournaments"),
]

56
tournament/viewset.py Normal file
View File

@ -0,0 +1,56 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication
from django.http import HttpRequest
from django.contrib.auth import login
from django.db.models import QuerySet
from .models import TournamentModel
from .serializers import TournamentSerializer
# Create your views here.
class TournamentViewSet(viewsets.ModelViewSet):
queryset = TournamentModel.objects.all
serializer_class = TournamentSerializer
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
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)
return Response(self.serializer_class(tournament).data, status=status.HTTP_201_CREATED)
def list(self, request: HttpRequest, state: str = ""):
query: QuerySet
match state:
case "started":
query = TournamentModel.objects.filter(started=True, finished=False)
case "finished":
query = TournamentModel.objects.filter(finished=True)
case "waiting":
query = TournamentModel.objects.filter(started=False, finished=False)
case _:
query = TournamentModel.objects.all()
serializer = TournamentSerializer(query, many=True)
return Response(serializer.data)
def retrieve(self, request: HttpRequest, pk):
if (not TournamentModel.objects.filter(pk=pk).exists()):
return Response({"detail": "Tournament not found."}, status=status.HTTP_404_NOT_FOUND)
tournament = TournamentModel.objects.get(pk=pk)
return Response(self.serializer_class(tournament).data, status=status.HTTP_200_OK)

View File

View File

@ -0,0 +1,51 @@
from channels.generic.websocket import WebsocketConsumer
from .AbstractRoomMember import AbstractRoomMember
class AbstractRoom:
def __init__(self, room_manager):
self._member_list: [AbstractRoomMember] = []
self.room_manager = room_manager
def broadcast(self, detail: str, data: dict = {}):
for member in self._member_list:
member: AbstractRoomMember
member.send(detail, data)
def clear(self):
self._member_list.clear()
def get_member_by_socket(self, socket: WebsocketConsumer):
for member in self._member_list:
member: AbstractRoomMember
if (member.socket is socket):
return member
return None
def get_member_by_user_id(self, user_id: int):
for member in self._member_list:
member: AbstractRoomMember
if (member.user_id == user_id):
return member
return None
def append(self, member: AbstractRoomMember):
self._member_list.append(member)
member.accept()
def remove(self, member: AbstractRoomMember, code: int = 1000):
self._member_list.remove(member)
member.disconnect(code)
def empty(self):
for _ in self._member_list:
return False
return True
def get_users_id(self):
return [member.user_id for member in self._member_list]
def __len__(self):
return len(self._member_list)

View File

@ -0,0 +1,12 @@
from .AbstractRoom import AbstractRoom
class AbstractRoomManager:
def __init__(self):
self._room_list: [AbstractRoom] = []
def remove(self, room: AbstractRoom):
self._room_list.remove(room)
def append(self, room: AbstractRoom):
self._room_list.append(room)

View File

@ -0,0 +1,20 @@
from channels.generic.websocket import WebsocketConsumer
import json
class AbstractRoomMember:
def __init__(self, user_id: int, socket: WebsocketConsumer):
self.user_id: int = user_id
self.socket: WebsocketConsumer = socket
def send(self, detail: str, data: dict = {}):
raw_data: dict = {"detail": detail}
raw_data.update(data)
self.socket.send(text_data=json.dumps(raw_data))
def accept(self):
self.socket.accept()
def disconnect(self, code: int = 1000):
self.socket.disconnect(code)

View File

@ -13,6 +13,8 @@ from channels.auth import AuthMiddlewareStack
import chat.routing import chat.routing
import matchmaking.routing import matchmaking.routing
import tournament.routing
import games.routing
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
@ -23,7 +25,9 @@ application = ProtocolTypeRouter({
'websocket':AuthMiddlewareStack( 'websocket':AuthMiddlewareStack(
URLRouter( URLRouter(
chat.routing.websocket_urlpatterns + chat.routing.websocket_urlpatterns +
matchmaking.routing.websocket_urlpatterns matchmaking.routing.websocket_urlpatterns +
tournament.routing.websocket_urlpatterns +
games.routing.websocket_urlpatterns
) )
) )
}) })

View File

@ -1,5 +1,5 @@
""" """
Django settings for trancendence project. Django settings for transcendence project.
Generated by 'django-admin startproject' using Django 4.2.6. Generated by 'django-admin startproject' using Django 4.2.6.
@ -43,6 +43,7 @@ INSTALLED_APPS = [
'channels', 'channels',
'daphne', 'daphne',
'tournament.apps.TournamentConfig',
'matchmaking.apps.MatchmakingConfig', 'matchmaking.apps.MatchmakingConfig',
'games.apps.GamesConfig', 'games.apps.GamesConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
@ -60,7 +61,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
] ]
ASGI_APPLICATION = 'trancendence.asgi.application' ASGI_APPLICATION = 'transcendence.asgi.application'
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
'default' :{ 'default' :{
@ -80,7 +81,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
ROOT_URLCONF = 'trancendence.urls' ROOT_URLCONF = 'transcendence.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@ -98,7 +99,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'trancendence.wsgi.application' WSGI_APPLICATION = 'transcendence.wsgi.application'
# Database # Database

View File

@ -22,5 +22,7 @@ urlpatterns = [
path('api/profiles/', include('profiles.urls')), path('api/profiles/', include('profiles.urls')),
path('api/accounts/', include('accounts.urls')), path('api/accounts/', include('accounts.urls')),
path('api/chat/', include('chat.urls')), path('api/chat/', include('chat.urls')),
path('api/tournaments/', include('tournament.urls')),
path('api/games/', include('games.urls')),
path('', include('frontend.urls')), path('', include('frontend.urls')),
] ]