Merge branch 'main' of codeberg.org:adrien-lsh/ft_transcendence

This commit is contained in:
Xamora 2024-05-14 03:28:36 +02:00
commit c91ca959f6
16 changed files with 518 additions and 653 deletions

View File

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

View File

@ -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) => {

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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 */ `
<link rel="stylesheet" href="/static/css/game.css">
<h2 id="game-state"></h2>
<input type="button" value="Switch to 3D" id="game-mode">
<h1 id='gamestate'></h1>
`;
}
}
async leavePage()
{
this.unregisterKey();
this.game.leave()
}
}

View File

@ -21,4 +21,4 @@ SERVER_TPS = 20
STROKE_THICKNESS = 10
GAME_MAX_SCORE = 7
GAME_MAX_SCORE = 3

View File

@ -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):

View File

@ -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()

View File

@ -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:

View File

@ -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:

View File

@ -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],

View File

@ -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(),

View File

@ -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):

View File

@ -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:

View File

@ -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))