From 7a0ff15cec9497f1fcd81366d2d99fb210049872 Mon Sep 17 00:00:00 2001 From: starnakin Date: Tue, 14 May 2024 03:28:05 +0200 Subject: [PATCH] game: online: recoded but collision not work --- frontend/static/js/api/game/AGame.js | 5 +- frontend/static/js/api/game/pong/PongGame.js | 65 ++- .../static/js/api/game/pong/PongMyPlayer.js | 2 +- .../static/js/api/game/pong/PongPlayer.js | 17 +- frontend/static/js/index.js | 2 +- frontend/static/js/views/PongOnlineView.js | 525 +++++++----------- games/config.py | 2 +- games/consumers.py | 2 +- games/models.py | 4 +- games/objects/APlayer.py | 5 - games/objects/pong/Ball.py | 3 +- games/objects/pong/PongGame.py | 249 +++++---- games/objects/pong/PongPlayer.py | 10 +- games/objects/pong/PongSpectator.py | 6 +- games/objects/pong/Position.py | 2 +- games/routine.py | 272 ++++----- 16 files changed, 518 insertions(+), 653 deletions(-) diff --git a/frontend/static/js/api/game/AGame.js b/frontend/static/js/api/game/AGame.js index 82df1c9..b72c227 100644 --- a/frontend/static/js/api/game/AGame.js +++ b/frontend/static/js/api/game/AGame.js @@ -2,6 +2,7 @@ import { AExchangeable } from "../AExchangable.js"; import { APlayer } from "./APlayer.js"; import { Client } from "../Client.js" import { sleep } from "../../utils/sleep.js"; +import { Profile } from "../Profile.js"; export class AGame extends AExchangeable { @@ -44,9 +45,9 @@ export class AGame extends AExchangeable this._disconntectHandler = disconntectHandler; /** - * @type {Number} + * @type {Profile} */ - this.winnerId; + this.winner; /** * @type {Number} diff --git a/frontend/static/js/api/game/pong/PongGame.js b/frontend/static/js/api/game/pong/PongGame.js index fe83365..d323c6b 100644 --- a/frontend/static/js/api/game/pong/PongGame.js +++ b/frontend/static/js/api/game/pong/PongGame.js @@ -6,7 +6,6 @@ import { Client } from "../../Client.js"; import { PongBall } from "./PongBall.js"; import { sleep } from "../../../utils/sleep.js"; import { Wall } from "./Wall.js" -import { Point } from "./Point.js"; import { Position } from "./Position.js"; export class PongGame extends AGame @@ -16,9 +15,10 @@ export class PongGame extends AGame * @param {CallableFunction} goal_handler * @param {CallableFunction} finish_handler * @param {CallableFunction} disconnect_handler + * @param {CallableFunction} startHandler * @param {Number} id */ - constructor(client, id, disconnectHandler, goalHandler, finishHandler) + constructor(client, id, disconnectHandler, goalHandler, startHandler, finishHandler) { super(client, id, undefined, disconnectHandler, "pong"); @@ -28,6 +28,11 @@ export class PongGame extends AGame * @type {CallableFunction} */ this._goalHandler = goalHandler; + + /** + * @type {CallableFunction} + */ + this._startHandler = startHandler; /** * @type {CallableFunction} @@ -39,7 +44,6 @@ export class PongGame extends AGame */ this.time; - /** * @type {Boolean} */ @@ -53,7 +57,7 @@ export class PongGame extends AGame /** * @type {Ball} */ - this.ball = new PongBall(this, undefined, new Position()); + this.ball = new PongBall(this, undefined, new Position(), 0, 0); /** * @type {[Wall]} @@ -122,18 +126,14 @@ export class PongGame extends AGame */ render(ctx) { - if(ctx instanceof CanvasRenderingContext2D) - ctx.clearRect(0, 0, this.config.MAP_SIZE_Y, this.config.MAP_SIZE_Y); + ctx.clearRect(0, 0, this.config.MAP_SIZE_Y, this.config.MAP_SIZE_Y); this.drawSides(ctx); this.ball.render(ctx); - if(ctx instanceof CanvasRenderingContext2D) - { - ctx.strokeStyle = "#000000"; - ctx.lineWidth = this.config.STROKE_THICKNESS; - ctx.stroke(); - } + ctx.strokeStyle = "#000000"; + ctx.lineWidth = this.config.STROKE_THICKNESS; + ctx.stroke(); } /** @@ -149,6 +149,8 @@ export class PongGame extends AGame */ async _receive(data) { + console.log(data) + if (this._inited === false && data.detail === "init_game") { this._initGame(data); @@ -162,7 +164,23 @@ export class PongGame extends AGame else if (data.detail === "goal") await this._receiveGoal(data); else if (data.detail === "finish") - await this._finishHandler(data); + await this._receiveFinish(data); + else if (data.detail === "start") + await this._receiveStart(); + } + + async _receiveFinish(data) + { + const winner = this.players.find(player => player.id === data.winner_id) + this.finished = true; + await this._finishHandler(winner); + } + + + async _receiveStart() + { + this.started = true; + await this._startHandler(); } async _receiveGoal(data) @@ -176,6 +194,7 @@ export class PongGame extends AGame } player.score.push(data.timestamp) + console.log(player) await this._goalHandler(player); } @@ -192,26 +211,6 @@ export class PongGame extends AGame this.ball.import(data); } - async _updateGoal(data) - { - /** - * @type { Player } - */ - let player_found; - - this.players.forEach(player => { - if (data.player_id === player.id) - { - player_found = player; - return; - } - }); - - player_found.score.push(data.timestamp); - - await this._goalHandler(player_found); - } - _initGame(data) { data.walls.forEach((wall_data) => { diff --git a/frontend/static/js/api/game/pong/PongMyPlayer.js b/frontend/static/js/api/game/pong/PongMyPlayer.js index 26efb4e..7d89401 100644 --- a/frontend/static/js/api/game/pong/PongMyPlayer.js +++ b/frontend/static/js/api/game/pong/PongMyPlayer.js @@ -4,7 +4,7 @@ import { Segment } from "./Segment.js"; import { PongGame } from "./PongGame.js"; import { Position } from "./Position.js"; -export class MyPlayer extends PongPlayer +export class PongMyPlayer extends PongPlayer { /** * @param {Client} client diff --git a/frontend/static/js/api/game/pong/PongPlayer.js b/frontend/static/js/api/game/pong/PongPlayer.js index 79d40b8..8c739be 100644 --- a/frontend/static/js/api/game/pong/PongPlayer.js +++ b/frontend/static/js/api/game/pong/PongPlayer.js @@ -17,8 +17,9 @@ export class PongPlayer extends APlayer * @param {String} username * @param {String} avatar * @param {Client} client + * @param {Boolean} isEliminated */ - constructor(client, game, id, username, avatar, score = [], rail = new Segment(game), position = new Position(0.5), isConnected) + constructor(client, game, id, username, avatar, score = [], rail = new Segment(game), position = new Position(0.5), isConnected, isEliminated) { super(client, game, id, username, avatar, isConnected) @@ -41,6 +42,11 @@ export class PongPlayer extends APlayer * @type {PongPlayer} */ this.game = game; + + /** + * @type {Boolean} + */ + this.isEliminated = isEliminated; } /** @@ -57,13 +63,10 @@ export class PongPlayer extends APlayer */ draw(ctx) { - if (this.isConnected === false) + if (this.isConnected === false || this.isEliminated === true) { - if(ctx instanceof CanvasRenderingContext2D) - { - ctx.moveTo(this.rail.start.x, this.rail.start.y); - ctx.lineTo(this.rail.stop.x, this.rail.stop.y); - } + ctx.moveTo(this.rail.start.x, this.rail.start.y); + ctx.lineTo(this.rail.stop.x, this.rail.stop.y); return; } diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index 97e325c..b4cfba3 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -4,7 +4,7 @@ import Search from "./views/Search.js"; import HomeView from "./views/HomeView.js"; import LogoutView from "./views/accounts/LogoutView.js"; -import { PongOnlineView } from "./views/PongOnlineView.js" +import PongOnlineView from "./views/PongOnlineView.js" import { PongOfflineView } from "./views/PongOfflineView.js" import { TicTacToeOnlineView } from "./views/TicTacToeOnlineView.js" diff --git a/frontend/static/js/views/PongOnlineView.js b/frontend/static/js/views/PongOnlineView.js index 5351544..9edc05a 100644 --- a/frontend/static/js/views/PongOnlineView.js +++ b/frontend/static/js/views/PongOnlineView.js @@ -1,214 +1,95 @@ -import { client, reloadView, lang } from "../index.js"; -import { initShaderProgram, shaderInfos } from "../3D/shaders.js" -import { initBuffers } from "../3D/buffers.js" -import "../3D/maths/gl-matrix-min.js" -import "../chartjs/chart.umd.min.js"; -import { get_labels, transformData } from "../utils/graph.js"; -import { sleep } from "../utils/sleep.js"; -import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; +import { client, reloadView } from "../index.js"; import { PongGame } from "../api/game/pong/PongGame.js"; -import { MyPlayer } from "../api/game/pong/PongMyPlayer.js"; import { PongPlayer } from "../api/game/pong/PongPlayer.js"; +import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; +import { Profile } from "../api/Profile.js"; +import { PongMyPlayer } from "../api/game/pong/PongMyPlayer.js"; -export class PongOnlineView extends AbstractAuthenticatedView +export default class PongOnlineView extends AbstractAuthenticatedView { constructor(params) { - super(params, "Game"); - - this.game_id = params.id; - this.ctx = null; - this.shader_prog = null; - this.buffers = null; - this.cam_pos = [0, 400, 0]; - this.cam_target = [0, 0, 0]; - this.cam_up = [0, 0, -1]; - this.game_mode = 1; // 1 is 2D, 2 is 3D + super(params, 'Pong'); /** - * @type {MyPlayer} + * @type {Number} */ - this.myPlayer; - } + this.game_id = params.id; + + /** + * @type {PongGame} + */ + this.game; - initWebGL() - { - let canva = document.createElement("canvas"); - canva.height = this.game.config.MAP_SIZE_X; - canva.width = this.game.config.MAP_SIZE_Y; - canva.id = "canva"; - document.getElementById("app").appendChild(canva); + /** + * @type {HTMLCanvasElement} + */ + this.canva; - this.ctx = canva.getContext("webgl"); + /** + * @type {CanvasRenderingContext2D} + */ + this.gameboard; - if(this.ctx === null) - { - alert("Unable to initialize WebGL. Your browser or machine may not support it. You may also be a bozo"); - return; - } + /** + * @type {HTMLElement} + */ + this.gamestate; - this.shader_prog = initShaderProgram(this.ctx); - this.buffers = initBuffers(this.ctx); + /** + * @type {HTMLTableElement} + */ + this.scoreboard; - this.ctx.enable(this.ctx.CULL_FACE); - this.ctx.cullFace(this.ctx.BACK); + /** + * @type {HTMLElement} + */ + this.app = document.getElementById("app"); + + /** + * @type {[]} + */ + this.keysPressed } - init2D() + createMyPlayer() { - let canva = document.createElement("canvas"); - canva.height = this.game.config.MAP_SIZE_X; - canva.width = this.game.config.MAP_SIZE_Y; - canva.id = "canva"; - document.getElementById("app").appendChild(canva); + let index = this.game.players.findIndex((player) => player.id === client.me.id); - this.ctx = canva.getContext('2d'); + if (index === -1) + return; + + let myPlayer = this.game.players[index]; + + if (myPlayer.isEliminated) + return; + + this.keysPressed = []; + + this.myPlayer = new PongMyPlayer(client, + this.game, + myPlayer.score, + myPlayer.rail, + myPlayer.position, + ); + + myPlayer = this.myPlayer; + + this.registerKey(); } keyReleaseHandler(event) { - const idx = this.keys_pressed.indexOf(event.key); + const idx = this.keysPressed.indexOf(event.key); if (idx != -1) - this.keys_pressed.splice(idx, 1); + this.keysPressed.splice(idx, 1); } keyPressHandler(event) { - if (!this.keys_pressed.includes(event.key)) - this.keys_pressed.push(event.key); - } - - setNormalAttribute() - { - const numComponents = 3; - const type = this.ctx.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.buffers.normal); - this.ctx.vertexAttribPointer( - shaderInfos.attribLocations.vertexNormal, - numComponents, - type, - normalize, - stride, - offset, - ); - this.ctx.enableVertexAttribArray(shaderInfos.attribLocations.vertexNormal); - } - - setPositionAttribute() - { - const numComponents = 3; - const type = this.ctx.FLOAT; - const normalize = false; - const stride = 0; - const offset = 0; - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.buffers.vertex); - this.ctx.bindBuffer(this.ctx.ELEMENT_ARRAY_BUFFER, this.buffers.index); - this.ctx.vertexAttribPointer( - shaderInfos.attribLocations.vertexPosition, - numComponents, - type, - normalize, - stride, - offset - ); - this.ctx.enableVertexAttribArray(shaderInfos.attribLocations.vertexPosition); - } - - renderGame() - { - const canva = document.getElementById('canva'); - if (canva === null) - return 1; - - if(this.ctx instanceof CanvasRenderingContext2D) - { - this.ctx.beginPath(); - this.game.render(this.ctx); - this.ctx.strokeStyle = "#000000"; - this.ctx.lineWidth = 1; - this.ctx.stroke(); - } - else if(this.ctx instanceof WebGLRenderingContext) - { - this.ctx.clearColor(0.1, 0.1, 0.1, 1.0); - this.ctx.clearDepth(1.0); - this.ctx.enable(this.ctx.DEPTH_TEST); - this.ctx.depthFunc(this.ctx.LEQUAL); - this.ctx.clear(this.ctx.COLOR_BUFFER_BIT | this.ctx.DEPTH_BUFFER_BIT); - - const projectionMatrix = mat4.create(); - const viewMatrix = mat4.create(); - - mat4.perspective(projectionMatrix, (90 * Math.PI) / 180, this.ctx.canvas.clientWidth / this.ctx.canvas.clientHeight, 0.1, 10000000.0); - mat4.lookAt(viewMatrix, this.cam_pos, this.cam_target, this.cam_up); - - this.setPositionAttribute(); - this.setNormalAttribute(); - - this.ctx.useProgram(shaderInfos.program); - - this.ctx.uniformMatrix4fv(shaderInfos.uniformLocations.projectionMatrix, false, projectionMatrix); - this.ctx.uniformMatrix4fv(shaderInfos.uniformLocations.viewMatrix, false, viewMatrix); - - this.game.render(this.ctx); - } - else - { - alert('Unknown rendering context type'); - } - } - - /** - * @param {PongPlayer} player - * @returns { Promise } - */ - async onGoal(player) - { - document.getElementById(`goal-${player.id}`).innerText = player.score.length; - } - - /** - * @param {*} data - * @returns { Promise } - */ - async onFinish(data /* unused */) - { - await reloadView(); - } - - render() - { - let loop_id = setInterval(() => { - if (this.game === undefined) - clearInterval(loop_id); - this.myPlayer?.updatePaddle(this.keys_pressed); - this.renderGame(); - this.game?.time?.new_frame(); - //clearInterval(loop_id); - // 1 sec fps - }, 1000 / 60); - } - - async toggleGameMode() - { - if(this.game_mode === 1) // 3D - { - this.game_mode = 2; - document.getElementById("game-mode").value = "Switch to 2D"; - } - else if(this.game_mode === 2) // 2D - { - this.game_mode = 1; - document.getElementById("game-mode").value = "Switch to 3D"; - } - const canva = document.getElementById('canva'); - if (canva === null) - return; - document.getElementById("app").removeChild(canva); - this.createGameBoard(this.game_mode); + console.log("bozo") + if (!this.keysPressed.includes(event.key)) + this.keysPressed.push(event.key); } registerKey() @@ -226,187 +107,155 @@ export class PongOnlineView extends AbstractAuthenticatedView } /** - * @param {int} game_mode - * @returns { Cramptex } + * @param {PongPlayer} player */ - createGameBoard(game_mode) + async onGoal(player) { - if(game_mode === 1) - this.init2D(); - else if(game_mode === 2) - this.initWebGL(); + document.getElementById(`score-${player.id}`).innerText = player.score.length; } - createMyPlayer() + async onStart() { - let index = this.game.players.findIndex((player) => player.id === client.me.id); - - if (index !== -1) - { - let myPlayer = this.game.players[index]; - - this.myPlayer = new MyPlayer(client, - this.game, - myPlayer.score, - myPlayer.rail, - myPlayer.position, - ); - - this.game.players[index] = this.myPlayer; - } + this.gamestate.innerHTML = this.game.getState(); } - async joinGame() + /** + * @param {PongPlayer} winner + */ + async onFinish(winner) { - document.getElementById("game-mode").onclick = this.toggleGameMode.bind(this); - - await this.game.join(); - await this.game.waitInit(); - - this.createGameBoard(1); // create the board for 2D game by default. Can switch to 3D with a toggle - this.createMyPlayer(); - this.displayPlayersList(); - this.registerKey(); - this.render(); - } - - async updateGameState() - { - document.getElementById("game-state").innerText = this.game.state; - - if (this.game.finished === false) - await this.joinGame(); - else - { - this.createGraph(); - let toggle = document.getElementById("game-mode"); - if(toggle !== null) - toggle.remove(); - } - } - - createGraph() - { - let players = this.game.players; - - if (players === undefined) - return; - - let graph = document.createElement("canvas"); - - graph.height = 450; - graph.width = 800; - graph.id = "graph"; - - document.getElementById("app").appendChild(graph); - - if (graph === null) - return; - - let datasets = []; - - players.forEach(player => { - - let data = transformData(player.score); - - data = [{x: 0, y: 0}, ...data]; - - data.push({x: Math.round((this.game.stop_timestamp - this.game.start_timestamp) / 1000), - y: data[data.length - 1].y}); - - datasets.push({ - data: data, - label: player.username, - borderColor: `#${Math.floor(Math.random() * 16777215).toString(16)}`, - fill: false, - }); - }); - - this.chart = new Chart(graph, { - type: "line", - data: { - labels: get_labels(datasets), - datasets: datasets, - } - }); - } - - displayPlayersList() - { - let table = document.createElement("table"), - thead = document.createElement("thead"), - tr = document.createElement("tr"), - usernameTitle = document.createElement("th"), - goalTitle = document.createElement("th"), - playersList = document.createElement("tbody") - ; - - usernameTitle.innerText = lang.get("gamePlayersListName"); - goalTitle.innerText = lang.get("gameGoalTaken"); - - tr.appendChild(usernameTitle); - tr.appendChild(goalTitle); - - table.appendChild(thead); - table.appendChild(playersList); - - document.getElementById("app").appendChild(table); - - this.game.players.forEach(player => { - - let tr = document.createElement("tr"); - let name = document.createElement("td"); - let goal = document.createElement("td"); - - name.id = `username-${player.id}`; - name.innerText = player.username; - - goal.id = `goal-${player.id}`; - goal.innerText = player.score.length; - - tr.appendChild(name); - tr.appendChild(goal); - - playersList.appendChild(tr); - }); + this.gamestate.innerHTML = this.game.getState(); + this.destroyGameboard(); + this.displayWinner(winner) } async onDisconnect() { - sleep(500); await reloadView(); } + createScoreboard() + { + this.scoreboard = document.createElement("table"); + + this.app.appendChild(this.scoreboard); + + let row = document.createElement("tr"); + + this.game.players.forEach(player => { + let th = document.createElement("th"); + th.innerText = player.username; + row.appendChild(th); + }); + + this.scoreboard.appendChild(row); + + row = document.createElement("tr"); + + this.game.players.forEach(player => { + let th = document.createElement("th"); + th.id = `score-${player.id}`; + th.innerText = player.score.length; + this.scoreboard.appendChild(th); + }); + + this.scoreboard.appendChild(row); + } + + createGameboard() + { + this.canva = document.createElement("canvas"); + + this.app.appendChild(this.canva) + + this.canva.height = this.game.config.MAP_SIZE_Y + this.canva.width = this.game.config.MAP_SIZE_X + + this.gameboard = this.canva.getContext('2d'); + } + + destroyGameboard() + { + this.app.removeChild(this.canva); + } + + /** + * + * @param {Profile} winner + */ + displayWinner(winner) + { + const winnerStand = document.createElement("h1"); + + document.getElementById("app").appendChild(winnerStand); + + winnerStand.innerText = `winner: ${winner.username}`; + } + + async connect() + { + await this.game.join(); + await this.game.waitInit(); + } + async postInit() { - this.game = new PongGame(client, this.game_id, this.onDisconnect, this.onGoal, this.onFinish); - this.keys_pressed = []; + this.gamestate = document.getElementById("gamestate"); + + this.game = new PongGame(client, this.game_id, this.onStart.bind(this), this.onGoal.bind(this), this.onStart.bind(this), this.onFinish.bind(this)); - let error_code = await this.game.init(); + const errorCode = await this.game.init(); + if (errorCode) + return errorCode; - if (error_code) - return error_code; + this.gamestate.innerHTML = this.game.getState(); - await this.updateGameState(); + if (this.game.finished) + { + this.displayWinner(this.game.winner); + } + else + { + await this.connect(); + this.createScoreboard(); + this.createGameboard(); + this.createMyPlayer(); + this.render(); + } } - async leavePage() + render() { - if (this.game.finished === false) - { - this.game.leave(); - this.game = undefined; - } - this.game = undefined; - this.unregisterKey(); + let loop_id = setInterval(() => { + if (this.game === undefined) + clearInterval(loop_id); + this.myPlayer?.updatePaddle(this.keysPressed); + this.renderGame(); + this.game?.time?.new_frame(); + //clearInterval(loop_id); + // 1 sec fps + }, 1000 / 60); + } + + renderGame() + { + this.gameboard.beginPath(); + this.game.render(this.gameboard); + this.gameboard.strokeStyle = "#000000"; + this.gameboard.lineWidth = 1; + this.gameboard.stroke(); } async getHtml() { return /* HTML */ ` - -

- +

`; } -} + + async leavePage() + { + this.unregisterKey(); + this.game.leave() + } +} \ No newline at end of file diff --git a/games/config.py b/games/config.py index 4762851..906d737 100644 --- a/games/config.py +++ b/games/config.py @@ -21,4 +21,4 @@ SERVER_TPS = 20 STROKE_THICKNESS = 10 -GAME_MAX_SCORE = 7 \ No newline at end of file +GAME_MAX_SCORE = 3 \ No newline at end of file diff --git a/games/consumers.py b/games/consumers.py index b2ed99c..fa1c759 100644 --- a/games/consumers.py +++ b/games/consumers.py @@ -103,7 +103,7 @@ class PongWebSocket(WebsocketConsumer): self.disconnect(1404) return - self.member: PongPlayer | PongSpectator = self.game.join(self.user.pk, self) + self.member: PongPlayer | PongSpectator = self.game.join(self.user, self) def disconnect(self, code): if (self.member is not None): diff --git a/games/models.py b/games/models.py index fe4718e..b523cce 100644 --- a/games/models.py +++ b/games/models.py @@ -45,11 +45,11 @@ class GameModel(models.Model): return score_data - def add_goal(self, goal_taker_id: int): + def add_goal(self, goal_defenser: User): timestamp: int = round(time.time() * 1000, 1) - self.start_timestamp - goal_model: GameGoalModel = GameGoalModel.objects.create(player_id = goal_taker_id, game_id = self.pk, timestamp = timestamp) + goal_model: GameGoalModel = GameGoalModel(player=goal_defenser, game=self, timestamp=timestamp) goal_model.save() diff --git a/games/objects/APlayer.py b/games/objects/APlayer.py index 1d4bcca..148448e 100644 --- a/games/objects/APlayer.py +++ b/games/objects/APlayer.py @@ -3,11 +3,6 @@ from __future__ import annotations from channels.generic.websocket import WebsocketConsumer from .ASpectator import ASpectator -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from games.objects.AGame import AGame - class APlayer(ASpectator): def is_connected(self) -> bool: diff --git a/games/objects/pong/Ball.py b/games/objects/pong/Ball.py index 55b865f..ada7b90 100644 --- a/games/objects/pong/Ball.py +++ b/games/objects/pong/Ball.py @@ -16,11 +16,12 @@ class Ball: self.speed: float self.reset() + self.speed = 0 def reset(self) -> None: self.size = config.BALL_SIZE self.position = Position(Point(config.BALL_SPAWN_POS_X + self.size / 2, config.BALL_SPAWN_POS_Y + self.size / 2), time.time()) - self.angle = math.pi * 0.3 + self.angle = math.pi * 1 self.speed = config.BALL_SPEED_START def to_dict(self) -> dict: diff --git a/games/objects/pong/PongGame.py b/games/objects/pong/PongGame.py index 76296c7..4af615e 100644 --- a/games/objects/pong/PongGame.py +++ b/games/objects/pong/PongGame.py @@ -1,184 +1,191 @@ -from transcendence.abstract.AbstractRoom import AbstractRoom -from transcendence.abstract.AbstractRoomMember import AbstractRoomMember - -from channels.generic.websocket import WebsocketConsumer - from ..AGame import AGame -from .Ball import Ball from .PongPlayer import PongPlayer from .PongSpectator import PongSpectator from .Wall import Wall -from .Point import Point from .Segment import Segment - -import math +from .Point import Point +from .Ball import Ball from ... import config +from channels.generic.websocket import WebsocketConsumer + +from django.contrib.auth.models import User + from ...routine import routine import threading -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from profiles.models import ProfileModel - class PongGame(AGame): - + def __init__(self, game_id: int, game_manager): - super().__init__("pong", game_id, game_manager) - self.ball: Ball = Ball() - - self.stopped: bool = False - - radius: float = min(config.MAP_SIZE_X, config.MAP_SIZE_Y) / 2 - 10 - - players: list[ProfileModel] = self.model.get_players() - - nb_sides = 4 - - polygon: list[Point] = [] - - for i in range(nb_sides): - - angle: float = (i * 2 * math.pi / nb_sides) + (math.pi * 3 / nb_sides) - - x: float = round(config.MAP_CENTER_X + radius * math.cos(angle)) - y: float = round(config.MAP_CENTER_Y + radius * math.sin(angle)) - - polygon.append(Point(x, y)) - - segments: list[Segment] = [] - for i in range(nb_sides): - segments.append(Segment(polygon[i], polygon[(i + 1) % nb_sides])) - - self.walls: list[Wall] self.players: list[PongPlayer] + self.walls: list[Wall] + + players: list[User] = self.model.get_players() nb_players: int = len(players) if (nb_players == 2): - self.players = [PongPlayer(self, players[0], None, segments[0]), PongPlayer(self, players[1], None, segments[2])] - self.walls = [Wall(segments[1].start, segments[1].stop), Wall(segments[3].start, segments[3].stop)] + corners = [Point(50, config.MAP_CENTER_Y - config.MAP_SIZE_Y / 4), + Point(config.MAP_SIZE_X - 50, config.MAP_CENTER_Y - config.MAP_SIZE_Y / 4), + Point(config.MAP_SIZE_X - 50, config.MAP_CENTER_Y + config.MAP_SIZE_Y / 4), + Point(50, config.MAP_CENTER_Y + config.MAP_SIZE_Y / 4) + ] + + self.players = [PongPlayer(self, players[0], None, Segment(corners[1].copy(), corners[2].copy())), + PongPlayer(self, players[1], None, Segment(corners[0].copy(), corners[3].copy()))] + + self.walls = [Wall(corners[0], corners[1]), + Wall(corners[2], corners[3])] else: + corners: list[Point] = [Point(50, 50), + Point(config.MAP_SIZE_X - 50, 50), + Point(config.MAP_SIZE_X - 50, config.MAP_CENTER_Y - 50), + Point(50, config.MAP_SIZE_Y - 50)] + self.players = [] self.walls = [] + for i in range(4): - if (i < nb_players): - self.players.append(PongPlayer(self, players[i], None, segments[i])) + if i < nb_players: + self.players.append(PongPlayer(self, players[i], None, Segment(corners[i], corners[(i + 1) % 4]))) else: - self.walls.append(Wall(segments[i])) + self.walls.append(Segment(corners[i], corners[(i + 1) % 4])) - self.spectators: list[PongSpectator] + self.ball: Ball = Ball() + + def goal(self, goal_defenser: PongPlayer) -> None: - self._updated_players: list[PongPlayer] = [] + timestamp: int = goal_defenser.add_goal() + + self.broadcast("goal", {"player_id": goal_defenser.user.pk, + "timestamp": timestamp}) + + if len(goal_defenser.score) >= config.GAME_MAX_SCORE: + + self.eliminate(goal_defenser) + + player_list = self.get_valid_players() + + if len(player_list) == 1: + self.finish(player_list[0]) + return + + self.ball.reset() + self.broadcast("update_ball", self.ball.to_dict()) - self.game_id: int = game_id + def get_valid_players(self) -> list[PongPlayer]: + return [player for player in self.players if player.is_connected and not player.is_eliminated] + + def finish(self, winner: PongPlayer) -> bool: + self.broadcast("finish", {'winner_id': winner.user.pk}) + self.model.finish(winner.user) + self.stopped = True + + def start(self): + + # Set to true to stop the thread routine + self.stopped: bool = False + + self.model.start() + + self.broadcast("start") - self.thread = threading.Thread(target = routine, args=(self,)) + self.ball.reset() + + self.broadcast("update_ball", self.ball.to_dict()) + + self.thread = threading.Thread(target=routine, args=(self,)) self.thread.start() - def goal(self, goal_taker: PongPlayer): + def eliminate(self, eliminated: PongPlayer): - timestamp = goal_taker.add_goal() + self.broadcast("eliminated", {"eliminated_id": eliminated.user.pk}) - self.broadcast("goal", {"player_id": goal_taker.user_id, - "timestamp": timestamp}) - if (len(goal_taker.score) >= config.GAME_MAX_SCORE): - connected_players: list[PongPlayer] = self.get_players_connected() - if (len(connected_players) == 2): - self.finish(connected_players[not connected_players.index(goal_taker)]) - else: - goal_taker.eliminate() - - def _send_game_data(self, member: PongSpectator | PongPlayer): - member.send("init_game", self.to_dict()) - - def _everbody_is_here(self): - return len(self.players) == len(self.get_players_connected()) - - def _nobody_is_here(self): - return len(self.get_players_connected()) == 0 - - def _player_join(self, user_id: int, socket: WebsocketConsumer): + eliminated.eliminate() + + def _player_join(self, user: User, socket: WebsocketConsumer) -> PongPlayer | None: if (self.model.started): return None - player = self.get_player_by_user_id(user_id) + player = self.get_player_by_user_id(user.pk) if (player is None): return None # check if player is already connected if (player.is_connected()): player.disconnect(1001) - + player.socket = socket - if (self._everbody_is_here()): + self.update_player(player) + + if len(self.players) == len(self.get_players_connected()): self.start() - - self.update_player(player) - + return player - - def update_player(self, player: PongPlayer): - self._updated_players.append(player) - - def finish(self, winner: PongPlayer): - self.broadcast("finish", {"winner": winner.to_dict()}) - self.model.finish(winner.user_id) - - def _player_leave(self, player: PongPlayer): + + def _spectator_join(self, user: User, socket: WebsocketConsumer) -> PongSpectator: - connected_players: list[PongPlayer] = self.get_players_connected() - - if (self.model.started): - if (len(connected_players) == 1): - print([player.username for player in connected_players]) - last_player: PongPlayer = connected_players[0] - self.finish(last_player) - return - - self.update_player(player) - - def _spectator_join(self, user_id: int, socket: WebsocketConsumer): - - spectator: PongSpectator = PongSpectator(user_id, socket, self) + spectator: PongSpectator = PongSpectator(user, socket, self) self.spectators.append(spectator) return spectator - def _spectator_leave(self, spectator: PongSpectator): - self.spectators.remove(spectator) - - def join(self, user_id: int, socket: WebsocketConsumer) -> PongSpectator | PongPlayer: - member: PongPlayer = self._player_join(user_id, socket) - if (member is None): - member: PongSpectator = self._spectator_join(user_id, socket) - self._send_game_data(member) - return member - - def start(self): - if (self.model.started == True): - return - self.model.start() + def join(self, user: User, socket: WebsocketConsumer) -> PongSpectator | PongPlayer: + + member: PongPlayer | PongSpectator + + member = self._player_join(user, socket) + + if member is None: + member = self._spectator_join(user, socket) + + self._send_game_data(member) + + return member + + def _player_leave(self, player: PongPlayer): + + if self.model.started: + + self.eliminate(player) + + players: list[PongPlayer] = self.get_valid_players() + + if len(players) == 1: + self.finish(players[0]) + + def _spectator_leave(self, spectator: PongSpectator): + self.spectators.remove(spectator) + + def leave(self, member: PongSpectator | PongPlayer): - def leave(self, member: AbstractRoomMember): if (isinstance(member, PongPlayer)): self._player_leave(member) elif (isinstance(member, PongSpectator)): self._spectator_leave(member) - if (self._nobody_is_here()): - self.stopped = True - self.thread.join(10) - self.game_manager.remove(self) + + if self.model.started: + if len(self.get_players_connected()) + len(self.spectators) == 0: + self.stopped = True + if hasattr(self, 'thread'): + self.thread.join(10) + self.game_manager.remove(self) - def to_dict(self): + def _send_game_data(self, member: PongSpectator | PongPlayer): + + member.send("init_game", self.to_dict()) + + def update_player(self, player: PongPlayer): + self.broadcast("update_player", player.to_dict(), [player]) + + def to_dict(self): data: dict = {"ball": self.ball.to_dict(), "players": [player.to_dict() for player in self.players], diff --git a/games/objects/pong/PongPlayer.py b/games/objects/pong/PongPlayer.py index 9efe82d..b642a21 100644 --- a/games/objects/pong/PongPlayer.py +++ b/games/objects/pong/PongPlayer.py @@ -27,14 +27,14 @@ class PongPlayer(APlayer): self.rail: Segment = rail + self.is_eliminated: bool = False + self.game: PongGame def eliminate(self): - self.disconnect(1000) + self.is_eliminated = True - self.game.update_player(self) - def receive(self, data: dict): detail: str = data.get("detail") @@ -108,7 +108,7 @@ class PongPlayer(APlayer): def add_goal(self): - timestamp = self.game.model.add_goal(self.user_id) + timestamp = self.game.model.add_goal(self.user) self.score.append(timestamp) @@ -122,6 +122,8 @@ class PongPlayer(APlayer): "position": self.position.to_dict(), "score": self.score, + "isEliminated": self.is_eliminated, + "rail": self.rail.to_dict(), "isConnected": self.is_connected(), diff --git a/games/objects/pong/PongSpectator.py b/games/objects/pong/PongSpectator.py index 9a82260..1dbe02d 100644 --- a/games/objects/pong/PongSpectator.py +++ b/games/objects/pong/PongSpectator.py @@ -2,7 +2,7 @@ from __future__ import annotations from channels.generic.websocket import WebsocketConsumer -from transcendence.abstract.AbstractRoomMember import AbstractRoomMember +from django.contrib.auth.models import User from typing import TYPE_CHECKING @@ -15,8 +15,8 @@ from .Ball import Ball class PongSpectator(ASpectator): - def __init__(self, user_id: int, socket: WebsocketConsumer, game: PongGame): - super().__init__(user_id, socket, game) + def __init__(self, user: User, socket: WebsocketConsumer, game: PongGame): + super().__init__(user, socket, game) self.game: PongGame = game def send_paddle(self, player: PongPlayer): diff --git a/games/objects/pong/Position.py b/games/objects/pong/Position.py index 63f6da8..718c0b8 100644 --- a/games/objects/pong/Position.py +++ b/games/objects/pong/Position.py @@ -5,7 +5,7 @@ class Position: def __init__(self, location: int | Point = 0, time: int = 0) -> None: self.time: float = time - self.location: float = location + self.location: float | Point = location def copy(self): try: diff --git a/games/routine.py b/games/routine.py index 6b1a0a5..9c69079 100644 --- a/games/routine.py +++ b/games/routine.py @@ -1,25 +1,145 @@ from __future__ import annotations +from .objects.pong.PongGame import PongPlayer +from .objects.pong.Segment import Segment +from .objects.pong.Point import Point +from .objects.pong.Ball import Ball + from typing import TYPE_CHECKING if TYPE_CHECKING: - from .objects.pong.PongPlayer import PongPlayer from .objects.pong.PongGame import PongGame - from .objects.pong.Ball import Ball - -from .objects.pong.Point import Point -from .objects.pong.Segment import Segment from . import config import math -import asyncio from asgiref.sync import SyncToAsync +import asyncio + VERTICALLY = 1 NORMAL = 2 +def get_player_hitted(players: list[PongPlayer], segment: Segment) -> PongPlayer | None: + + for player in players: + if (player.rail is segment): + return player + return None + +def wall_collision(ball_angle: float, wall: Segment) -> float: + + wall_angle: float = wall.angle() + + cos: float = math.cos(wall_angle) * -1 + sin: float = math.sin(wall_angle) + + wall_angle: float = math.atan2(sin, cos) + + incident_angle: float = ball_angle - wall_angle + + reflection_angle: float = wall_angle - incident_angle + + return reflection_angle + +async def paddle_collision(game: PongGame, impact: Point, player: PongPlayer, inc_x: float, inc_y: float): + + diff_x: float = player.rail.stop.x - player.rail.start.x + diff_y: float = player.rail.stop.y - player.rail.start.y + + paddle_center_x: float = player.rail.start.x + diff_x * player.position.location + paddle_center_y: float = player.rail.start.y + diff_y * player.position.location + + paddle_center: Point = Point(paddle_center_x, paddle_center_y) + + rail_length: float = player.rail.length() + paddle_length: float = rail_length * config.PADDLE_RATIO + + start_x: float = paddle_center.x - (diff_x * (paddle_length / 2 / rail_length)) + start_y: float = paddle_center.y - (diff_y * (paddle_length / 2 / rail_length)) + stop_x: float = paddle_center.x + (diff_x * (paddle_length / 2 / rail_length)) + stop_y: float = paddle_center.y + (diff_y * (paddle_length / 2 / rail_length)) + + start: Point = Point(start_x, start_y) + stop: Point = Point(stop_x, stop_y) + + paddle: Segment = Segment(start, stop) + + hit_point: Point = Point(impact.x - inc_x, impact.y - inc_y) + + if not paddle.is_on(hit_point): + await asyncio.sleep(0.1) # delay to create frontend animation + await SyncToAsync(game.goal)(player) + return None + + paddle_angle: float = paddle.angle() + + normal: float = paddle_angle - math.pi / 2 + + start_distance: float = paddle.start.distance(impact) + stop_distance: float = paddle.stop.distance(impact) + + hit_percent: float = (start_distance) / (start_distance + stop_distance) + + hit_percent = round(hit_percent, 1) + + new_angle: float = normal + (math.pi * 0.85) * (hit_percent - 0.5) + + return new_angle + +async def collision(game: PongGame, impact_data: dict) -> bool: + + segment: Segment = impact_data.get("segment") + + player_hitted = None + for player in game.players: + if (not player.is_connected()): + continue + if (player.rail is segment): + player_hitted = player + break + + angle: float + + print(impact_data.get("impact")) + + if (player_hitted is None): + print("wall") + angle = wall_collision(game.ball.angle, segment) + else: + print("paddle") + angle = await paddle_collision(game, impact_data.get("impact"), player_hitted, impact_data.get("inc_x"), impact_data.get("inc_y")) + + if (angle is None): + return False + + print((game.ball.angle % (math.pi * 2)) * 180 / math.pi, (angle % (math.pi * 2)) * 180 / math.pi) + + game.ball.speed += config.BALL_SPEED_INC + game.ball.angle = angle + + return True + + +async def update_ball(game: PongGame, impact_data: dict): + + distance: float = impact_data.get("distance") + + time_before_impact: float = distance / game.ball.speed + + print(time_before_impact) + + await asyncio.sleep(time_before_impact) + + hit: bool = await collision(game, impact_data) + + print("HIT" * 10 + str(hit)) + + if hit: + game.ball.position.location = impact_data.get("impact") + SyncToAsync(game.broadcast)("update_ball", game.ball.to_dict()) + def get_sign(num: float) -> int: if (num == 0): return 0 @@ -153,143 +273,31 @@ def get_impact_data(segments: list[Segment], ball: Ball) -> dict: return closest -def wall_collision(ball_angle: float, wall: Segment) -> float: - - wall_angle: float = wall.angle() - - cos: float = math.cos(wall_angle) * -1 - sin: float = math.sin(wall_angle) - - wall_angle: float = math.atan2(sin, cos) - - incident_angle: float = ball_angle - wall_angle - - reflection_angle: float = wall_angle - incident_angle - - return reflection_angle - -async def paddle_collision(game: PongGame, impact: Point, player: PongPlayer, inc_x: float, inc_y: float): - - diff_x: float = player.rail.stop.x - player.rail.start.x - diff_y: float = player.rail.stop.y - player.rail.start.y - - paddle_center_x: float = player.rail.start.x + diff_x * player.position.location - paddle_center_y: float = player.rail.start.y + diff_y * player.position.location - - paddle_center: Point = Point(paddle_center_x, paddle_center_y) - - rail_length: float = player.rail.length() - paddle_length: float = rail_length * config.PADDLE_RATIO - - start_x: float = paddle_center.x - (diff_x * (paddle_length / 2 / rail_length)) - start_y: float = paddle_center.y - (diff_y * (paddle_length / 2 / rail_length)) - stop_x: float = paddle_center.x + (diff_x * (paddle_length / 2 / rail_length)) - stop_y: float = paddle_center.y + (diff_y * (paddle_length / 2 / rail_length)) - - start: Point = Point(start_x, start_y) - stop: Point = Point(stop_x, stop_y) - - paddle: Segment = Segment(start, stop) - - hit_point: Point = Point(impact.x - inc_x, impact.y - inc_y) - - if (not paddle.is_on(hit_point)): - await SyncToAsync(game.goal)(player) - return None - - paddle_angle: float = paddle.angle() - - normal: float = paddle_angle - math.pi / 2 - - start_distance: float = paddle.start.distance(impact) - stop_distance: float = paddle.stop.distance(impact) - - hit_percent: float = (start_distance) / (start_distance + stop_distance) - - hit_percent = round(hit_percent, 1) - - new_angle: float = normal + (math.pi * 0.85) * (hit_percent - 0.5) - - return new_angle - -async def collision(game: PongGame, impact_data: dict) -> bool: - - segment: Segment = impact_data.get("segment") - - player_hitted = None - for player in game.players: - if (not player.is_connected()): - continue - if (player.rail is segment): - player_hitted = player - break - - angle: float - - if (player_hitted is None): - angle = wall_collision(game.ball.angle, segment) - else: - angle = await paddle_collision(game, impact_data.get("impact"), player_hitted, impact_data.get("inc_x"), impact_data.get("inc_y")) - - if (angle is None): - return False - - game.ball.speed += config.BALL_SPEED_INC - game.ball.angle = angle - - return True - -async def update_ball(game: PongGame, impact_data: dict) -> None: - - distance: float = impact_data.get("distance") - - time_before_impact: float = distance / game.ball.speed - - await asyncio.sleep(time_before_impact) - - hit: bool = await collision(game, impact_data) - - if (hit == False): - await asyncio.sleep(0.1) # delay to create frontend animation - game.ball.reset() - else: - game.ball.position.location = impact_data.get("impact") - - await SyncToAsync(game.broadcast)("update_ball", game.ball.to_dict()) - async def render_ball(game: PongGame): + + segments: list[Segment] = [player.rail for player in game.players] + game.walls while True: - segments: list[Segment] = [player.rail for player in game.players] + game.walls - impact_data: dict = get_impact_data(segments, game.ball) await update_ball(game, impact_data) + +async def async_routine(game: PongGame): -async def render_players(game: PongGame): + #TODO DEBUG collision + + ball_routine = asyncio.create_task(render_ball(game)) while True: - for player in game._updated_players: - await SyncToAsync(game.broadcast)("update_player", player.to_dict(), [player]) - - game._updated_players.clear() - - await asyncio.sleep(1 / config.SERVER_TPS) - -async def render(game: PongGame): - - routine_ball = asyncio.create_task(render_ball(game)) - routine_players = asyncio.create_task(render_players(game)) - - while(True): - if (game.stopped): - routine_ball.cancel() - routine_players.cancel() + + if game.stopped: + + ball_routine.cancel() return + await asyncio.sleep(0.05) def routine(game: PongGame): - - asyncio.run(render(game)) - + + asyncio.run(async_routine(game)) \ No newline at end of file