113 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
4fd6616786 add block option 2023-12-20 23:48:52 +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
57ed6165ea bug issue; rewrite all messages 2023-12-19 12:42:46 +01:00
acba77e228 Réarrangement du code; correction css; écriture uniquement du nouveau message 2023-12-19 11:27:18 +01:00
0e946b3bb8 fix: gameModel.create is not cringe now 2023-12-18 23:18:39 +01:00
fb0f9be103 merge with Chatte; patch css 2023-12-18 22:37:05 +01:00
41a7a6dfb8 css patch 2023-12-18 22:35:42 +01:00
a10fdefd23 css patch 2023-12-18 22:29:40 +01:00
kdx
b139652fc5 fix css 2023-12-18 22:29:38 +01:00
c6f6e216f2 css patch 2023-12-18 22:27:17 +01:00
kdx
7479088254 fix css 2023-12-18 22:27:02 +01:00
651fbbd67f css patch 2023-12-18 22:18:49 +01:00
778d10d22f fix: client.me 2 2023-12-18 21:41:00 +01:00
1961047703 merge with Chatte 2023-12-18 21:29:35 +01:00
929c1fdeb3 merge with Chatte 2023-12-18 21:27:52 +01:00
980a66fb47 fix: client.me 2023-12-18 21:27:10 +01:00
609b045315 better css, add debug 2023-12-18 21:26:18 +01:00
ceed7c2c4a fix: profile page 2 2023-12-17 20:29:04 +01:00
6a537a9b68 fix: profile work 2023-12-17 20:27:13 +01:00
26a9152756 fix: register view 2023-12-17 20:17:52 +01:00
f32d38287a tentative de merge 2023-12-16 18:02:07 +01:00
51354d9922 tentative de merge 2023-12-16 18:00:38 +01:00
b5d73e59fd core: simplified login and register, fix: me 2023-12-16 17:41:31 +01:00
86e2528d04 merge with Chatte 2023-12-16 17:01:03 +01:00
3be50e747d bug issue, null users_id creation channel 2023-12-16 17:00:38 +01:00
1f41e62a86 core: refonte profile 2023-12-16 16:42:30 +01:00
51059e6bf3 merge with Chatte 2023-12-16 16:15:13 +01:00
92221926a1 Better Css 2023-12-16 16:14:46 +01:00
115ae9357a fix: profile not found GET return error 2023-12-16 11:06:24 +01:00
1b44d5c94f Merge remote-tracking branch 'refs/remotes/origin/server' into server 2023-12-16 10:58:11 +01:00
468a3917a9 fix css file to /login /register 2023-12-16 10:39:07 +01:00
9be65a2bab Update README.md 2023-12-16 10:35:52 +01:00
d03f90367f bug issue 2023-12-15 20:39:31 +01:00
6171528371 merge with Chatte 2023-12-15 20:34:55 +01:00
bcb072f7d9 chat functional 2023-12-15 20:32:43 +01:00
c303042588 add: 404 not found 2023-12-14 13:55:31 +01:00
072c97d1c0 Update README.md 2023-12-13 14:55:17 +01:00
8e375c4805 Update README.md 2023-12-13 13:45:29 +01:00
4c3dd1be4a update: requirement 2023-12-13 13:01:51 +01:00
5d46ff5123 add: offline button 2023-12-13 12:40:47 +01:00
0ce8645770 Merge branch 'jspong' into server 2023-12-13 12:35:06 +01:00
63e1520e6a add: ball respawn timeout 2023-12-12 18:25:16 +01:00
754e5867f2 welp I guess we're done 2023-12-12 15:58:50 +01:00
a15c59fbf7 clean: remove trash file 2023-12-12 15:21:06 +01:00
243c5f266a clean: remove bozo print 2023-12-12 15:20:53 +01:00
926ac0dd54 clean: print 2023-12-12 15:20:26 +01:00
f0aaf0f29e clean: remove print 2023-12-12 14:43:57 +01:00
ca6dba2763 add: ball angle calculations 2023-12-12 12:10:48 +01:00
7e2c29e78b merge with server 2023-12-12 10:37:44 +01:00
0e3b19fcd9 Advance don't merge 2023-12-12 10:05:13 +01:00
bc892bc157 Advance don't merge 2023-12-12 10:04:46 +01:00
9b523f082f merge with server 2023-12-11 16:15:21 +01:00
78379aea1b Bug fix with username in chat 2023-12-11 16:14:27 +01:00
624fb47e04 merge with server 2023-12-11 14:55:17 +01:00
c178556a2e Add functional research bar 2023-12-11 14:54:39 +01:00
cb5affab48 merge with server 2023-12-11 13:10:17 +01:00
08093627c9 Camille à trop les cramptés, en plus il va voir francis éboué qui est passé chez noz avec son daron qui se dore la biscotte en guadeloupe 2023-12-11 13:09:20 +01:00
208dd206ce merge with server 2023-12-11 10:57:51 +01:00
cee188145d no idea 2023-12-11 10:53:34 +01:00
aa35514c44 do u guys wanna see my balls ? 2023-12-10 11:05:00 +01:00
12056554fc game: moving paddle :) 2023-12-05 12:18:34 +01:00
c2317d5404 la putain de sa mere 2023-12-04 16:51:24 +01:00
af9595c447 Don't merge, it's prototypal 2023-11-30 16:36:21 +01:00
98 changed files with 2719 additions and 456 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
db.sqlite3
**/migrations/**
/profiles/static/avatars/*
!/profiles/static/avatars/default
!/profiles/static/avatars/default.env

View File

@ -22,10 +22,18 @@ pip install -r requirements.txt
```
- Setup database
```
python manage.py runserver makemigrations profiles games
python manage.py migrate profiles games
python manage.py makemigrations games
python manage.py makemigrations profiles
python manage.py makemigrations chat
python manage.py makemigrations tournament
python manage.py migrate
```
- Start the developpement server
```
python manage.py runserver 0.0.0.0:8000
```
coc nvim
```
pip install django-stubs
```

View File

@ -15,4 +15,4 @@ class LoggedView(APIView):
def get(self, request: HttpRequest):
if (request.user.is_authenticated):
return Response({'id': request.user.pk}, status=status.HTTP_200_OK)
return Response('false', status=status.HTTP_200_OK)
return Response('false', status=status.HTTP_200_OK)

View File

@ -15,4 +15,4 @@ class RegisterView(APIView):
if user:
login(request, user)
return Response("user created", status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,3 +1,6 @@
from django.contrib import admin
from .models import ChatChannelModel, ChatMemberModel, ChatMessageModel
# Register your models here.
admin.site.register(ChatChannelModel)
admin.site.register(ChatMemberModel)
admin.site.register(ChatMessageModel)

View File

@ -1,10 +1,29 @@
import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from .models import ChatMemberModel, ChatMessageModel
from profiles.models import BlockModel
import time
import json
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_group_name = 'test'
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
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
if (self.channel_layer == None):
return
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
@ -14,23 +33,67 @@ class ChatConsumer(WebsocketConsumer):
self.accept()
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
def receive(self, text_data=None, bytes_data=None):
if text_data == None:
return
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
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
if (self.channel_layer == None):
return
message_time: int = int(time.time() * 1000)
if (len(receivers_id) == 1 and
BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or
BlockModel.objects.filter(blocker=receivers_id[0], blocked=user.pk)
):
return
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type':'chat_message',
'message':message
'author_id':user.pk,
'content':message,
'time':message_time,
}
)
new_message = ChatMessageModel(channel_id = channel_id, author_id = user.pk, content = message, time = message_time).save()
def chat_message(self, event):
message = event['message']
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
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
self.send(text_data=json.dumps({
'type':'chat',
'username':self.scope["user"].username,
'message':message
'author_id':event['author_id'],
'content':event['content'],
'time': event['time'],
}))

View File

@ -1,3 +1,31 @@
from django.db import models
from django.db.models import IntegerField
from django.contrib.auth.models import User
from django.contrib import admin
# Create your models here.
class ChatChannelModel(models.Model):
def create(self, users_id: [int]):
self.save()
for user_id in users_id:
ChatMemberModel(channel_id = self.pk, member_id = user_id).save()
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)
channel_id = IntegerField(primary_key=False)
def __str__(self):
return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id)
class ChatMessageModel(models.Model):
channel_id = IntegerField(primary_key=False)
author_id = IntegerField(primary_key=False)
content = models.CharField(max_length=255)
time = IntegerField(primary_key=False)
def __str__(self):
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
websocket_urlpatterns = [
re_path(r'ws/socket-server/', consumers.ChatConsumer.as_asgi())
re_path(r'ws/chat/(?P<chat_id>\d+)$', consumers.ChatConsumer.as_asgi())
]

30
chat/serializers.py Normal file
View File

@ -0,0 +1,30 @@
from rest_framework import serializers
from profiles.models import ProfileModel
from .models import ChatChannelModel, ChatMessageModel
class ChatChannelSerializer(serializers.ModelSerializer):
members_id = serializers.ListField(child = serializers.IntegerField())
class Meta:
model = ChatChannelModel
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.client import Client
from django.http import HttpResponse, HttpRequest
from django.contrib.auth.models import User
# 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.'})

9
chat/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views
urlpatterns = [
path("", views.ChannelView.as_view(), name="chats_page"),
]

View File

@ -1,3 +1,45 @@
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.authentication import SessionAuthentication
# Create your views here.
from django.http import HttpRequest
from django.contrib.auth import login
from django.db.models import QuerySet
from django.core import serializers
from .models import ChatChannelModel, ChatMemberModel, ChatMessageModel
from .serializers import ChatChannelSerializer, ChatMessageSerializer
class ChannelView(APIView):
queryset = ChatChannelModel.objects
serializer_class = ChatChannelSerializer
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
def post(self, request):
serializer = self.serializer_class(data = request.data)
serializer.is_valid(raise_exception=True)
data: dict = serializer.validated_data
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)
for member_channel in ChatMemberModel.objects.filter(member_id = members_id[0]):
channel_id: int = member_channel.channel_id
if not ChatChannelModel.objects.filter(pk = channel_id).exists():
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)
new_channel_id = ChatChannelModel().create(members_id)
return Response({'channel_id': new_channel_id}, status=status.HTTP_201_CREATED)

View File

@ -9,4 +9,4 @@
margin-right: auto;
margin-top: 90px;
border: 15px black solid;
}
}

View File

@ -9,4 +9,4 @@
margin-right: auto;
margin-top: 90px;
border: 15px black solid;
}
}

View File

@ -1,7 +1,8 @@
body {
margin: 10;
margin: 0.5em;
font-family: 'Quicksand', sans-serif;
font-size: 18px;
font-size: 30px;
}
a {

View File

@ -1,19 +1,18 @@
#app .account
#app #main .account
{
background-color: red;
}
#app .account, #app .profile
#app #main
{
width: 60%;
display: flex;
margin-left: auto;
margin-right: auto;
flex-direction: column;
flex-wrap: wrap;
}
#app .profile
#app #main .profile
{
background-color: green;
}
}

View File

@ -0,0 +1,20 @@
#app #avatar
{
height: 100px;
width: 100px;
}
#app #username
{
font-size: 0.8em;
}
#app #block {
cursor: pointer;
font-size: 0.7em;
text-decoration: underline;
}
#app {
margin-top: 20px;
}

View File

@ -1,11 +0,0 @@
#app #avatar
{
height: 100px;
width: 100px;
}
#app #username
{
height: 100px;
width: 100px;
}

View File

@ -1,11 +0,0 @@
#app .item img
{
height: 100px;
width: 100px;
}
#app .item a
{
height: 100px;
width: 100px;
}

View File

@ -0,0 +1,104 @@
#app img
{
max-height: 3em;
max-width: 3em;
}
#app ul
{
font-size: 0.75em;
margin: 0.25em 0 0 0;
padding: 0 0 0 0;
list-style-type: none;
max-height: 80vh;
overflow: auto;
}
#app li
{
margin: 0.25em 0.25em 0 0;
}
#app #chats {
overflow: hidden;
display: flex;
text-align: left;
}
#app #users {
margin: 0em 1.0em 0em 0.05em;
}
#app #chat {
position: relative;
max-height: 100vh;
width: 100vh;
/*border: 2px solid green;*/
overflow: hidden;
}
#app #members {
font-size: 1em;
}
#app #add_chat_off {
text-decoration: underline;
cursor: pointer;
}
#app #add_chat_on {
cursor: pointer;
}
#app #messages {
max-height: 60vh;
overflow: scroll;
overflow-y: scroll;
overflow-x: hidden;
font-size: 0.75em;
}
#app #input_user{
color: green;
width: 8.5em;
height: 1.1em;
font-size: 0.65em;
border: none;
outline: none;
border-bottom: 0.15em solid green;
}
#app #input_chat{
position: sticky;
bottom: 0;
/*width: calc(100% - 8px);*/
width: 100%;
border: none;
outline: none;
border-bottom: 0.2em solid green;
caret-color: green;
color: green;
font-size: 16px;
}
#app #you {
text-align: left;
position: relative;
max-width: 48%;
left: 0.5em;
margin: 0.5em 0 0 0;
color: green;
word-wrap: break-word;
}
#app #other {
text-align: right;
position: relative;
max-width: 48%;
margin: 0.5em 0 0 auto;
right: 0.5em;
color: red;
/* permet le retour à la ligne à la place de dépasser*/
word-wrap: break-word;
}

View File

@ -0,0 +1,19 @@
import { Profile } from "./profile.js";
class MyProfile extends Profile
{
async change_avatar(form_data)
{
let response = await this.client._patch_file(`/api/profiles/me`, form_data);
let response_data = await response.json()
return response_data;
}
async init()
{
super.init("me");
}
}
export {MyProfile}

View File

@ -1,7 +1,15 @@
import { Client } from "./client.js";
class Account
{
/**
* @param {Client} client
*/
constructor (client)
{
/**
* @type {Client} client
*/
this.client = client;
}
@ -25,10 +33,9 @@ class Account
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{
this.client._logged = false;
this.client._update_logged(false);
return null;
}
console.log(response_data)
if (response_data == "user deleted")
this.client._logged = false;
return response_data;
@ -55,7 +62,7 @@ class Account
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{
this.client._logged = false;
this.client._;
return null;
}
return response_data;

View File

@ -0,0 +1,80 @@
import {Message} from "./message.js"
class Channel {
constructor(client, channel_id, members_id, messages, reload) {
this.client = client;
this.channel_id = channel_id;
this.members_id = members_id;
this.messages = [];
if (messages != undefined)
this.updateMessages(messages);
this.connect(reload);
}
// reload = function to use when we receive a message
async connect(reload) {
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/chat/${this.channel_id}`;
this.chatSocket = new WebSocket(url);
this.chatSocket.onmessage = (event) =>{
let data = JSON.parse(event.data)
this.messages.push(new Message(
this.channel_id,
data.author_id,
data.content,
data.time,
));
reload();
};
}
async disconnect() {
this.chatSocket.close();
}
updateMessages(messages)
{
console.log(messages);
messages = JSON.parse(messages);
let new_messages = [];
messages.forEach((message) => {
message = message["fields"];
new_messages.push(new Message(
message["channel_id"],
message["author_id"],
message["content"],
message["time"],
));
});
//console.log(new_messages);
this.messages = new_messages;
return new_messages;
}
async sendMessageChannel(message, receivers_id) {
if (this.chatSocket == undefined)
return;
this.chatSocket.send(JSON.stringify({
'message':message,
'receivers_id':receivers_id,
}));
}
async deleteChannel() {
let response = await this.client._delete("/api/chat/" + this.channel_id, {
});
let data = await response.json();
return data;
}
}
export {Channel}

View File

@ -0,0 +1,47 @@
import {Channel} from "./channel.js"
import {Message} from "./message.js"
class Channels {
constructor(client) {
this.client = client;
}
async createChannel(members_id, reload) {
let null_id = false;
members_id.forEach(member_id => {
if (member_id == null)
null_id = true;
});
if (null_id)
return console.log(members_id, "createChannel error, null id;");
let response = await this.client._post("/api/chat/", {
members_id:members_id
});
let data = await response.json();
let exit_code = await response.status;
if (exit_code >= 300)
return undefined;
let messages = undefined;
if (exit_code == 200)
messages = data.messages;
return new Channel(this.client, data.channel_id, members_id, messages, reload);
}
async deleteChannel(members_id) {
let response = await this.client._delete("/api/chat/", {
members_id:members_id
});
let data = await response.json();
console.log(response.status)
return data;
}
}
export {Channels}

View File

@ -0,0 +1,10 @@
class Message {
constructor(channel_id, author_id, content, time) {
this.channel_id = channel_id;
this.author_id = author_id;
this.content = content;
this.time = time;
}
}
export {Message}

View File

@ -1,7 +1,10 @@
import { Account } from "./account.js";
import { MatchMaking } from "./matchmaking.js";
import { Profile } from "./profile.js";
import { Profiles } from "./profiles.js";
import { Channels } from './chat/channels.js';
import { MyProfile } from "./MyProfile.js";
import { navigateTo } from "../index.js"
import { Tourmanents } from "./tournament/tournaments.js";
function getCookie(name)
{
@ -21,7 +24,11 @@ class Client
this.account = new Account(this);
this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this);
this.tournaments = new Tourmanents(this);
this._logged = undefined;
this.channels = new Channels(this);
this.channel = undefined;
}
async isAuthentificate()
@ -31,10 +38,11 @@ class Client
return this.logged;
}
async _get(uri)
async _get(uri, data)
{
let response = await fetch(this._url + uri, {
method: "GET",
body: JSON.stringify(data),
});
return response;
}
@ -90,15 +98,27 @@ class Client
return response;
}
async _update_logged(state)
{
if (!this.logged && state)
{
this.me = new MyProfile(this);
await this.me.init();
}
if (this.logged && !state)
{
navigateTo("/login");
}
this.logged = state;
}
async login(username, password)
{
let response = await this._post("/api/accounts/login", {username: username, password: password})
let data = await response.json();
if (data.id != undefined)
{
this.me = new Profile(this)
await this.me.init(data.id)
this.logged = true;
await this._update_logged(true);
return null;
}
return data;
@ -107,7 +127,7 @@ class Client
async logout()
{
await this._get("/api/accounts/logout");
this.logged = false;
await this._update_logged(false);
}
async _test_logged()
@ -116,12 +136,9 @@ class Client
let data = await response.json();
if (data.id !== undefined)
{
this.me = new Profile(this)
await this.me.init(data.id)
}
await this._update_logged(true);
return data.id !== undefined;
}
}
export {Client}
export {Client}

View File

@ -1,42 +1,52 @@
import { client, navigateTo } from "../index.js"
import { Client } from "./client.js";
class MatchMaking
{
/**
* @param {client} client
* @param {Client} client
*/
constructor(client)
{
/**
* @type {client}
* @type {Client}
*/
this.client = client
this.searching = false;
}
async start(func)
async start(receive_func, disconnect_func, mode)
{
if (!await this.client.isAuthentificate())
return null;
console.log(func)
this.callback = func
console.log(this.callback)
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`;
let url = `wss://${window.location.host}/ws/matchmaking/`;
this._socket = new WebSocket(url);
this._chatSocket = new WebSocket(url);
this._chatSocket.onmessage = function (event) {
this.searching = true;
this.receive_func = receive_func;
this.disconnect_func = disconnect_func;
this._socket.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log(func, 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()
{
this._chatSocket.close()
this.searching = false;
this._socket.close()
}
}
export {MatchMaking}
export {MatchMaking}

View File

@ -1,35 +1,49 @@
import { Client } from "./client.js";
class Profile
{
/**
* @param {Client} client
*/
constructor (client, username = undefined, avatar_url = undefined, user_id = undefined)
{
/**
* @type {Client} client
*/
this.client = client;
this.username = username;
this.avatar_url = avatar_url
this.user_id = user_id
this.avatar_url = avatar_url;
this.user_id = user_id;
this.isBlocked = false;
}
async init(user_id)
{
let response = await this.client._get(`/api/profiles/${user_id}`);
if (response.status === 404)
return 1;
let response_data = await response.json();
this.user_id = user_id;
this.user_id = response_data.user_id;
this.username = response_data.username;
this.avatar_url = response_data.avatar_url;
}
async change_avatar(form_data)
{
let response = await this.client._patch_file(`/api/profiles/${this.user_id}`, form_data);
let response_data = await response.json()
let block_response = await this.client._get("/api/profiles/block");
return response_data;
}
if (block_response.status == 404)
return
async setData (data)
{
let block_data = await block_response.json();
let block_list = JSON.parse(block_data);
block_list.forEach(block => {
let blocker = block.fields.blocker;
let blocked = block.fields.blocked;
if (blocker == this.client.me.user_id && blocked == user_id)
return this.isBlocked = true;
});
}
}
export {Profile}
export {Profile}

View File

@ -2,8 +2,14 @@ import { Profile } from "./profile.js";
class Profiles
{
/**
* @param {Client} client
*/
constructor (client)
{
/**
* @type {Client} client
*/
this.client = client
}
@ -22,9 +28,38 @@ class Profiles
async getProfile(user_id)
{
let profile = new Profile(this.client);
await profile.init(user_id);
if (await profile.init(user_id))
return null;
return profile;
}
async block(user_id) {
// blocker & blocked
let response = await this.client._post("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id],
});
let data = await response.json();
console.log(response.status);
console.log(data);
return data;
}
async deblock(user_id) {
// blocker & blocked
let response = await this.client._delete("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id],
});
let data = await response.json();
console.log(response.status);
console.log(data);
return data;
}
}
export {Profiles}
export {Profiles}

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

@ -1,17 +1,22 @@
import { Client } from "./api/client.js";
import LoginView from "./views/accounts/LoginView.js";
import Dashboard from "./views/Dashboard.js";
import Settings from "./views/Settings.js";
import Chat from "./views/Chat.js";
import Search from "./views/Search.js";
import HomeView from "./views/HomeView.js";
import RegisterView from "./views/accounts/RegisterView.js";
import LogoutView from "./views/accounts/LogoutView.js";
import GameView from "./views/Game.js"
import { Client } from "./api/client.js";
import AbstractRedirectView from "./views/AbstractRedirectView.js";
import PageNotFoundView from './views/PageNotFoundView.js'
import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
import MeView from "./views/MeView.js";
import ProfilePageView from "./views/profiles/ProfilePageView.js";
import ProfilesView from "./views/profiles/ProfilesView.js";
import ProfilePageView from "./views/ProfilePageView.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)
@ -33,19 +38,34 @@ const navigateTo = async (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 routes = [
{ path: "/", view: Dashboard },
{ path: "/profiles", view: ProfilesView},
{ path: "/profiles/:id", view: ProfilePageView },
{ path: "/settings", view: Settings },
{ path: "/tournaments/create", view: TournamentCreateView },
{ path: "/tournaments/:id", view: TournamentPageView },
{ path: "/tournaments/", view: TournamentsView },
{ path: "/login", view: LoginView },
{ path: "/logout", view: LogoutView },
{ path: "/register", view: RegisterView },
{ path: "/chat", view: Chat },
{ path: "/search", view: Search },
{ path: "/home", view: HomeView },
{ path: "/me", view: MeView },
{ path: "/matchmaking", view: MatchMakingView },
{ path: "/game/offline", view: GameView },
];
// Test each route for potential match
@ -60,7 +80,10 @@ const router = async (uri) => {
if (!match) {
match = {
route: routes[0],
route: {
path: uri,
view: PageNotFoundView
},
result: [uri]
};
}
@ -75,14 +98,8 @@ const router = async (uri) => {
lastView = view;
let content = await view.getHtml();
if (content == null)
return 1;
view.setTitle();
document.querySelector("#app").innerHTML = content
await view.postInit();
await client.isAuthentificate();
renderView(view);
return 0;
};
@ -98,4 +115,4 @@ document.addEventListener("DOMContentLoaded", () => {
router(location.pathname);
});
export { client, navigateTo }
export { client, navigateTo }

View File

@ -0,0 +1,18 @@
function clear(property_name, elements_id)
{
elements_id.forEach(element_id => {
let element = document.getElementById(element_id)
element[property_name] = ""
});
}
function fill_errors(errors, property_name)
{
Object.keys(errors).forEach(error_field =>
{
let element = document.getElementById(error_field);
element[property_name] = errors[error_field];
});
}
export {fill_errors, clear}

View File

@ -1,56 +0,0 @@
import AbstractAuthentifiedView from "./AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView {
constructor(params) {
super(params, "Chat");
let url = `wss://${window.location.host}/ws/socket-server/`
this.chatSocket = new WebSocket(url)
this.chatSocket.onmessage = function(e){
let data = JSON.parse(e.data)
console.log('Data:', data)
if (data.type === 'chat') {
let messages = document.getElementById('messages')
let username = data.username === null || data.username.length <= 0 ? "NoName" : data.username;
messages.insertAdjacentHTML('beforeend', `
<div><p>${username}: ${data.message}</p></div>
`)
}
}
}
async postInit() {
let form = document.getElementById('form')
form.addEventListener('submit', (e)=> {
e.preventDefault()
let message = e.target.message.value
this.chatSocket.send(JSON.stringify({
'message':message
}))
form.reset()
})
}
async leavePage() {
this.chatSocket.close();
}
async getHtml() {
return `
<h1>Chat</h1>
<form id="form">
<input type="text" name="message" />
</form>
<div id="messages">
</div>
`;
}
}

View File

@ -1,4 +1,4 @@
import AbstractView from "./AbstractView.js";
import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView {
constructor(params) {
@ -16,4 +16,4 @@ export default class extends AbstractView {
</p>
`;
}
}
}

View File

@ -0,0 +1,250 @@
import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, 'Game');
this.game = null;
}
async getHtml() {
return `
<h1>Game</h1>
<button id='startGameButton'>Start Game</button>
<button id='stopGameButton'>Stop Game</button>
`;
}
async postInit() {
document.getElementById('startGameButton').onclick = this.startGame.bind(this);
document.getElementById('stopGameButton').onclick = this.stopGame.bind(this);
}
startGame() {
if (this.game == null) {
document.getElementById('startGameButton').innerHTML = 'Reset Game';
this.game = new Game;
}
else {
document.getElementById('app').removeChild(this.game.canvas);
this.game.cleanup();
this.game = new Game;
}
}
stopGame() {
if (!this.game)
return;
document.getElementById('app').removeChild(this.game.canvas);
document.getElementById('app').removeChild(this.game.scoresDisplay);
this.game.cleanup();
this.game = null;
document.getElementById('startGameButton').innerHTML = 'Start Game';
}
}
class Game {
constructor() {
//Global variables
this.def = {
CANVASHEIGHT: 270,
CANVASWIDTH: 480,
PADDLEHEIGHT: 70,
PADDLEWIDTH: 10,
PADDLEMARGIN: 5,
PADDLESPEED: 3,
BALLRADIUS: 5,
BALLSPEED: 2,
BALLSPEEDINCR: 0.15,
MAXBOUNCEANGLE: 10 * (Math.PI / 12),
MAXSCORE: 6
};
this.canvas = document.createElement('canvas');
this.canvas.id = 'gameCanvas';
this.canvas.width = this.def.CANVASWIDTH;
this.canvas.height = this.def.CANVASHEIGHT;
this.canvas.style.border = '1px solid #d3d3d3';
this.canvas.style.backgroundColor = '#f1f1f1';
this.context = this.canvas.getContext('2d');
document.getElementById('app').appendChild(this.canvas);
this.scoresDisplay = document.createElement('p');
this.scoresDisplay.innerHTML = 'Scores: 0 - 0';
document.getElementById('app').appendChild(this.scoresDisplay);
this.players = [
{
paddle: new Paddle(this.context,
this.def.PADDLEMARGIN,
this.def),
score: 0
},
{
paddle: new Paddle(this.context,
this.def.CANVASWIDTH - this.def.PADDLEMARGIN - this.def.PADDLEWIDTH,
this.def),
score: 0
}
];
this.ballStartSide = 0;
this.ballRespawned = false;
this.ball = new Ball(this.context, this.def, this.ballStartSide);
this.interval = setInterval(this.updateGame.bind(this), 10);
this.keys = [];
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
document.addEventListener('keydown', this.keyDownHandler);
document.addEventListener('keyup', this.keyUpHandler);
}
cleanup() {
clearInterval(this.interval);
document.removeEventListener('keydown', this.keyDownHandler);
document.removeEventListener('keyup', this.keyUpHandler);
this.canvas.style.display = 'none';
}
clear() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
updateGame() {
//Paddle movement
if (this.keys.includes('s') &&
this.players[0].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN)
this.players[0].paddle.y += this.def.PADDLESPEED;
if (this.keys.includes('w') &&
this.players[0].paddle.y > 0 + this.def.PADDLEMARGIN)
this.players[0].paddle.y -= this.def.PADDLESPEED;
if (this.keys.includes('ArrowDown') &&
this.players[1].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN)
this.players[1].paddle.y += this.def.PADDLESPEED;
if (this.keys.includes('ArrowUp') &&
this.players[1].paddle.y > 0 + this.def.PADDLEMARGIN)
this.players[1].paddle.y -= this.def.PADDLESPEED;
//GOOAAAAL
if (this.ball.x <= 0)
this.updateScore(this.players[0].score, ++this.players[1].score);
else if (this.ball.x >= this.def.CANVASWIDTH)
this.updateScore(++this.players[0].score, this.players[1].score);
//Ball collisions
if (this.detectCollision(this.players[0].paddle, this.ball))
this.calculateBallVelocity(this.players[0].paddle.getCenter().y, this.ball);
else if (this.detectCollision(this.players[1].paddle, this.ball))
this.calculateBallVelocity(this.players[1].paddle.getCenter().y, this.ball, -1);
if (this.ball.y - this.ball.radius <= 0)
this.ball.vy *= -1;
else if (this.ball.y + this.ball.radius >= this.canvas.height)
this.ball.vy *= -1;
if (!this.ballRespawned) {
this.ball.x += this.ball.vx;
this.ball.y += this.ball.vy;
}
this.clear();
this.players[0].paddle.update();
this.players[1].paddle.update();
this.ball.update();
}
updateScore(p1Score, p2Score) {
if (p1Score > this.def.MAXSCORE) {
this.scoresDisplay.innerHTML = 'Player 1 wins!! GGS';
this.cleanup();
}
else if (p2Score > this.def.MAXSCORE) {
this.scoresDisplay.innerHTML = 'Player 2 wins!! GGS';
this.cleanup();
} else {
this.scoresDisplay.innerHTML = `Scores: ${p1Score} - ${p2Score}`;
this.ballStartSide = 1 - this.ballStartSide;
this.ball = new Ball(this.context, this.def, this.ballStartSide);
this.ballRespawned = true;
new Promise(r => setTimeout(r, 300))
.then(_ => this.ballRespawned = false);
}
}
detectCollision(paddle, ball) {
let paddleCenter = paddle.getCenter();
let dx = Math.abs(ball.x - paddleCenter.x);
let dy = Math.abs(ball.y - paddleCenter.y);
if (dx <= ball.radius + paddle.width / 2 &&
dy <= ball.radius + paddle.height / 2)
return true;
return false;
}
calculateBallVelocity(paddleCenterY, ball, side = 1) {
let relativeIntersectY = paddleCenterY - ball.y;
let normRelIntersectY = relativeIntersectY / this.def.PADDLEHEIGHT / 2;
let bounceAngle = normRelIntersectY * this.def.MAXBOUNCEANGLE;
ball.speed += this.def.BALLSPEEDINCR;
ball.vx = ball.speed * side * Math.cos(bounceAngle);
ball.vy = ball.speed * -Math.sin(bounceAngle);
}
keyUpHandler(ev) {
const idx = this.keys.indexOf(ev.key);
if (idx != -1)
this.keys.splice(idx, 1);
}
keyDownHandler(ev) {
if (!this.keys.includes(ev.key))
this.keys.push(ev.key);
}
}
class Paddle {
constructor(context, paddleSide, def) {
this.width = def.PADDLEWIDTH;
this.height = def.PADDLEHEIGHT;
this.x = paddleSide;
this.y = def.CANVASHEIGHT / 2 - this.height / 2;
this.ctx = context;
this.update();
}
update() {
this.ctx.fillStyle = 'black';
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
getCenter() {
return {
x: this.x + this.width / 2,
y: this.y + this.height / 2
};
}
}
class Ball {
constructor(context, def, startSide) {
this.radius = def.BALLRADIUS;
this.speed = def.BALLSPEED;
this.x = def.CANVASWIDTH / 2;
this.y = def.CANVASHEIGHT / 2;
this.vy = 0;
if (startSide === 0)
this.vx = -this.speed;
else
this.vx = this.speed;
this.ctx = context;
this.update();
}
update() {
this.ctx.fillStyle = 'black';
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
this.ctx.fill();
}
}

View File

@ -1,4 +1,4 @@
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js";
import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentificateView {
constructor(params) {
@ -10,8 +10,9 @@ export default class extends AbstractAuthentificateView {
return `
<h1>HOME</h1>
<a href="/matchmaking" class="nav__link" data-link>Play a game</a>
<a href="/game/offline" class="nav__link" data-link>Play offline</a>
<a href="/me" class="nav__link" data-link>Me</a>
<a href="/logout" class="nav__link" data-link>Logout</a>
`;
}
}
}

View File

@ -1,25 +1,64 @@
import { client, navigateTo } from "../index.js";
import AbstractView from "./AbstractView.js";
import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js";
function game_found(game_id)
{
navigateTo(`/games/${game_id}`)
}
export default class extends AbstractAuthentifiedView {
constructor(params)
{
super(params, "Matchmaking");
}
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
async press_button()
{
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()
{
await client.matchmaking.start(game_found)
console.log("start matchmaking")
document.getElementById("button").onclick = this.press_button.bind(this)
}
async getHtml() {
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>
`;
}
@ -27,4 +66,4 @@ export default class extends AbstractView {
{
await client.matchmaking.stop();
}
}
}

View File

@ -1,5 +1,6 @@
import { client, navigateTo } from "../index.js";
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js";
import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentificateView
{
@ -10,63 +11,39 @@ export default class extends AbstractAuthentificateView
async postInit()
{
if (this.fill() === null)
return;
document.getElementById("save-account-button").onclick = this.acccount_save;
document.getElementById("delete-account-button").onclick = this.account_delete_accounts;
document.getElementById("save-account-button").onclick = this.save_account;
document.getElementById("delete-account-button").onclick = this.delete_account;
document.getElementById("save-profile-button").onclick = this.save_profile;
}
async fill()
async delete_account()
{
let data = await client.account.get();
if (data === null)
{
navigateTo("/login")
return;
}
document.getElementById("username").value = data.username;
}
async delete_accounts()
{
let current_password = document.getElementById("current_password").value;
let current_password = document.getElementById("current_password-input").value;
let response_data = await client.account.delete(current_password);
if (response_data === null)
console.log(await client.isAuthentificate())
if (response_data === null || response_data === "user deleted")
{
navigateTo("/login");
return;
}
["delete", "current_password"].forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
if (response_data === "user deleted")
{
document.getElementById(`error_delete`).innerHTML = "OK";
navigateTo("/login")
return;
}
document.getElementById("error_current_password").innerHTML = response_data["password"]
clear("innerHTML", ["current_password-input"])
fill_errors({"current_password-input": response_data["password"]}, "innerHTML")
}
async save()
async save_account()
{
let username = document.getElementById("username").value;
let new_password = document.getElementById("new_password").value;
let current_password = document.getElementById("current_password").value;
let username = document.getElementById("username-input").value;
let new_password = document.getElementById("new_password-input").value;
let current_password = document.getElementById("current_password-input").value;
let data = {};
data.username = username;
if (new_password.length != 0)
data.new_password = new_password;
let response_data = await client.account.update(data, current_password);
if (response_data === null)
@ -74,58 +51,55 @@ export default class extends AbstractAuthentificateView
navigateTo("/login");
return;
}
else if (response_data === "data has been alterate")
{
navigateTo("/me");
return;
}
if (response_data === "data has been alterate")
response_data = {"save-account": "saved"}
clear("innerHTML", ["username", "new_password", "current_password", "save-account", "delete-account"])
fill_errors(response_data, "innerHTML")
}
["username", "new_password", "current_password"].forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
let avatar = document.getElementById("avatar");
async save_profile()
{
let avatar = document.getElementById("avatar-input");
if (avatar.files[0] !== undefined)
{
let form_data = new FormData();
form_data.append("file", avatar.files[0]);
await client.me.change_avatar(form_data)
await client.me.change_avatar(form_data);
}
document.getElementById("save-profile").innerHTML = "Saved";
}
async getHtml()
{
return `
<link rel="stylesheet" href="static/css/me.css">
<link rel="stylesheet" href="/static/css/me.css">
<h1>ME</h1>
<div class="account">
<h3>Account</h3>
<input type="text" placeholder="username" id="username">
<span id="error_username"></span>
<input type=password placeholder="new password" id="new_password">
<span id="error_new_password"></span>
<input type=password placeholder="current password" id="current_password">
<span id="error_current_password"></span>
<input type="button" value="Save Credentials" id="save-account-button">
<span id="error_save"></span>
<input type="button" value="Delete Account" id="delete-account-button">
<span id="error_delete"></span>
<div id="main">
<div class="account">
<h3>Account</h3>
<input type="text" placeholder="username" id="username-input" text=${client.me.username}>
<span id="username"></span>
<input type=password placeholder="new_password" id="new_password-input">
<span id="new_password"></span>
<input type=password placeholder="current_password" id="current_password-input">
<span id="current_password"></span>
<input type="button" value="Save Credentials" id="save-account-button">
<span id="save-account"></span>
<input type="button" value="Delete Account" id="delete-account-button">
<span id="delete-account"></span>
</div>
<div class="profile">
<h3>Profile</h3>
<input type="file" id="avatar-input" accept="image/png, image/jpeg">
<input type="button" value="Save profile" id="save-profile-button">
<span id="save-profile"></span>
</div>
<a href="/logout" class="nav__link" data-link>Logout</a>
</div>
<div class="profile">
<h3>Profile</h3>
<input type="file" id="avatar" accept="image/png, image/jpeg">
<input type="button" value="Save profile" id="save-profile-button">
<span id="error_save"></span>
</div>
<a href="/logout" class="nav__link" data-link>Logout</a>
`;
}
}
}

View File

@ -0,0 +1,14 @@
import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
}
async getHtml() {
return `
<h1>404 Bozo</h1>
<p>Git gud</p>
`;
}
}

View File

@ -0,0 +1,67 @@
import AbstractView from "./abstracts/AbstractView.js";
import { client } from "../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profile ");
this.user_id = params.id;
}
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.info = document.getElementById("info");
// Username
let username = document.createElement("a");
username.id = "username";
username.appendChild(document.createTextNode(this.profile.username));
this.info.appendChild(username);
this.info.appendChild(document.createElement("br"));
// Avatar
let avatar = document.createElement("img");
avatar.id = "avatar";
avatar.src = this.profile.avatar_url;
this.info.appendChild(avatar);
await this.blockButton();
}
async blockButton() {
// Block option
if (client.me.user_id != this.user_id) {
let block = document.getElementById("block") || document.createElement("a");
block.id = "block";
block.innerText = "";
block.onclick = async () => {
if (!this.profile.isBlocked)
await client.profiles.block(this.user_id);
else
await client.profiles.deblock(this.user_id);
this.profile = await client.profiles.getProfile(this.user_id);
this.blockButton();
};
if (this.profile.isBlocked)
block.appendChild(document.createTextNode("Deblock"));
else
block.appendChild(document.createTextNode("Block"));
this.info.appendChild(block);
}
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profile.css">
<div id="info">
</div>
`;
}
}

View File

@ -0,0 +1,227 @@
import AbstractView from "./abstracts/AbstractView.js";
import {client} from "../index.js";
import {Message} from "../api/chat/message.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Search");
}
async postInit() {
let search = document.getElementById("input_user");
search.oninput = this.users;
let chat_input = document.getElementById("input_chat");
//chat_input.addEventListener("keydown", this.chat_manager)
this.last_add_chat = undefined;
this.users();
this.chat();
}
async users() {
let search = document.getElementById("input_user").value.toLowerCase();
let logged = await client.isAuthentificate();
let users = await client.profiles.all();
let list_users = document.getElementById('list_users');
list_users.innerHTML = "";
users.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => {
if (user.user_id == null) {
console.log("list User one with id null;");
return;
}
var new_user = document.createElement("li");
// username
let username = document.createElement("a");
username.href = `/profiles/${user.user_id}`;
username.appendChild(document.createTextNode(user.username));
new_user.appendChild(username);
// space
new_user.appendChild(document.createTextNode(" "));
// chat
if (logged && client.me.user_id != user.user_id) {
let add_chat = document.createElement("a");
add_chat.id = "add_chat_off";
add_chat.onclick = async () => {
if (client.channel != undefined) {
client.channel.members_id.forEach((member_id) => {
if (member_id == user.user_id)
client.channel = undefined;
});
if (client.channel == undefined) {
add_chat.id = "add_chat_off";
this.last_add_chat = undefined;
return this.hideChat();
}
client.channel.disconnect();
}
client.channel = await client.channels.createChannel([client.me.user_id , user.user_id], this.chat);
this.chat();
if (this.last_add_chat != undefined)
this.last_add_chat.id = "add_chat_off";
this.last_add_chat = add_chat;
add_chat.id = "add_chat_on";
};
add_chat.appendChild(document.createTextNode("Chat"));
new_user.appendChild(add_chat);
new_user.appendChild(document.createTextNode(" "));
}
// break line
new_user.appendChild(document.createElement("br"));
// avatar
var img = document.createElement("img");
img.src = user.avatar_url;
new_user.appendChild(img);
list_users.appendChild(new_user);
});
//console.log(list_users);
}
async chat() {
let users = await client.profiles.all();
let logged = await client.isAuthentificate();
/*let reload = document.getElementById("messages");
if (reload != null)
reload.remove();*/
let reload = document.getElementById("members");
if (reload != null)
reload.remove();
if (client.channel == undefined || !logged)
return ;
let chats = document.getElementById("chats");
if (document.getElementById("chat") == null) {
let chat = document.createElement("div");
chat.id = "chat";
chats.appendChild(chat);
}
// div des messages
let messages = document.getElementById("messages");
if (messages == null) {
messages = document.createElement("div");
messages.id = "messages";
if (document.getElementById("input_chat") == null)
chat.appendChild(messages);
else
document.getElementById("input_chat").before(messages);
}
// les messages, réecriture seulement du dernier
let i = 0;
client.channel.messages.forEach((message) => {
if (messages.children[i] == null || message.content != messages.children[i].innerText) {
let text = document.createElement("p");
text.appendChild(document.createTextNode(message.content));
if (message.author_id == client.me.user_id)
text.id = "you";
else
text.id = "other";
messages.appendChild(text);
}
i++;
});
// Input pour rentrer un message
if (document.getElementById("input_chat") == null) {
let chat_input = document.createElement("input");
chat_input.id="input_chat";
chat_input.type="text";
chat_input.name="message";
chat_input.placeholder="message bozo";
chat_input.maxLength=255;
chat.appendChild(chat_input);
chat_input.onkeydown = async () => {
if (event.keyCode == 13 && client.channel != undefined) {
//let chat_input = document.getElementById("input_chat");
let chat_text = chat_input.value;
let receivers_id = [];
client.channel.members_id.forEach((member_id) => {
if (member_id != client.me.user_id)
receivers_id.push(users.filter(user => user.user_id == member_id)[0].user_id);
});
await client.channel.sendMessageChannel(chat_text, receivers_id)
// Reset
chat_input.value = "";
}
};
}
// nom des membres du chat
let members = document.createElement("h2");
members.id = "members";
let usernames = "";
client.channel.members_id.forEach((member_id) => {
if (member_id != client.me.user_id) {
if (usernames.length > 0)
usernames += ", ";
usernames += (users.filter(user => user.user_id == member_id)[0].username);
}
});
members.appendChild(document.createTextNode(usernames));
messages.before(members);
// Scroll to the bottom of messages
messages.scrollTop = messages.scrollHeight;
}
async hideChat() {
let close = document.getElementById("chat");
if (close != null)
close.remove();
}
async leavePage() {
if (client.channel != undefined)
client.channel.disconnect();
client.channel = undefined
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/search.css">
<div id="chats">
<div id="users">
<input id="input_user" type="text" name="message" placeholder="userbozo"/>
<ul id="list_users">
</ul>
</div>
</div>
`;
}
}

View File

@ -1,14 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Settings");
}
async getHtml() {
return `
<h1>Settings</h1>
<p>Manage your privacy and configuration.</p>
`;
}
}

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>
`
}
}

View File

@ -1,4 +1,4 @@
import { client, navigateTo } from "../index.js";
import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{

View File

@ -1,4 +1,4 @@
import { client, navigateTo } from "../index.js";
import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{

View File

@ -1,4 +1,4 @@
import { navigateTo } from "../index.js";
import { navigateTo } from "../../index.js";
import AbstractView from "./AbstractView.js";
export default class extends AbstractView{

View File

@ -1,10 +1,11 @@
import { client, navigateTo } from "../../index.js";
import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js";
import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function login()
{
let username = document.getElementById("username").value;
let password = document.getElementById("password").value;
let username = document.getElementById("username-input").value;
let password = document.getElementById("password-input").value;
let response_data = await client.login(username, password);
@ -14,17 +15,8 @@ async function login()
return;
}
["username", "user", "password"].forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
clear("innerHTML", ["username", "user", "password"]);
fill_errors(response_data, "innerHTML");
}
export default class extends AbstractNonAuthentifiedView {
@ -34,22 +26,22 @@ export default class extends AbstractNonAuthentifiedView {
async postInit()
{
document.getElementById("button").onclick = login;
document.getElementById("login-button").onclick = login;
}
async getHtml() {
return `
<div class=form>
<label>Login</label>
<link rel="stylesheet" href="static/css/accounts/login.css">
<input type="text" id="username" placeholder="username">
<span id="error_username"></span>
<input type="password" id="password" placeholder="password">
<span id="error_password"></span>
<input type="button" value="login" id="button">
<span id="error_user"></span>
<link rel="stylesheet" href="/static/css/accounts/login.css">
<input type="text" id="username-input" placeholder="username">
<span id="username"></span>
<input type="password" id="password-input" placeholder="password">
<span id="password"></span>
<input type="button" value="Login" id="login-button">
<span id="user"></span>
<a href="/register" class="nav__link" data-link>Register</a>
</div>
`;
}
}
}

View File

@ -1,5 +1,5 @@
import { client, navigateTo } from "../../index.js";
import AbstractAuthentifiedView from "../AbstractAuthentifiedView.js";
import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView
{
@ -8,4 +8,4 @@ export default class extends AbstractAuthentifiedView
client.logout();
navigateTo("/login")
}
}
}

View File

@ -1,10 +1,11 @@
import { client, navigateTo } from "../../index.js";
import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js";
import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function register()
{
let username = document.getElementById("username").value;
let password = document.getElementById("password").value;
let username = document.getElementById("username-input").value;
let password = document.getElementById("password-input").value;
let response_data = await client.account.create(username, password);
@ -14,17 +15,8 @@ async function register()
return;
}
["username", "user", "password"].forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
clear("innerHTML", ["username", "user", "password"]);
fill_errors(response_data, "innerHTML");
}
export default class extends AbstractNonAuthentifiedView {
@ -34,22 +26,22 @@ export default class extends AbstractNonAuthentifiedView {
async postInit()
{
document.getElementById("button").onclick = register;
document.getElementById("register-button").onclick = register;
}
async getHtml() {
return `
<div class=form>
<label>Register</label>
<link rel="stylesheet" href="static/css/accounts/register.css">
<input type="text" id="username" placeholder="username">
<span id="error_username"></span>
<input type="password" id="password" placeholder="password">
<span id="error_password"></span>
<input type="button" value="register" id="button">
<span id="error_user"></span>
<link rel="stylesheet" href="/static/css/accounts/register.css">
<input type="text" id="username-input" placeholder="username">
<span id="username"></span>
<input type="password" id="password-input" placeholder="password">
<span id="password"></span>
<input type="button" value="Register" id="register-button">
<span id="user"></span>
<a href="/login" class="nav__link" data-link>Login</a>
</div>
`;
}
}
}

View File

@ -1,29 +0,0 @@
import AbstractView from "../AbstractView.js";
import { client } from "../../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profile ");
this.user_id = params.id;
}
async postInit()
{
let profile = await client.profiles.getProfile(this.user_id);
let username_element = document.getElementById("username");
username_element.href = `/profiles/${this.user_id}`;
username_element.appendChild(document.createTextNode(profile.username));
let avatar_element = document.getElementById("avatar");
avatar_element.src = profile.avatar_url;
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profiles/profile.css">
<img id="avatar"/>
<a id="username"></a>
`;
}
}

View File

@ -1,40 +0,0 @@
import AbstractView from "../AbstractView.js";
import { client } from "../../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profiles");
}
async postInit()
{
let profiles = await client.profiles.all()
let profile_list_element = document.getElementById("profile-list")
profiles.forEach((profile) => {
let profile_element = document.createElement("div");
profile_element.className = "item";
let avatar = document.createElement("img");
avatar.src = profile.avatar_url;
let username = document.createElement("a");
username.href = `/profiles/${profile.user_id}`;
username.appendChild(document.createTextNode(profile.username));
profile_element.appendChild(avatar);
profile_element.appendChild(username);
profile_list_element.appendChild(profile_element)
});
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profiles/profiles.css">
<div id="profile-list">
</div>
`;
}
}

View File

@ -10,10 +10,10 @@
<body>
<nav class="nav">
<a href="/" class="nav__link" data-link>Dashboard</a>
<a href="/profiles" class="nav__link" data-link>Profiles</a>
<a href="/search" class="nav__link" data-link>Search</a>
<a href="/home" class="nav__link" data-link>Home</a>
<a href="/login" class="nav__link" data-link>Login</a>
<a href="/register" class="nav__link" data-link>Register</a>
<a href="/chat" class="nav__link" data-link>Chat</a>
</nav>
<div id="app"></div>
<script type="module" src="{% static 'js/index.js' %}"></script>

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 .objects.GameRoomManager import GameRoomManager
from channels.generic.websocket import AsyncWebsocketConsumer
# Create your models here.
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()
for user_id in users_id:
GameMembersModel(game_id=self.pk, member_id=user_id)
for player_id in players_id:
GameMembersModel(game_id = self.pk, player_id = player_id).save()
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):
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():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'transcendence.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -6,8 +6,7 @@ from games.models import GameModel
import json
queue_id: [int] = []
queue_ws: [WebsocketConsumer] = []
from .models import Waiter, WaitingRoom, WaitingRoomManager, normal
class MatchMaking(WebsocketConsumer):
@ -24,25 +23,19 @@ class MatchMaking(WebsocketConsumer):
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
queue_id.append(user.pk)
queue_ws.append(self)
waiting_room: WaitingRoom = normal.get(self.mode)
waiting_room.append(Waiter(user.pk, 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):
user: User = self.scope["user"]
global queue_id, queue_ws
if (user.pk in queue_id):
queue_ws.pop(queue_id.index(user.pk))
queue_id.remove(user.pk)
self.channel_layer.group_discard(self.group_name, self.channel_name)
waiting_room: WaitingRoom = normal.get(self.mode)
waiter: Waiter = waiting_room.get_member_by_socket(self)
if (waiter is not None):
waiting_room.remove(waiter, 1016)

View File

@ -1,3 +1,39 @@
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.
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
websocket_urlpatterns = [
re_path(r'ws/matchmaking/', consumers.MatchMaking.as_asgi())
re_path(r'ws/matchmaking/(?P<mode>\d+)$', consumers.MatchMaking.as_asgi())
]

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "ft_transcendence",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -1,6 +1,7 @@
from django.contrib import admin
from .models import ProfileModel
from .models import ProfileModel, BlockModel
# Register your models here.
admin.site.register(ProfileModel)
admin.site.register(ProfileModel)
admin.site.register(BlockModel)

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from django.db.models import IntegerField
def upload_to(instance, filename: str):
return f"./profiles/static/avatars/{instance.pk}.{filename.split('.')[1]}"
@ -17,4 +18,11 @@ class ProfileModel(models.Model):
def on_user_created(sender, instance, created, **kwargs):
if created:
profile: ProfileModel = ProfileModel.objects.create(pk = instance.pk, user = instance)
profile.save()
profile.save()
class BlockModel(models.Model):
blocker = IntegerField(primary_key=False)
blocked = IntegerField(primary_key=False)
def __str__(self):
return "blocker_id: " + str(self.blocker) + ", blocked_id: " + str(self.blocked)

View File

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

View File

@ -3,9 +3,13 @@ from django.conf import settings
from django.conf.urls.static import static
from . import viewsets
from . import views
urlpatterns = [
path("<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve', 'patch': 'partial_update'}), name="profile_page"),
path("me", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update', 'get': 'retrieve'}), name="my_profile_page"),
path("<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"),
path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"),
#path("me", viewsets.ProfileViewSet.as_view(), name="my_profile_page"),
] + static("/static/avatars/", document_root="./avatars")
path("block", views.BlocksView.as_view(), name="block_page"),
path("block/<int:pk>", views.BlockView.as_view(), name="block_page"),
] + static("/static/avatars/", document_root="./avatars")

66
profiles/views.py Normal file
View File

@ -0,0 +1,66 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.renderers import JSONRenderer
from django.core import serializers
from .models import BlockModel
class BlockView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
def get(self, request, pk):
block = BlockModel.objects.filter(pk=pk)
if (block.exists()):
return Response(serializers.serialize("json", block), status=status.HTTP_200_OK)
else:
return Response("Not Found", status=status.HTTP_404_NOT_FOUND)
class BlocksView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
def get(self, request):
blocks = BlockModel.objects.filter(blocker=request.user.pk)
if (blocks):
return Response(serializers.serialize("json", BlockModel.objects.filter(blocker=request.user.pk)), status=status.HTTP_200_OK)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
def post(self, request):
data: dict = request.data
users_id = request.data.get("users_id", None)
if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST)
if (BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])):
return Response({"Already Exist"}, status=status.HTTP_409_CONFLICT)
new_block = BlockModel()
new_block.blocker = users_id[0]
new_block.blocked = users_id[1]
new_block.save()
return Response({"block_id": new_block.pk}, status=status.HTTP_201_CREATED)
def delete(self, request):
data: dict = request.data
users_id = request.data.get("users_id", None)
if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST)
block = BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])
print(list(block))
if (block.count() > 1):
return Response("Not normal >:[", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if (not block):
return Response("Don't exist", status=status.HTTP_404_NOT_FOUND)
block.delete()
return Response("Deleted", status=status.HTTP_200_OK)

View File

@ -3,6 +3,7 @@ from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework import permissions, status
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from django.http import HttpRequest
from django.db.models import QuerySet
@ -17,7 +18,9 @@ class ProfileViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def retrieve(self, request: HttpRequest, pk=None):
instance = ProfileModel.objects.get(pk=pk)
if (not self.queryset().filter(pk=pk).exists()):
return Response({"detail": "Profile not found."}, status=status.HTTP_404_NOT_FOUND)
instance = self.queryset().get(pk=pk)
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK)
@ -28,17 +31,28 @@ class ProfileViewSet(viewsets.ModelViewSet):
profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:]
return Response(serializer.data)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class MyProfileViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer):
query: QuerySet = ProfileModel.objects.filter(pk=self.request.user.pk)
if (not query.exists()):
return Response("profile not found", status=status.HTTP_400_BAD_REQUEST)
profile: ProfileModel = ProfileModel.objects.get(pk=self.request.user.pk)
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
serializer_class = ProfileSerializer
queryset = ProfileModel.objects.all
def get_object(self):
obj = self.queryset().get(pk=self.request.user.pk)
return obj
def perform_update(self, serializer, pk=None):
profile: ProfileModel = self.get_object()
avatar = self.request.data.get("file", None)
if (avatar is not None):
if (profile.avatar_url.name != "./profiles/static/avatars/default.avif"):
profile.avatar_url.storage.delete(profile.avatar_url.name)
profile.avatar_url = avatar
profile.save()
profile.save()
def retrieve(self, request: HttpRequest, pk=None):
instance: ProfileModel = self.get_object()
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK)

View File

@ -1,10 +1,30 @@
asgiref==3.7.2
attrs==23.1.0
autobahn==23.6.2
Automat==22.10.0
cffi==1.16.0
channels==4.0.0
constantly==23.10.4
cryptography==41.0.7
daphne==4.0.0
Django==4.2.6
django-cors-headers==4.3.0
django-restapi==0.1.3
djangorestframework==3.14.0
hyperlink==21.0.0
idna==3.6
incremental==22.10.0
install==1.3.5
Pillow==10.1.0
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
pyOpenSSL==23.3.0
pytz==2023.3.post1
service-identity==23.1.0
six==1.16.0
sqlparse==0.4.4
channels==4.0.0
daphne
Twisted==23.10.0
txaio==23.1.1
typing_extensions==4.8.0
zope.interface==6.1

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,17 +13,21 @@ from channels.auth import AuthMiddlewareStack
import chat.routing
import matchmaking.routing
import tournament.routing
import games.routing
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'http':get_asgi_application(),
'websocket':AuthMiddlewareStack(
URLRouter(
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.
@ -43,11 +43,13 @@ INSTALLED_APPS = [
'channels',
'daphne',
'tournament.apps.TournamentConfig',
'matchmaking.apps.MatchmakingConfig',
'games.apps.GamesConfig',
'accounts.apps.AccountsConfig',
'profiles.apps.ProfilesConfig',
'frontend.apps.FrontendConfig',
'chat.apps.ChatConfig',
'corsheaders',
'rest_framework',
@ -59,7 +61,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
]
ASGI_APPLICATION = 'trancendence.asgi.application'
ASGI_APPLICATION = 'transcendence.asgi.application'
CHANNEL_LAYERS = {
'default' :{
@ -79,7 +81,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'trancendence.urls'
ROOT_URLCONF = 'transcendence.urls'
TEMPLATES = [
{
@ -97,7 +99,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'trancendence.wsgi.application'
WSGI_APPLICATION = 'transcendence.wsgi.application'
# Database
@ -150,4 +152,4 @@ STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

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