J'ai fais n'imp

This commit is contained in:
Xamora 2023-12-16 12:06:05 +01:00
commit 42287d4a86
26 changed files with 482 additions and 21 deletions

View File

@ -22,8 +22,15 @@ pip install -r requirements.txt
``` ```
- Setup database - Setup database
``` ```
<<<<<<< HEAD
python manage.py runserver makemigrations profiles python manage.py runserver makemigrations profiles
python manage.py migrate profiles python manage.py migrate profiles
=======
python manage.py makemigrations games
python manage.py makemigrations profiles
python manage.py runserver makemigrations chat
python manage.py migrate
>>>>>>> d03f90367f1fd075e79f4a69d31c5a49b9e53d01
``` ```
- Start the developpement server - Start the developpement server
``` ```

View File

@ -1,4 +1,5 @@
import { Account } from "./account.js"; import { Account } from "./account.js";
import { MatchMaking } from "./matchmaking.js";
import { Profile } from "./profile.js"; import { Profile } from "./profile.js";
import { Profiles } from "./profiles.js"; import { Profiles } from "./profiles.js";
@ -19,6 +20,7 @@ class Client
this._url = url; this._url = url;
this.account = new Account(this); this.account = new Account(this);
this.profiles = new Profiles(this); this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this);
this._logged = undefined; this._logged = undefined;
} }

View File

@ -0,0 +1,37 @@
import { client, navigateTo } from "../index.js"
class MatchMaking
{
/**
* @param {client} client
*/
constructor(client)
{
/**
* @type {client}
*/
this.client = client
}
async start(func)
{
if (!await this.client.isAuthentificate())
return null;
let url = `wss://${window.location.host}/ws/matchmaking/`;
this._chatSocket = new WebSocket(url);
this._chatSocket.onmessage = function (event) {
const data = JSON.parse(event.data);
func(data.game_id)
};
}
async stop()
{
this._chatSocket.close()
}
}
export {MatchMaking}

View File

@ -1,16 +1,20 @@
import { Client } from "./api/client.js";
import LoginView from "./views/accounts/LoginView.js"; import LoginView from "./views/accounts/LoginView.js";
import Dashboard from "./views/Dashboard.js"; import Dashboard from "./views/Dashboard.js";
import Settings from "./views/Settings.js";
import Search from "./views/Search.js"; import Search from "./views/Search.js";
import Chat from "./views/Chat.js"; import Chat from "./views/Chat.js";
import HomeView from "./views/HomeView.js"; import HomeView from "./views/HomeView.js";
import RegisterView from "./views/accounts/RegisterView.js"; import RegisterView from "./views/accounts/RegisterView.js";
import LogoutView from "./views/accounts/LogoutView.js"; import LogoutView from "./views/accounts/LogoutView.js";
import GameView from "./views/Game.js"
import PageNotFoundView from './views/PageNotFoundView.js'
import { Client } from "./api/client.js";
import AbstractRedirectView from "./views/AbstractRedirectView.js"; import AbstractRedirectView from "./views/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import MeView from "./views/MeView.js";
import ProfilePageView from "./views/profiles/ProfilePageView.js"; import ProfilePageView from "./views/profiles/ProfilePageView.js";
import MatchMakingView from "./views/MatchMakingView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.protocol + "//" + location.host)
@ -36,7 +40,6 @@ const router = async (uri) => {
const routes = [ const routes = [
{ path: "/", view: Dashboard }, { path: "/", view: Dashboard },
{ path: "/profiles/:id", view: ProfilePageView }, { path: "/profiles/:id", view: ProfilePageView },
{ path: "/settings", view: Settings },
{ path: "/login", view: LoginView }, { path: "/login", view: LoginView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
{ path: "/register", view: RegisterView }, { path: "/register", view: RegisterView },
@ -44,6 +47,8 @@ const router = async (uri) => {
{ path: "/chat", view: Chat }, { path: "/chat", view: Chat },
{ path: "/home", view: HomeView }, { path: "/home", view: HomeView },
{ path: "/me", view: MeView }, { path: "/me", view: MeView },
{ path: "/matchmaking", view: MatchMakingView },
{ path: "/game/offline", view: GameView },
]; ];
// Test each route for potential match // Test each route for potential match
@ -58,7 +63,10 @@ const router = async (uri) => {
if (!match) { if (!match) {
match = { match = {
route: routes[0], route: {
path: uri,
view: PageNotFoundView
},
result: [uri] result: [uri]
}; };
} }

View File

@ -0,0 +1,250 @@
import AbstractView from './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

@ -9,6 +9,8 @@ export default class extends AbstractAuthentificateView {
async getHtml() { async getHtml() {
return ` return `
<h1>HOME</h1> <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="/me" class="nav__link" data-link>Me</a>
<a href="/logout" class="nav__link" data-link>Logout</a> <a href="/logout" class="nav__link" data-link>Logout</a>
`; `;

View File

@ -0,0 +1,29 @@
import { client, navigateTo } from "../index.js";
import AbstractView from "./AbstractView.js";
function game_found(game_id)
{
navigateTo(`/games/${game_id}`)
}
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
}
async postInit()
{
await client.matchmaking.start(game_found)
}
async getHtml() {
return `
<h1>finding<h1>
`;
}
async leavePage()
{
await client.matchmaking.stop();
}
}

View File

@ -0,0 +1,14 @@
import AbstractView from "./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

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

0
games/__init__.py Normal file
View File

3
games/admin.py Normal file
View File

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

6
games/apps.py Normal file
View File

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

14
games/models.py Normal file
View File

@ -0,0 +1,14 @@
from django.db import models
# Create your models here.
class GameModel(models.Model):
def create(self, users_id: [int]):
self.save()
for user_id in users_id:
GameMembersModel(game_id=self.pk, member_id=user_id)
return self.pk
class GameMembersModel(models.Model):
game_id = models.IntegerField()
member_id = models.IntegerField()

3
games/tests.py Normal file
View File

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

3
games/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
matchmaking/__init__.py Normal file
View File

3
matchmaking/admin.py Normal file
View File

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

6
matchmaking/apps.py Normal file
View File

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

48
matchmaking/consumers.py Normal file
View File

@ -0,0 +1,48 @@
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from games.models import GameModel
import json
queue_id: [int] = []
queue_ws: [WebsocketConsumer] = []
class MatchMaking(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_name = "matchmaking"
self.group_name = "matchmaking"
def connect(self):
user: User = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
self.channel_layer.group_add(self.group_name, self.channel_name)
self.accept()
global queue_id, queue_ws
queue_id.append(user.pk)
queue_ws.append(self)
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)

3
matchmaking/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

6
matchmaking/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/matchmaking/', consumers.MatchMaking.as_asgi())
]

3
matchmaking/tests.py Normal file
View File

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

3
matchmaking/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -1,10 +1,30 @@
asgiref==3.7.2 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==4.2.6
django-cors-headers==4.3.0 django-cors-headers==4.3.0
django-restapi==0.1.3 django-restapi==0.1.3
djangorestframework==3.14.0 djangorestframework==3.14.0
hyperlink==21.0.0
idna==3.6
incremental==22.10.0
install==1.3.5 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 pytz==2023.3.post1
service-identity==23.1.0
six==1.16.0
sqlparse==0.4.4 sqlparse==0.4.4
channels==4.0.0 Twisted==23.10.0
daphne txaio==23.1.1
typing_extensions==4.8.0
zope.interface==6.1

View File

@ -10,7 +10,9 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
import os import os
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
import chat.routing import chat.routing
import matchmaking.routing
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
@ -20,7 +22,8 @@ application = ProtocolTypeRouter({
'http':get_asgi_application(), 'http':get_asgi_application(),
'websocket':AuthMiddlewareStack( 'websocket':AuthMiddlewareStack(
URLRouter( URLRouter(
chat.routing.websocket_urlpatterns chat.routing.websocket_urlpatterns +
matchmaking.routing.websocket_urlpatterns
) )
) )
}) })

View File

@ -43,6 +43,8 @@ INSTALLED_APPS = [
'channels', 'channels',
'daphne', 'daphne',
'matchmaking.apps.MatchmakingConfig',
'games.apps.GamesConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'profiles.apps.ProfilesConfig', 'profiles.apps.ProfilesConfig',
'frontend.apps.FrontendConfig', 'frontend.apps.FrontendConfig',