Merge branch 'cherif_back_and_game' of bitbucket.org:LuckyLaszlo/ft_transcendence

This commit is contained in:
batche
2022-12-22 18:45:10 +01:00
128 changed files with 4267 additions and 1703 deletions

View File

@@ -24,8 +24,7 @@ start_dev:
start_prod:
docker compose -f ${DOCKERCOMPOSEPATH} start prod
restart:stop
@make up
re: down dev
down:
docker compose -f ${DOCKERCOMPOSEPATH} -v down

50
memo.txt Normal file
View File

@@ -0,0 +1,50 @@
DONE :
TODO :
- timeout in gameserver for private match (TO test)
- If in game, destroy game scripts stuff when changing page.
(I need to dig deeper in svelte to know how this could work)
- mode spectateur
- quelques routes cote serveur
- une interface cote front (liste des matchs en cours)
- etat du client (en ligne, en jeu, ...)
- le chat
-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
"Bonus" :
- HTTPS
- mettre le site en ligne
-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
BUG :
- Bug de son étonnant dans le front, ça pop une fois de temps en temps :
Uncaught (in promise) DOMException: The element has no supported sources.
18.ogg et 24.ogg bug peut-etre.
- l'avatar ne se charge pas après avoir redémarré les containers (mais sans avoir supprimé les volumes)
normal ou oubli ?
-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
- Comment fonctionne .env ? Comment faire pour ne pas le push sur le depot ?
- certains status 200 pourrait peut-être être du 204 ?
(exemple dans TwoFactorAuthentication.svelte)
A la place de :
```
if (response.status === 401) {
// Wrong
}
if (response.status === 200) {
// Ok
}
```
On pourrait mettre :
```
if (!response.ok) {
// Wrong
}
else {
// Ok
}
```

View File

@@ -1,4 +1,5 @@
NODE_ENV=development
WEBSITE_HOST=transcendance
POSTGRES_USER=postgres
POSTGRES_PASSWORD=9pKpKEgiamxwk5P7Ggsz
POSTGRES_DB=transcendance_db

View File

@@ -16,6 +16,18 @@ services:
- postgresql
- redis
game_server:
build:
context: ./requirements/game_server
dockerfile: Dockerfile
environment:
NODE_ENV: "${NODE_ENV}"
restart: unless-stopped
ports:
- "8042:8042"
depends_on:
- backend_dev
frontend_dev:
build:
context: ./requirements/svelte
@@ -42,6 +54,7 @@ services:
restart: unless-stopped
volumes:
- ./requirements/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./requirements/nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro
command: [nginx-debug, "-g", "daemon off;"]
ports:
- "8080:8080"
@@ -68,8 +81,6 @@ services:
redis:
container_name: nestjs_redis
image: redis:alpine
expose:
- "6379"
restart: unless-stopped
environment:
REDIS_HOST: "${REDIS_HOST}"

View File

@@ -0,0 +1,15 @@
FROM node:alpine AS build
WORKDIR /usr/app
COPY ./game_back ./
RUN npm install typescript
RUN npx tsc
WORKDIR /usr/app/src/server
EXPOSE 8042
CMD [ "node", "wsServer.js"]

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@@ -0,0 +1,121 @@
{
"name": "game_back",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
},
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.9.4"
}
},
"node_modules/@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"requires": {}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"type": "module",
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.9.4"
},
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
}
}

View File

@@ -0,0 +1,36 @@
import { WebSocket } from "../wsServer.js";
import { Racket } from "../../shared_js/class/Rectangle.js";
import { GameSession } from "./GameSession.js";
import * as ev from "../../shared_js/class/Event.js"
import * as en from "../../shared_js/enums.js"
export class Client {
socket: WebSocket;
id: string; // same as "socket.id"
isAlive: boolean = true;
gameSession: GameSession = null;
constructor(socket: WebSocket, id: string) {
this.socket = socket;
this.id = id;
}
}
export class ClientPlayer extends Client {
token: string;
username: string;
matchOptions: en.MatchOptions = 0;
inputBuffer: ev.EventInput = new ev.EventInput();
lastInputId: number = 0;
racket: Racket;
constructor(socket: WebSocket, id: string, racket: Racket) {
super(socket, id);
this.racket = racket;
}
}
export class ClientSpectator extends Client {
constructor(socket: WebSocket, id: string) {
super(socket, id);
}
}

View File

@@ -0,0 +1,12 @@
import * as en from "../../shared_js/enums.js"
import { GameComponents } from "../../shared_js/class/GameComponents.js";
export class GameComponentsServer extends GameComponents {
scoreLeft: number = 0;
scoreRight: number = 0;
constructor(options: en.MatchOptions)
{
super(options);
}
}

View File

@@ -0,0 +1,267 @@
import * as en from "../../shared_js/enums.js"
import * as ev from "../../shared_js/class/Event.js"
import * as c from "../constants.js"
import { ClientPlayer, ClientSpectator } from "./Client";
import { GameComponentsServer } from "./GameComponentsServer.js";
import { clientInputListener, clientTerminate } from "../wsServer.js";
import { random } from "../utils.js";
import { Ball } from "../../shared_js/class/Rectangle.js";
import { wallsMovements } from "../../shared_js/wallsMovement.js";
/*
multiples methods of GameSession have parameter "s: GameSession".
its used with calls to setTimeout(),
because "this" is not equal to the GameSession but to "this: Timeout"
*/
export class GameSession {
id: string; // url ?
playersMap: Map<string, ClientPlayer> = new Map();
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
spectatorsMap: Map<string, ClientSpectator> = new Map();
gameLoopInterval: NodeJS.Timer | number = 0;
playersUpdateInterval: NodeJS.Timer | number = 0;
spectatorsUpdateInterval: NodeJS.Timer | number = 0;
components: GameComponentsServer;
matchOptions: en.MatchOptions;
isPrivateMatch: boolean; // WIP: could be used to separate leaderboards for example.
matchEnded: boolean = false;
lastStateSnapshot: ev.EventGameUpdate;
actual_time: number;
last_time: number;
delta_time: number;
constructor(id: string, matchOptions: en.MatchOptions, isPrivateMatch: boolean = false) {
this.id = id;
this.matchOptions = matchOptions;
this.isPrivateMatch = isPrivateMatch;
this.components = new GameComponentsServer(this.matchOptions);
}
start() {
const gc = this.components;
setTimeout(this.resume, c.matchStartDelay, this);
let timeout = c.matchStartDelay + c.newRoundDelay;
gc.ballsArr.forEach((ball) => {
setTimeout(this._newRound, timeout, this, ball);
timeout += c.newRoundDelay*0.5;
});
}
resume(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.on("message", clientInputListener);
});
s.actual_time = Date.now();
s.lastStateSnapshot = s._gameStateSnapshot();
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
s.playersUpdateInterval = setInterval(s._playersUpdate, c.playersUpdateIntervalMS, s);
s.spectatorsUpdateInterval = setInterval(s._spectatorsUpdate, c.spectatorsUpdateIntervalMS, s);
}
pause(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.off("message", clientInputListener);
});
clearInterval(s.gameLoopInterval);
clearInterval(s.playersUpdateInterval);
clearInterval(s.spectatorsUpdateInterval);
}
instantInputDebug(client: ClientPlayer) {
this._handleInput(c.fixedDeltaTime, client);
}
private _handleInput(delta: number, client: ClientPlayer) {
// if (client.inputBuffer === null) {return;}
const gc = this.components;
const input = client.inputBuffer.input;
if (input === en.InputEnum.up) {
client.racket.dir.y = -1;
}
else if (input === en.InputEnum.down) {
client.racket.dir.y = 1;
}
if (input !== en.InputEnum.noInput) {
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
}
client.lastInputId = client.inputBuffer.id;
// client.inputBuffer = null;
}
private _gameLoop(s: GameSession) {
/* s.last_time = s.actual_time;
s.actual_time = Date.now();
s.delta_time = (s.actual_time - s.last_time) / 1000; */
s.delta_time = c.fixedDeltaTime;
// WIP, replaced by instantInputDebug() to prevent desynchro
/* s.playersMap.forEach( (client) => {
s._handleInput(s.delta_time, client);
}); */
const gc = s.components;
gc.ballsArr.forEach((ball) => {
s._ballMovement(s.delta_time, ball);
});
if (s.matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(s.delta_time, gc);
}
}
private _ballMovement(delta: number, ball: Ball) {
const gc = this.components;
if (ball.ballInPlay)
{
ball.moveAndBounce(delta, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
if (ball.pos.x > c.w
|| ball.pos.x < 0 - ball.width)
{
ball.ballInPlay = false;
if (this.matchEnded) {
return;
}
this._scoreUpdate(ball);
setTimeout(this._newRound, c.newRoundDelay, this, ball);
}
}
}
private _scoreUpdate(ball: Ball) {
const gc = this.components;
if (ball.pos.x > c.w) {
++gc.scoreLeft;
}
else if (ball.pos.x < 0 - ball.width) {
++gc.scoreRight;
}
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
}
private _playersUpdate(s: GameSession) {
s.lastStateSnapshot = s._gameStateSnapshot();
s.playersMap.forEach( (client) => {
s.lastStateSnapshot.lastInputId = client.lastInputId;
client.socket.send(JSON.stringify(s.lastStateSnapshot));
});
}
private _spectatorsUpdate(s: GameSession) {
s.lastStateSnapshot.lastInputId = 0;
s.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(s.lastStateSnapshot));
});
}
private _gameStateSnapshot() : ev.EventGameUpdate {
const gc = this.components;
const snapshot = new ev.EventGameUpdate();
snapshot.playerLeft.y = gc.playerLeft.pos.y;
snapshot.playerRight.y = gc.playerRight.pos.y;
gc.ballsArr.forEach((ball) => {
snapshot.ballsArr.push({
x: ball.pos.x,
y: ball.pos.y,
dirX: ball.dir.x,
dirY: ball.dir.y,
speed: ball.speed
});
});
if (this.matchOptions & en.MatchOptions.movingWalls) {
snapshot.wallTop.y = gc.wallTop.pos.y;
snapshot.wallBottom.y = gc.wallBottom.pos.y;
}
return (snapshot);
}
private _newRound(s: GameSession, ball: Ball) {
if (s._checkDisconnexions()) {
return;
}
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
const gc = s.components;
const minScore = 11;// can be changed for testing
if (gc.scoreLeft >= minScore || gc.scoreRight >= minScore)
{
if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2)
{
if (gc.scoreLeft > gc.scoreRight) {
s._matchEnd(en.PlayerSide.left);
}
else {
s._matchEnd(en.PlayerSide.right);
}
return;
}
}
ball.pos.x = c.w_mid;
ball.pos.y = random(c.h*0.3, c.h*0.7);
ball.speed = ball.baseSpeed;
ball.ballInPlay = true;
}
private _checkDisconnexions() {
if (this.playersMap.size !== 2)
{
this.matchEnded = true;
if (this.playersMap.size != 0)
{
console.log("Forfeit Ending");
const gc = this.components;
const luckyWinner: ClientPlayer = this.playersMap.values().next().value;
if (luckyWinner.racket === gc.playerLeft) {
this._matchEnd(en.PlayerSide.left, true);
}
else {
this._matchEnd(en.PlayerSide.right, true);
}
}
return true;
}
return false;
}
private async _matchEnd(winner: en.PlayerSide, forfeit_flag: boolean = false)
{
this.matchEnded = true;
let eventEnd: ev.EventMatchEnd;
eventEnd = new ev.EventMatchEnd(winner, forfeit_flag);
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
const gc = this.components;
await fetch(c.addressBackEnd + "/game/gameserver/updategame",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
gameServerIdOfTheMatch: this.id,
playerOneUsernameResult: gc.scoreLeft,
playerTwoUsernameResult: gc.scoreRight,
})
});
const gameSession = this;
setTimeout(function kickRemainingClients() {
gameSession.spectatorsMap.forEach((client) => {
clientTerminate(client);
});
gameSession.playersMap.forEach((client) => {
clientTerminate(client);
});
}, 15000);
// logs
if (winner === en.PlayerSide.left) {
console.log("Player Left WIN");
}
else {
console.log("Player Right WIN");
}
}
}

View File

@@ -0,0 +1,12 @@
export * from "../shared_js/constants.js"
// 15ms == 1000/66.666
export const serverGameLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
// 33.333ms == 1000/30
export const playersUpdateIntervalMS = 1000/30; // millisecond
export const spectatorsUpdateIntervalMS = 1000/30; // millisecond
export const addressBackEnd = "http://backend_dev:3000/api/v2";

View File

@@ -0,0 +1,6 @@
export * from "../shared_js/utils.js"
export function shortId(id: string): string {
return id.substring(0, id.indexOf("-"));
}

View File

@@ -0,0 +1,393 @@
import { WebSocketServer, WebSocket as BaseLibWebSocket } from "ws";
export class WebSocket extends BaseLibWebSocket {
id?: string;
}
import { IncomingMessage } from "http";
import { v4 as uuidv4 } from 'uuid';
import * as en from "../shared_js/enums.js"
import * as ev from "../shared_js/class/Event.js"
import * as c from "./constants.js"
import { Client, ClientPlayer, ClientSpectator } from "./class/Client.js"
import { GameSession } from "./class/GameSession.js"
import { shortId } from "./utils.js";
import { gameSessionIdPLACEHOLDER } from "./constants.js";
const wsPort = 8042;
export const wsServer = new WebSocketServer<WebSocket>({host: "0.0.0.0", port: wsPort, path: "/pong"});
const clientsMap: Map<string, Client> = new Map; // socket.id/Client
const matchmakingMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const privateMatchmakingMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession
wsServer.on("connection", connectionListener);
wsServer.on("error", errorListener);
wsServer.on("close", closeListener);
function connectionListener(socket: WebSocket, request: IncomingMessage)
{
const id = uuidv4();
const client = new Client(socket, id);
clientsMap.set(id, client);
socket.id = id;
socket.on("pong", function heartbeat() {
client.isAlive = true;
console.log(`client ${shortId(client.id)} is alive`);
});
socket.on("message", function log(data: string) {
try {
const event: ev.ClientEvent = JSON.parse(data);
if (event.type === en.EventTypes.clientInput) {
return;
}
}
catch (e) {}
console.log("data: " + data);
});
socket.once("message", clientAnnounceListener);
}
async function clientAnnounceListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientAnnounce = JSON.parse(data);
if (msg.type === en.EventTypes.clientAnnounce)
{
// TODO: reconnection with msg.clientId ?
// "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that)
if (msg.role === en.ClientRole.player)
{
const announce: ev.ClientAnnouncePlayer = <ev.ClientAnnouncePlayer>msg;
// WIP nest, fetch token validation
const body = {
playerOneUsername: announce.username,
playerTwoUsername: "",
gameOptions: announce.matchOptions,
isGameIsWithInvitation: announce.privateMatch,
token: announce.token,
};
if (announce.privateMatch) {
body.playerTwoUsername = announce.playerTwoUsername;
}
const response = await fetch(c.addressBackEnd + "/game/gameserver/validate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body)
});
if (!response.ok)
{
this.send(JSON.stringify( new ev.EventError((await response.json()).message)));
clientTerminate(clientsMap.get(this.id));
return;
}
const player = clientsMap.get(this.id) as ClientPlayer;
player.matchOptions = announce.matchOptions;
player.token = announce.token;
announce.isInvitedPerson ? player.username = announce.playerTwoUsername : player.username = announce.username;
this.send(JSON.stringify( new ev.EventAssignId(this.id) )); // unused
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
if (announce.privateMatch) {
privateMatchmaking(player);
}
else {
publicMatchmaking(player);
}
}
else if (msg.role === en.ClientRole.spectator)
{
const announce: ev.ClientAnnounceSpectator = <ev.ClientAnnounceSpectator>msg;
const gameSession = gameSessionsMap.get(announce.gameSessionId);
if (!gameSession) {
this.send(JSON.stringify( new ev.EventError("invalid gameSessionId")));
clientTerminate(clientsMap.get(this.id));
return;
}
const spectator = clientsMap.get(this.id) as ClientSpectator;
spectator.gameSession = gameSession;
gameSession.spectatorsMap.set(spectator.id, spectator);
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
}
}
else {
console.log("Invalid ClientAnnounce");
}
return;
}
catch (e) {
console.log("Invalid JSON (clientAnnounceListener)");
}
this.once("message", clientAnnounceListener);
}
function publicMatchmaking(player: ClientPlayer)
{
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
matchmakingMap.set(player.id, player);
const matchOptions = player.matchOptions;
const compatiblePlayers: ClientPlayer[] = [];
for (const [id, client] of matchmakingMap)
{
if (client.matchOptions === matchOptions)
{
compatiblePlayers.push(client);
if (compatiblePlayers.length === maxPlayersNumber) {
break;
}
}
}
if (compatiblePlayers.length >= minPlayersNumber) {
compatiblePlayers.forEach((client) => {
matchmakingMap.delete(client.id);
});
createGameSession(compatiblePlayers, matchOptions);
}
}
function privateMatchmaking(player: ClientPlayer)
{
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
privateMatchmakingMap.set(player.id, player);
const matchOptions = player.matchOptions;
const token = player.token;
const compatiblePlayers: ClientPlayer[] = [];
for (const [id, client] of privateMatchmakingMap)
{
if (client.token === token)
{
compatiblePlayers.push(client);
if (compatiblePlayers.length === maxPlayersNumber) {
break;
}
}
}
if (compatiblePlayers.length >= minPlayersNumber) {
compatiblePlayers.forEach((client) => {
privateMatchmakingMap.delete(client.id);
});
createGameSession(compatiblePlayers, matchOptions);
}
else
{
setTimeout(async function abortMatch() {
if (!player.gameSession)
{
if (player.socket.OPEN) {
player.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
}
const response = await fetch(c.addressBackEnd + "/game/gameserver/destroysession",{
method: "POST",
headers : {"Content-Type": "application/json"},
body : JSON.stringify({
token : player.token
})
})
.then(x => x.json())
.catch(error => console.log("ERROR : " + error));
clientTerminate(player);
}
}, 60000);
}
}
function createGameSession(playersArr: ClientPlayer[], matchOptions: en.MatchOptions)
{
// const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR
const id = uuidv4();
const gameSession = new GameSession(id, matchOptions);
gameSessionsMap.set(id, gameSession);
playersArr.forEach((client) => {
client.gameSession = gameSession;
gameSession.playersMap.set(client.id, client);
gameSession.unreadyPlayersMap.set(client.id, client);
client.socket.once("message", playerReadyConfirmationListener);
});
// REFACTORING: Not pretty, hardcoded two players.
// Could be done in gameSession maybe ?
const gameSessionPlayersIterator = gameSession.playersMap.values();
let player: ClientPlayer;
player = (<ClientPlayer>gameSessionPlayersIterator.next().value);
player.racket = gameSession.components.playerLeft;
player.socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) ));
player = (<ClientPlayer>gameSessionPlayersIterator.next().value);
player.racket = gameSession.components.playerRight;
player.socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) ));
// REFACTORING
setTimeout(function abortMatch() {
if (gameSession.unreadyPlayersMap.size !== 0)
{
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
client.gameSession = null;
clientTerminate(client);
});
}
}, 5000);
}
async function playerReadyConfirmationListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientEvent = JSON.parse(data);
if (msg.type === en.EventTypes.clientPlayerReady)
{
const client = clientsMap.get(this.id);
const gameSession = client.gameSession;
gameSession.unreadyPlayersMap.delete(this.id);
if (gameSession.unreadyPlayersMap.size === 0)
{
// WIP nest , send gameSession.id
const gameSessionPlayersIterator = gameSession.playersMap.values();
const body = {
gameServerIdOfTheMatch : gameSession.id,
gameOptions: gameSession.matchOptions,
playerOneUsername: (<ClientPlayer>gameSessionPlayersIterator.next().value).username,
playerTwoUsername: (<ClientPlayer>gameSessionPlayersIterator.next().value).username,
playerOneUsernameResult : 0,
playerTwoUsernameResult : 0
};
const response = await fetch(c.addressBackEnd + "/game/gameserver/creategame",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body)
});
if (!response.ok)
{
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
client.gameSession = null;
clientTerminate(client);
});
return;
}
gameSession.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
});
gameSession.start();
}
}
else {
console.log("Invalid playerReadyConfirmation");
}
return;
}
catch (e) {
console.log("Invalid JSON (playerReadyConfirmationListener)");
}
this.once("message", playerReadyConfirmationListener);
}
export function clientInputListener(this: WebSocket, data: string)
{
try {
// const input: ev.ClientEvent = JSON.parse(data);
const input: ev.EventInput = JSON.parse(data);
if (input.type === en.EventTypes.clientInput)
{
const client = clientsMap.get(this.id) as ClientPlayer;
client.inputBuffer = input;
client.gameSession.instantInputDebug(client); // wip
}
else {
console.log("Invalid clientInput");
}
}
catch (e) {
console.log("Invalid JSON (clientInputListener)");
}
}
////////////
////////////
const pingInterval = setInterval( () => {
let deleteLog = "";
clientsMap.forEach( (client) => {
if (!client.isAlive) {
clientTerminate(client);
deleteLog += ` ${shortId(client.id)} |`;
}
else {
client.isAlive = false;
client.socket.ping();
}
});
if (deleteLog) {
console.log(`Disconnected:${deleteLog}`);
}
console.log("gameSessionMap size: " + gameSessionsMap.size);
console.log("clientsMap size: " + clientsMap.size);
console.log("matchmakingMap size: " + matchmakingMap.size);
console.log("privateMatchmakingMap size: " + privateMatchmakingMap.size);
console.log("");
}, 4200);
export function clientTerminate(client: Client)
{
client.socket.terminate();
if (client.gameSession)
{
client.gameSession.playersMap.delete(client.id);
if (client.gameSession.playersMap.size === 0)
{
clearInterval(client.gameSession.playersUpdateInterval);
clearInterval(client.gameSession.spectatorsUpdateInterval);
clearInterval(client.gameSession.gameLoopInterval);
gameSessionsMap.delete(client.gameSession.id);
}
}
clientsMap.delete(client.id);
if (matchmakingMap.has(client.id)) {
matchmakingMap.delete(client.id);
}
else if (privateMatchmakingMap.has(client.id)) {
privateMatchmakingMap.delete(client.id);
}
}
function closeListener()
{
clearInterval(pingInterval);
}
function errorListener(error: Error)
{
console.log("Error: " + JSON.stringify(error));
}

View File

@@ -0,0 +1,144 @@
import * as en from "../enums.js"
/* From Server */
export class ServerEvent {
type: en.EventTypes;
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
export class EventAssignId extends ServerEvent {
id: string;
constructor(id: string) {
super(en.EventTypes.assignId);
this.id = id;
}
}
export class EventMatchmakingComplete extends ServerEvent {
side: en.PlayerSide;
constructor(side: en.PlayerSide) {
super(en.EventTypes.matchmakingComplete);
this.side = side;
}
}
export class EventGameUpdate extends ServerEvent {
playerLeft = {
y: 0
};
playerRight = {
y: 0
};
ballsArr: {
x: number,
y: number,
dirX: number,
dirY: number,
speed: number
}[] = [];
wallTop? = {
y: 0
};
wallBottom? = {
y: 0
};
lastInputId = 0;
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
super(en.EventTypes.gameUpdate);
}
}
export class EventScoreUpdate extends ServerEvent {
scoreLeft: number;
scoreRight: number;
constructor(scoreLeft: number, scoreRight: number) {
super(en.EventTypes.scoreUpdate);
this.scoreLeft = scoreLeft;
this.scoreRight = scoreRight;
}
}
export class EventMatchEnd extends ServerEvent {
winner: en.PlayerSide;
forfeit: boolean;
constructor(winner: en.PlayerSide, forfeit = false) {
super(en.EventTypes.matchEnd);
this.winner = winner;
this.forfeit = forfeit;
}
}
export class EventMatchAbort extends ServerEvent {
constructor() {
super(en.EventTypes.matchAbort);
}
}
export class EventError extends ServerEvent {
message: string;
constructor(message: string) {
super(en.EventTypes.error);
this.message = message;
}
}
/* From Client */
export class ClientEvent {
type: en.EventTypes; // readonly ?
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
export class ClientAnnounce extends ClientEvent {
role: en.ClientRole;
constructor(role: en.ClientRole) {
super(en.EventTypes.clientAnnounce);
this.role = role;
}
}
export class ClientAnnouncePlayer extends ClientAnnounce {
clientId: string; // unused
matchOptions: en.MatchOptions;
token: string;
username: string;
privateMatch: boolean;
playerTwoUsername?: string;
isInvitedPerson? : boolean;
constructor(matchOptions: en.MatchOptions, token: string, username: string, privateMatch: boolean = false, playerTwoUsername?: string, isInvitedPerson? : boolean) {
super(en.ClientRole.player);
this.matchOptions = matchOptions;
this.token = token;
this.username = username;
this.privateMatch = privateMatch;
if (isInvitedPerson) {
this.isInvitedPerson = isInvitedPerson;
}
if (playerTwoUsername) {
this.playerTwoUsername = playerTwoUsername;
}
}
}
export class ClientAnnounceSpectator extends ClientAnnounce {
gameSessionId: string;
constructor(gameSessionId: string) {
super(en.ClientRole.spectator);
this.gameSessionId = gameSessionId;
}
}
export class EventInput extends ClientEvent {
input: en.InputEnum;
id: number;
constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) {
super(en.EventTypes.clientInput);
this.input = input;
this.id = id;
}
}

View File

@@ -5,7 +5,7 @@ import { VectorInteger } from "./Vector.js";
import { Rectangle, MovingRectangle, Racket, Ball } from "./Rectangle.js";
import { random } from "../utils.js";
class GameComponents {
export class GameComponents {
wallTop: Rectangle | MovingRectangle;
wallBottom: Rectangle | MovingRectangle;
playerLeft: Racket;
@@ -61,5 +61,3 @@ class GameComponents {
}
}
}
export {GameComponents}

View File

@@ -1,9 +1,9 @@
import { Vector, VectorInteger } from "./Vector.js";
import { Component, Moving } from "./interface.js";
import type { Component, Moving } from "./interface.js";
import * as c from "../constants.js"
class Rectangle implements Component {
export class Rectangle implements Component {
pos: VectorInteger;
width: number;
height: number;
@@ -33,7 +33,7 @@ class Rectangle implements Component {
}
}
class MovingRectangle extends Rectangle implements Moving {
export class MovingRectangle extends Rectangle implements Moving {
dir: Vector = new Vector(0,0);
speed: number;
readonly baseSpeed: number;
@@ -61,7 +61,7 @@ class MovingRectangle extends Rectangle implements Moving {
}
}
class Racket extends MovingRectangle {
export class Racket extends MovingRectangle {
constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(pos, width, height, baseSpeed);
}
@@ -72,13 +72,22 @@ class Racket extends MovingRectangle {
}
}
class Ball extends MovingRectangle {
export class Ball extends MovingRectangle {
readonly speedIncrease: number;
ballInPlay: boolean = false;
constructor(pos: VectorInteger, size: number, baseSpeed: number, speedIncrease: number) {
super(pos, size, size, baseSpeed);
this.speedIncrease = speedIncrease;
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
bounce(collider?: Rectangle) {
this._bounceAlgo(collider);
}
@@ -92,15 +101,6 @@ class Ball extends MovingRectangle {
this._bounceWall();
}
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
protected _bounceWall() { // Should be enough for Wall
this.dir.y = this.dir.y * -1;
}
@@ -140,5 +140,3 @@ class Ball extends MovingRectangle {
// console.log(`x: ${this.dir.x}, y: ${this.dir.y}`);
}
}
export {Rectangle, MovingRectangle, Racket, Ball}

View File

@@ -1,5 +1,5 @@
class Vector {
export class Vector {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
@@ -16,13 +16,13 @@ class Vector {
}
}
class VectorInteger extends Vector {
export class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
class VectorInteger {
export class VectorInteger {
// private _x: number = 0;
// private _y: number = 0;
// constructor(x: number = 0, y: number = 0) {
@@ -45,5 +45,3 @@ class VectorInteger {
// }
}
*/
export {Vector, VectorInteger}

View File

@@ -1,21 +1,19 @@
import { Vector, VectorInteger } from "./Vector.js";
import type { Vector, VectorInteger } from "./Vector.js";
interface Component {
export interface Component {
pos: VectorInteger;
}
interface GraphicComponent extends Component {
export interface GraphicComponent extends Component {
ctx: CanvasRenderingContext2D;
color: string;
update: () => void;
clear: (pos?: VectorInteger) => void;
}
interface Moving {
export interface Moving {
dir: Vector;
speed: number; // pixel per second
move(delta: number): void;
}
export {Component, GraphicComponent, Moving}

View File

@@ -11,8 +11,8 @@ export const pw = Math.floor(w*0.017);
export const ph = pw*6;
export const ballSize = pw;
export const wallSize = Math.floor(w*0.01);
export const racketSpeed = Math.floor(w*0.66); // pixel per second
export const ballSpeed = Math.floor(w*0.66); // pixel per second
export const racketSpeed = Math.floor(w*0.60); // pixel per second
export const ballSpeed = Math.floor(w*0.55); // pixel per second
export const ballSpeedIncrease = Math.floor(ballSpeed*0.05); // pixel per second
export const normalizedSpeed = false; // for consistency in speed independent of direction
@@ -24,3 +24,7 @@ export const newRoundDelay = 1500; // millisecond
export const multiBallsCount = 3;
export const movingWallPosMax = Math.floor(w*0.12);
export const movingWallSpeed = Math.floor(w*0.08);
export const gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER
// for testing, force gameSession.id in wsServer.ts->matchmaking()

View File

@@ -1,15 +1,17 @@
enum EventTypes {
export enum EventTypes {
// Class Implemented
gameUpdate = 1,
scoreUpdate,
matchEnd,
assignId,
matchmakingComplete,
error,
// Generic
matchmakingInProgress,
matchStart,
matchAbort,
matchNewRound, // unused
matchPause, // unused
matchResume, // unused
@@ -21,27 +23,25 @@ enum EventTypes {
}
enum InputEnum {
export enum InputEnum {
noInput = 0,
up = 1,
down,
}
enum PlayerSide {
export enum PlayerSide {
left = 1,
right
}
enum ClientRole {
export enum ClientRole {
player = 1,
spectator
}
enum MatchOptions {
export enum MatchOptions {
// binary flags, can be mixed
noOption = 0b0,
multiBalls = 1 << 0,
movingWalls = 1 << 1
}
export {EventTypes, InputEnum, PlayerSide, ClientRole, MatchOptions}

View File

@@ -0,0 +1,25 @@
import type { MovingRectangle } from "./class/Rectangle.js";
export function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
export function sleep (ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function clamp(n: number, min: number, max: number) : number
{
if (n < min)
n = min;
else if (n > max)
n = max;
return (n);
}
// Typescript hack, unused
export function assertMovingRectangle(value: unknown): asserts value is MovingRectangle {
// if (value !== MovingRectangle) throw new Error("Not a MovingRectangle");
return;
}

View File

@@ -1,9 +1,9 @@
import * as c from "./constants.js";
import { MovingRectangle } from "../shared_js/class/Rectangle.js";
import { GameComponents } from "./class/GameComponents.js";
import type { MovingRectangle } from "../shared_js/class/Rectangle.js";
import type { GameComponents } from "./class/GameComponents.js";
function wallsMovements(delta: number, gc: GameComponents)
export function wallsMovements(delta: number, gc: GameComponents)
{
const wallTop = <MovingRectangle>gc.wallTop;
const wallBottom = <MovingRectangle>gc.wallBottom;
@@ -16,5 +16,3 @@ function wallsMovements(delta: number, gc: GameComponents)
wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
}
export {wallsMovements}

View File

@@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES6", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -7,7 +7,7 @@ POSTGRES_DATABASE=transcendance_db
# OAUTH2 42 API
FORTYTWO_CLIENT_ID=u-s4t2ud-49dc7b539bcfe1acb48b928b2b281671c99fc5bfab1faca57a536ab7e0075500
FORTYTWO_CLIENT_SECRET=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
FORTYTWO_CLIENT_SECRET=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
FORTYTWO_CALLBACK_URL=http://transcendance:8080/api/v2/auth/redirect
COOKIE_SECRET=248cdc831110eec8796d7c1edbf79835
# JWT
@@ -20,3 +20,6 @@ REDIS_PORT=6379
REDIS_PASSWORD=1a5e04138b91b3d683c708e4689454c2
#2fa
TWO_FACTOR_AUTHENTICATION_APP_NAME=Transcendance
NAME_OF_REDIS_DB = tokenGameDatabase
TICKET_FOR_PLAYING_GAME_SECRET = a2aef785c388497f5fca18f9ccff37ed

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,14 @@ import { ConfigModule } from '@nestjs/config';
import { FriendshipsModule } from './friendship/friendships.module';
import { AuthenticationModule } from './auth/42/authentication.module';
import { PassportModule } from '@nestjs/passport';
// import { GameModule } from './game/game/game.module';
import { GameModule } from './game/game.module';
@Module({
imports: [UsersModule,
AuthenticationModule,
PassportModule.register({ session: true }),
FriendshipsModule,
GameModule,
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',
@@ -27,7 +28,6 @@ import { PassportModule } from '@nestjs/passport';
//avec une classe pour le module
synchronize: true,
}),
// GameModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -18,7 +18,7 @@ import { CreateUsersDto } from "src/users/dto/create-users.dto";
async validate(accessToken: string, refreshToken: string, profile: Profile, callbackURL: string) {
console.log("Validate inside strategy.ts");
console.log(profile.id, profile.username, profile.phoneNumbers[0].value, profile.emails[0].value, profile.photos[0].value);
const userDTO: CreateUsersDto = { fortyTwoId: profile.id, username: profile.username, email: profile.emails[0].value, image_url: 'default.png', isEnabledTwoFactorAuth: false , status: "connected" };
const userDTO: CreateUsersDto = { fortyTwoId: profile.id, username: profile.username, email: profile.emails[0].value, image_url: 'default.png', isEnabledTwoFactorAuth: false , status: "Connected" };
const user = await this.authenticationService.validateUser(userDTO);
if (!user)
throw new UnauthorizedException();

View File

@@ -1,16 +0,0 @@
import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class CreateUsersDto {
@IsString()
@IsNotEmpty()
readonly username: string;
readonly fortyTwoId: string;
@IsEmail()
readonly email: string;
@IsString()
readonly image_url: string;
@IsString()
readonly status: string;
@IsBoolean()
readonly isEnabledTwoFactorAuth: boolean;
}

View File

@@ -0,0 +1,19 @@
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from "class-validator";
export class CreateGameDto {
@IsString()
@IsNotEmpty()
gameServerIdOfTheMatch : string
@IsNumber()
@IsNotEmpty()
gameOptions: number
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
playerTwoUsernameResult : number
@IsNumber()
playerOneUsernameResult : number
}

View File

@@ -0,0 +1,14 @@
import { IsBoolean, IsEmpty, IsInt, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { IsNull } from "typeorm";
export class GrantTicketDto {
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
gameOptions : number
@IsBoolean()
isGameIsWithInvitation : boolean
}

View File

@@ -0,0 +1,5 @@
import { OmitType } from "@nestjs/mapped-types";
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { CreateGameDto } from "./createGame.dto";
export class UpdateGameDto extends OmitType(CreateGameDto, ['playerOneUsername', 'playerTwoUsername', 'gameOptions'] as const){}

View File

@@ -0,0 +1,16 @@
import { IsBase64, IsBoolean, IsEmpty, IsNotEmpty, IsNumber, IsString } from "class-validator";
export class ValidateTicketDto {
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
gameOptions : number
@IsBoolean()
isGameIsWithInvitation : boolean
@IsBase64()
@IsNotEmpty()
token : string
}

View File

@@ -0,0 +1,25 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('game')
export class Game {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOneUsername: string
@Column()
playerTwoUsername: string
@Column({default : 0, nullable : true})
playerOneUsernameResult : number
@Column({default : 0, nullable : true})
playerTwoUsernameResult : number
@Column({unique : true})
gameServerIdOfTheMatch: string
@Column({default: false, nullable : true}) //éric pourra trouver un meilleur mot : ongoing ?
isMatchIsFinished: boolean
}

View File

@@ -0,0 +1,22 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('tokenGame')
export class TokenGame {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOneUsername : string
@Column({nullable: true})
playerTwoUsername : string
@Column()
gameOptions : number
@Column()
isGameIsWithInvitation : boolean
@Column({default: 0, nullable: true})
numberOfRegisteredUser : number
@Column({default : false})
isSecondUserAcceptedRequest : boolean
@Column()
token : string
}

View File

@@ -1,22 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('gameParty')
export class gameParty {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOne: string
@Column()
playerTwo: string
@Column()
resultOfTheMatch: string
@Column()
gameServerIdOfTheMatch: string
}

View File

@@ -1,4 +1,116 @@
import { Controller } from '@nestjs/common';
import { Body, Controller, Get, HttpException, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
import { Console } from 'console';
import { request } from 'http';
import { use } from 'passport';
import { AuthenticateGuard, TwoFactorGuard } from 'src/auth/42/guards/42guards';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';
import { CreateGameDto } from './dto/createGame.dto';
import { GrantTicketDto } from './dto/grantTicket.dto';
import { UpdateGameDto } from './dto/updateGame.dto';
import { ValidateTicketDto } from './dto/validateTicket.dto';
import { TokenGame } from './entity/tokenGame.entity';
import { GameService } from './game.service';
@Controller('game')
export class GameController {}
export class GameController {
constructor (private readonly gameService : GameService) { }
@Get('ranking')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async getRankingForAllUsers(@Req() req)
{
const currentUser : User = req.user
return this.gameService.getRankingForAllUsers(currentUser);
}
@Post('ticket')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async grantTicket(@Req() req, @Body() grantTicketDto : GrantTicketDto)
{
const user : User = req.user
if (grantTicketDto.playerOneUsername != user.username)
return new HttpException('You can\'t request a game for another person.', 403 )
// else if (user.status !== "connected")
// return new HttpException('You must not be in game...', HttpStatus.FORBIDDEN )
return this.gameService.generateToken(user, grantTicketDto);
}
@Post('requested')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async requestIfAnotherUserHasRespondToquestForGame(@Req() req, @Body('token') token)
{
const user : User = req.user;
return this.gameService.requestIfAnotherUserHasRespondToquestForGame(user, token);
}
@Post('decline')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async declineInvitation(@Body('token') token, @Req() req)
{
const user : User = req.user;
return this.gameService.declineInvitation(user, token);
}
@Post('accept')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async acceptInvitation(@Body('token') token, @Req() req)
{
const user : User = req.user;
return this.gameService.acceptInvitation(user, token);
}
@Get('invitations')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async findInvitations(@Req() request)
{
const user : User = request.user;
return this.gameService.findInvitations(user);
}
//
//N'est valable que pour le game-serveur.
@Post('gameserver/validate')
async validateTicket(@Body() validateTicketDto : ValidateTicketDto, @Req() request)
{
if (await this.gameService.validateToken(validateTicketDto) === false)
return new HttpException("The token is not valid", HttpStatus.NOT_FOUND);
console.log("200 retourné côté nest")
return HttpStatus.OK;
}
@Post('gameserver/creategame')
async createGame(@Body() creategameDto : CreateGameDto)
{
console.log("On est dans create game")
console.log(creategameDto)
return this.gameService.createGame(creategameDto);
}
@Post('gameserver/updategame')
async updateGame(@Body() updateGameDto : UpdateGameDto)
{
console.log("On est dans update game")
console.log(updateGameDto)
return this.gameService.updateGame(updateGameDto);
}
@Post('gameserver/destroysession')
async destroySession(@Body('token') token)
{
return this.gameService.destroySession(token);
}
}

View File

@@ -1,9 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Friendship } from 'src/friendship/entities/friendship.entity';
import { FriendshipService } from 'src/friendship/friendship.service';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';
import { Game } from './entity/game.entity';
import { TokenGame } from './entity/tokenGame.entity';
import { GameController } from './game.controller';
import { GameService } from './game.service';
@Module({
controllers: [GameController],
providers: [GameService]
imports: [TypeOrmModule.forFeature([TokenGame, User, Game, Friendship])],
controllers: [GameController],
providers: [GameService, UsersService, FriendshipService]
})
export class GameModule {}

View File

@@ -1,18 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameService } from './game.service';
// import { Test, TestingModule } from '@nestjs/testing';
// // import { GameService } from './game.service';
describe('GameService', () => {
let service: GameService;
// describe('GameService', () => {
// let service: GameService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GameService],
}).compile();
// beforeEach(async () => {
// const module: TestingModule = await Test.createTestingModule({
// providers: [GameService],
// }).compile();
service = module.get<GameService>(GameService);
});
// service = module.get<GameService>(GameService);
// });
it('should be defined', () => {
expect(service).toBeDefined();
});
});
// it('should be defined', () => {
// expect(service).toBeDefined();
// });
// });

View File

@@ -1,4 +1,277 @@
import { Injectable } from '@nestjs/common';
import { ConsoleLogger, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createCipheriv, randomBytes, scrypt } from 'crypto';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import { promisify } from 'util';
import { GrantTicketDto } from './dto/grantTicket.dto';
import { Game } from './entity/game.entity';
import { ValidateTicketDto } from './dto/validateTicket.dto';
import { TokenGame } from './entity/tokenGame.entity';
import { UsersService } from 'src/users/users.service';
import { CreateGameDto } from './dto/createGame.dto';
import { UpdateGameDto } from './dto/updateGame.dto';
import { FriendshipService } from 'src/friendship/friendship.service';
@Injectable()
export class GameService {}
export class GameService {
constructor (
@InjectRepository(Game)
private readonly gameRepository : Repository<Game>,
@InjectRepository(User)
private readonly userRepository : Repository<User>,
@InjectRepository(TokenGame)
private readonly tokenGameRepository : Repository<TokenGame>,
private readonly userService : UsersService,
private readonly friendShipService : FriendshipService
) { }
async getRankingForAllUsers(currentUser : User) {
const users = await this.userRepository.createQueryBuilder("user")
.leftJoinAndSelect("user.stats", "stats")
.orderBy('stats.winGame', "DESC")
.getMany();
const partialUser : Partial<User>[] = []
for (const user of users)
{
if (await this.friendShipService.findIfUserIsBlockedOrHasBlocked(currentUser.id.toString(), user.id.toString()) === false)
partialUser.push({username : user.username, stats : user.stats })
}
console.log(...partialUser)
return partialUser;
}
async encryptToken(toEncrypt : string) : Promise<string> {
const iv = randomBytes(16);
const password = process.env.TICKET_FOR_PLAYING_GAME_SECRET + new Date();
const key = (await promisify(scrypt)(password, 'salt', 32)) as Buffer;
const cipher = createCipheriv('aes-256-ctr', key, iv);
const encryptedText = Buffer.concat([
cipher.update(toEncrypt),
cipher.final(),
]);
const encryptedTextToReturn = encryptedText.toString('base64');
return encryptedTextToReturn
}
async generateToken(user : User, grantTicketDto : GrantTicketDto)
{
console.log(user.status);
if (user.status === "In Game" || user.status === "In Pool")
return new HttpException("You can't play two games", HttpStatus.FORBIDDEN);
if (grantTicketDto.isGameIsWithInvitation === true)
{
const secondUser : Partial<User> = await this.userService.findOneByUsername(user.id.toString(), grantTicketDto.playerTwoUsername)
if (!secondUser || secondUser.username === user.username)
return new HttpException("The requested second player does not exist OR you want to play against yourself. :P", HttpStatus.NOT_FOUND);
const encryptedTextToReturn = await this.encryptToken(user.username + '_' + secondUser.username + '_'
+ grantTicketDto.gameOptions + '_' + grantTicketDto.isGameIsWithInvitation + '_' + new Date())
const tok = this.tokenGameRepository.create(grantTicketDto);
tok.isSecondUserAcceptedRequest = false;
tok.numberOfRegisteredUser = 0;
tok.token = encryptedTextToReturn;
this.tokenGameRepository.save(tok);
this.userService.updateStatus(user.id, "In Pool")
return { token : encryptedTextToReturn };
}
else if (grantTicketDto.isGameIsWithInvitation === false) {
const encryptedTextToReturn = await this.encryptToken(user.username + '_'
+ grantTicketDto.gameOptions + '_' + grantTicketDto.isGameIsWithInvitation + '_' + new Date())
const tok = this.tokenGameRepository.create(grantTicketDto);
tok.numberOfRegisteredUser = 0;
tok.token = encryptedTextToReturn;
this.tokenGameRepository.save(tok);
this.userService.updateStatus(user.id, "In Pool")
return { token : encryptedTextToReturn };
}
return new HttpException("Something went wrong !", HttpStatus.INTERNAL_SERVER_ERROR)
}
async validateToken(validateTicketDto : ValidateTicketDto) {
if (validateTicketDto.isGameIsWithInvitation === true)
{
const tokenGame : TokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : validateTicketDto.playerTwoUsername})
.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: true})
.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : true})
.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
.getOne();
if (tokenGame)
{
tokenGame.numberOfRegisteredUser++;
if (tokenGame.numberOfRegisteredUser === 2)
{
this.tokenGameRepository.remove(tokenGame)
const userOne : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerOneUsername})
.getOne();
this.userService.updateStatus(userOne.id, "In Game")
const userTwo : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerTwoUsername})
.getOne();
this.userService.updateStatus(userTwo.id, "In Game")
}
return true;
}
}
else if (validateTicketDto.isGameIsWithInvitation === false)
{
const tokenGame : TokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: false})
.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
.getOne();
if (tokenGame)
{
this.tokenGameRepository.remove(tokenGame)
console.log("USERNAME : " + tokenGame.playerOneUsername)
const user : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerOneUsername})
.getOne();
this.userService.updateStatus(user.id, "In Game")
return true;
}
}
return false;
}
async findInvitations(user : User) {
const game = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokengame.isGameIsWithInvitation = :invit', {invit : true})
.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : false})
.getMany();
if (!game)
return new HttpException( "No invitations !", HttpStatus.NOT_FOUND);
let partialGame : Partial<TokenGame>[] = [];
for (const gameToken of game) {
partialGame.push({
playerOneUsername : gameToken.playerOneUsername,
playerTwoUsername : gameToken.playerTwoUsername,
gameOptions : gameToken.gameOptions,
token : gameToken.token,
});
}
return partialGame;
}
async declineInvitation(user : User, token : string)
{
if (user.status !== "Connected")
return new HttpException("You must finish your game before decline.", HttpStatus.FORBIDDEN)
console.log("On décline l'invitation")
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokengame.token = :token', {token : token})
.getOne();
if (tokenGame)
return this.tokenGameRepository.remove(tokenGame);
return new HttpException("Invitation not found !", HttpStatus.NOT_FOUND)
}
async destroySession(token : string)
{
console.log("On détruit le token et la session qui va avec")
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.token = :token', {token : token})
.getOne();
if (tokenGame)
{
const playerOne = await this.userRepository.findOneBy({username : tokenGame.playerOneUsername})
const playerTwo = await this.userRepository.findOneBy({username : tokenGame.playerTwoUsername})
if (playerOne.status !== "Disconnected")
this.userService.updateStatus(playerOne.id, "Connected")
if (playerTwo.status !== "Disconnected")
this.userService.updateStatus(playerTwo.id, "Connected")
return this.tokenGameRepository.remove(tokenGame);
}
return new HttpException("Token not found !", HttpStatus.NOT_FOUND)
}
async acceptInvitation(user : User, token : string)
{
if (user.status !== "Connected")
return new HttpException("You must finish your game before accept.", HttpStatus.FORBIDDEN)
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokenGame')
.andWhere('tokenGame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokenGame.token = :token', {token : token})
.getOne();
if (tokenGame)
{
tokenGame.isSecondUserAcceptedRequest = true;
this.tokenGameRepository.save(tokenGame)
return HttpStatus.OK
}
return new HttpException("Invitation not found !", HttpStatus.NOT_FOUND)
}
async requestIfAnotherUserHasRespondToquestForGame(user : User, token : string) {
if (user.status !== "Connected")
return new HttpException("You can't do that.", HttpStatus.BAD_REQUEST)
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokenGame')
.where('tokenGame.token = :token', {token : token})
.andWhere('tokenGame.isSecondUserAcceptedRequest = :isSecondUserAcceptedRequest', {isSecondUserAcceptedRequest : true})
.getOne();
if (tokenGame && tokenGame.isSecondUserAcceptedRequest === true)
return {isSecondUserAcceptedRequest : true}
else if (tokenGame && tokenGame.isSecondUserAcceptedRequest === false)
return {isSecondUserAcceptedRequest : false}
else if (!tokenGame)
return new HttpException("Not Found", HttpStatus.NOT_FOUND)
}
async createGame(creategameDto : CreateGameDto)
{
if (creategameDto.playerOneUsername === "" || creategameDto.playerTwoUsername === ""
|| creategameDto.playerOneUsername === creategameDto.playerTwoUsername)
return HttpStatus.INTERNAL_SERVER_ERROR
const game = this.gameRepository.create(creategameDto)
game.isMatchIsFinished = false;
this.gameRepository.save(game);
if (!game)
return HttpStatus.INTERNAL_SERVER_ERROR
console.log("200 retourné pour la création de partie")
return HttpStatus.OK
}
async updateGame(updateGameDto : UpdateGameDto) {
console.log("Updata game" + updateGameDto)
const game = await this.gameRepository.createQueryBuilder('game')
.where("game.gameServerIdOfTheMatch = :gameServerIdOfTheMatch", {gameServerIdOfTheMatch : updateGameDto.gameServerIdOfTheMatch})
.getOne();
if (!game)
throw new HttpException(`The game could not be updated.`,HttpStatus.NOT_FOUND);
game.isMatchIsFinished = true;
game.playerOneUsernameResult = updateGameDto.playerOneUsernameResult
game.playerTwoUsernameResult = updateGameDto.playerTwoUsernameResult
this.userRepository.save(game);
const playerOne = await this.userRepository.findOneBy({username : game.playerOneUsername})
const playerTwo = await this.userRepository.findOneBy({username : game.playerTwoUsername})
if (!playerOne || !playerTwo)
return new HttpException("Internal Server Error. Impossible to update the database", HttpStatus.INTERNAL_SERVER_ERROR);
if (game.playerOneUsernameResult === game.playerTwoUsernameResult)
{
this.userService.incrementDraws(playerOne.id)
this.userService.incrementDraws(playerTwo.id)
}
else if (game.playerOneUsernameResult < game.playerTwoUsernameResult)
{
this.userService.incrementDefeats(playerOne.id)
this.userService.incrementVictories(playerTwo.id)
}
else
{
this.userService.incrementVictories(playerOne.id)
this.userService.incrementDefeats(playerTwo.id)
}
this.userService.updateStatus(playerOne.id, "Connected")
this.userService.updateStatus(playerTwo.id, "Connected")
return HttpStatus.OK
}
}

View File

@@ -2,7 +2,7 @@
// et de les mettre comme optionnelles. De plus on peut hériter
// des décorateurs de la classe parente (par exemple @IsString()).
import { OmitType, PartialType } from "@nestjs/mapped-types";
import { OmitType } from "@nestjs/mapped-types";
import { CreateUsersDto } from "./create-users.dto";
export class UpdateUsersDto extends OmitType(CreateUsersDto, ['fortyTwoId', 'email', 'image_url', 'status'] as const){}

View File

@@ -28,7 +28,7 @@ export class User {
@Column({ nullable: true })
phone: string;
@Column({ default: 'disconnected' })
@Column({ default: 'Disconnected' })
status: string;
// @Column()

View File

@@ -49,11 +49,9 @@ export class UsersController {
@Get()
findOne(@Query('username') username: string, @Req() req) {
if (username === undefined) {
console.log("Backend Getting current user");
return this.usersService.findOne(req.user.id);
} else {
const user : User = req.user;
console.log('we have a query: ' + username)
return this.usersService.findOneByUsername(user.id.toString(),username);
}
}
@@ -64,7 +62,6 @@ export class UsersController {
@Get('search')
findOneByUsername(@Query('username') username: string, @Req() req) {
const user : User = req.user;
console.log('searching for user' + user.username);
return this.usersService.findOneByUsername(user.id.toString(),username);
}
@@ -81,12 +78,9 @@ export class UsersController {
@UseGuards(TwoFactorGuard)
@Patch()
async update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto, @Res() response : Response) {
console.log("DANS PATCH USERS");
const user = await this.usersService.update(req.user.id, usersUpdateDto);
// const user : User = req.user;
if (user.isEnabledTwoFactorAuth === false && user.isTwoFactorAuthenticated === true)
this.usersService.setIsTwoFactorAuthenticatedWhenLogout(user.id);
console.log ("Enbale 2FA " + user.isEnabledTwoFactorAuth + " Is authenticated " + user.isTwoFactorAuthenticated);
if (user.isEnabledTwoFactorAuth === true && user.isTwoFactorAuthenticated === false)
{
response.status(201).send('2FA redirect')

View File

@@ -4,12 +4,9 @@ import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
import { CreateUsersDto } from './dto/create-users.dto';
import { UpdateUsersDto } from './dto/update-users.dto';
import { Friendship } from '../friendship/entities/friendship.entity';
import { PaginationQueryDto } from 'src/common/dto/pagination-query.dto';
import { UserStats } from './entities/userStat.entities';
import { FriendshipService } from 'src/friendship/friendship.service';
import { stringify } from 'querystring';
// On va devoir sûrement trouver un moyen plus simple pour passer l'id, sûrement via des pipes
// ou des interceptors, mais pour l'instant on va faire comme ça.
@Injectable()

View File

@@ -10,6 +10,19 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend_dev:3000;
}
location /api/v2/game/gameserver {
deny all;
}
location /pong {
proxy_pass http://game_server:8042/pong;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -0,0 +1,37 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,107 +0,0 @@
import * as en from "../enums.js"
/* From Server */
class ServerEvent {
type: en.EventTypes;
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
class EventAssignId extends ServerEvent {
id: string;
constructor(id: string) {
super(en.EventTypes.assignId);
this.id = id;
}
}
class EventMatchmakingComplete extends ServerEvent {
side: en.PlayerSide;
constructor(side: en.PlayerSide) {
super(en.EventTypes.matchmakingComplete);
this.side = side;
}
}
class EventGameUpdate extends ServerEvent {
playerLeft = {
y: 0
};
playerRight = {
y: 0
};
ballsArr: {
x: number,
y: number,
dirX: number,
dirY: number,
speed: number
}[] = [];
wallTop? = {
y: 0
};
wallBottom? = {
y: 0
};
lastInputId = 0;
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
super(en.EventTypes.gameUpdate);
}
}
class EventScoreUpdate extends ServerEvent {
scoreLeft: number;
scoreRight: number;
constructor(scoreLeft: number, scoreRight: number) {
super(en.EventTypes.scoreUpdate);
this.scoreLeft = scoreLeft;
this.scoreRight = scoreRight;
}
}
class EventMatchEnd extends ServerEvent {
winner: en.PlayerSide;
constructor(winner: en.PlayerSide) {
super(en.EventTypes.matchEnd);
this.winner = winner;
}
}
/* From Client */
class ClientEvent {
type: en.EventTypes; // readonly ?
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
class ClientAnnounce extends ClientEvent {
role: en.ClientRole;
clientId: string;
matchOptions: en.MatchOptions;
constructor(role: en.ClientRole, matchOptions: en.MatchOptions, clientId: string = "") {
super(en.EventTypes.clientAnnounce);
this.role = role;
this.clientId = clientId;
this.matchOptions = matchOptions;
}
}
class EventInput extends ClientEvent {
input: en.InputEnum;
id: number;
constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) {
super(en.EventTypes.clientInput);
this.input = input;
this.id = id;
}
}
export {
ServerEvent, EventAssignId, EventMatchmakingComplete,
EventGameUpdate, EventScoreUpdate, EventMatchEnd,
ClientEvent, ClientAnnounce, EventInput
}

View File

@@ -1,49 +0,0 @@
import * as c from "./constants.js";
import * as en from "../shared_js/enums.js"
import { gc, matchOptions, clientInfo } from "./global.js";
import { wallsMovements } from "../shared_js/wallsMovement.js";
let actual_time: number = Date.now();
let last_time: number;
let delta_time: number;
function gameLoop()
{
/* last_time = actual_time;
actual_time = Date.now();
delta_time = (actual_time - last_time) / 1000; */
delta_time = c.fixedDeltaTime;
// console.log(`delta_gameLoop: ${delta_time}`);
// interpolation
// console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`);
if (clientInfo.opponent.dir.y != 0 ) {
opponentInterpolation(delta_time);
}
// client prediction
gc.ballsArr.forEach((ball) => {
ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
});
if (matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(delta_time, gc);
}
}
function opponentInterpolation(delta: number)
{
// interpolation
clientInfo.opponent.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if ((clientInfo.opponent.dir.y > 0 && clientInfo.opponent.pos.y > clientInfo.opponentNextPos.y)
|| (clientInfo.opponent.dir.y < 0 && clientInfo.opponent.pos.y < clientInfo.opponentNextPos.y))
{
clientInfo.opponent.dir.y = 0;
clientInfo.opponent.pos.y = clientInfo.opponentNextPos.y;
}
}
export {gameLoop}

View File

@@ -1,3 +0,0 @@
export {pong, gc, matchOptions} from "./pong.js"
export {socket, clientInfo} from "./ws.js"

View File

@@ -1,99 +0,0 @@
initDom();
function initDom() {
document.getElementById("play_pong_button").addEventListener("click", init);
}
import * as c from "./constants.js"
import * as en from "../shared_js/enums.js"
import { GameArea } from "./class/GameArea.js";
import { GameComponentsClient } from "./class/GameComponentsClient.js";
import { handleInput } from "./handleInput.js";
// import { sendLoop } from "./handleInput.js";
import { gameLoop } from "./gameLoop.js"
import { drawLoop } from "./draw.js";
import { countdown } from "./utils.js";
import { initWebSocket } from "./ws.js";
import { initAudio } from "./audio.js";
/* Keys
Racket: W/S OR Up/Down
Grid On-Off: G
*/
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
export let pong: GameArea;
export let gc: GameComponentsClient;
export let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
function init()
{
console.log("multi_balls:"+(<HTMLInputElement>document.getElementById("multi_balls")).checked);
console.log("moving_walls:"+(<HTMLInputElement>document.getElementById("moving_walls")).checked);
console.log("sound_on:"+(<HTMLInputElement>document.getElementById("sound_on")).checked);
let soundMutedFlag = false;
if ( (<HTMLInputElement>document.getElementById("sound_off")).checked ) {
soundMutedFlag = true;
}
initAudio(soundMutedFlag);
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
matchOptions |= en.MatchOptions.multiBalls;
}
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
matchOptions |= en.MatchOptions.movingWalls;
}
document.getElementById("div_game_options").hidden = true;
pong = new GameArea();
gc = new GameComponentsClient(matchOptions, pong.ctx);
initWebSocket(matchOptions);
}
function matchmaking()
{
console.log("Searching an opponent...");
gc.text1.clear();
gc.text1.pos.assign(c.w/5, c.h_mid);
gc.text1.text = "Searching...";
gc.text1.update();
}
function matchmakingComplete()
{
console.log("Match Found !");
gc.text1.clear();
gc.text1.pos.assign(c.w/8, c.h_mid);
gc.text1.text = "Match Found !";
gc.text1.update();
}
function startGame() {
gc.text1.pos.assign(c.w_mid, c.h_mid+c.h/4);
countdown(c.matchStartDelay/1000, (count: number) => {
gc.text1.clear();
gc.text1.text = `${count}`;
gc.text1.update();
}, resumeGame);
}
function resumeGame()
{
gc.text1.text = "";
window.addEventListener('keydown', function (e) {
pong.addKey(e.key);
});
window.addEventListener('keyup', function (e) {
pong.deleteKey(e.key);
});
pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS);
// pong.handleInputInterval = window.setInterval(sendLoop, c.sendLoopIntervalMS);
pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS);
pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
}
export {matchmaking, matchmakingComplete, startGame}

View File

@@ -1,27 +0,0 @@
import { MovingRectangle } from "./class/Rectangle.js";
function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
function sleep (ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function clamp(n: number, min: number, max: number) : number
{
if (n < min)
n = min;
else if (n > max)
n = max;
return (n);
}
// Typescript hack, unused
function assertMovingRectangle(value: unknown): asserts value is MovingRectangle {
// if (value !== MovingRectangle) throw new Error("Not a MovingRectangle");
return;
}
export {random, sleep, clamp, assertMovingRectangle}

View File

@@ -4,9 +4,9 @@
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<title>Potato Pong</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='icon' type='image/x-icon' href='/favicon.ico'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>

View File

@@ -2,7 +2,7 @@
// routing
// may not need {link} here
import Router, { link, replace } from "svelte-spa-router";
import { primaryRoutes } from "./routes/primaryRoutes.js";
import { primaryRoutes } from "./routes/primaryRoutes.js";
// import primaryRoutes from "./routes/primaryRoutes.svelte";
const conditionsFailed = (event) => {

View File

@@ -0,0 +1,9 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
// name: 'world'
}
});
export default app;
//# sourceMappingURL=main.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAE/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC;IACnB,MAAM,EAAE,QAAQ,CAAC,IAAI;IACrB,KAAK,EAAE;IACN,gBAAgB;KAChB;CACD,CAAC,CAAC;AAEH,eAAe,GAAG,CAAC"}

View File

@@ -1,106 +0,0 @@
<script lang="ts">
// import { initDom } from "../game/client/pong.js";
import {onMount} from 'svelte';
onMount(() => {
// initDom();
})
</script>
<body>
<div id="div_game_options">
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls">
<label for="multi_balls">multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls">
<label for="moving_walls">moving walls</label>
</div>
<div>
<label>sound :</label>
<input type="radio" id="sound_on" name="sound_selector" checked>
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector">
<label for="sound_off">off</label>
</div>
<div>
<button id="play_pong_button">PLAY</button>
</div>
</fieldset>
</div>
<div id="div_game_instructions">
<h2>--- keys ---</h2>
<p>move up: 'w' or 'up arrow'</p>
<p>move down: 's' OR 'down arrow'</p>
<p>grid on/off: 'g'</p>
</div>
<div id="canvas_container">
<!-- <p> =) </p> -->
</div>
<!-- <script src="http://localhost:8080/js/pong.js" type="module" defer></script> -->
</body>
<style>
@font-face {
font-family: "Bit5x3";
src: url("/fonts/Bit5x3.woff2") format("woff2"),
url("/fonts/Bit5x3.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
body {
margin: 0;
background-color: #222425;
}
#canvas_container {
margin-top: 20px;
text-align: center;
/* border: dashed rgb(245, 245, 245) 5px; */
/* max-height: 80vh; */
/* overflow: hidden; */
}
#div_game_instructions {
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: large;
}
#div_game_options {
margin-top: 20px;
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#div_game_options fieldset {
max-width: 50vw;
width: auto;
margin: 0 auto;
}
#div_game_options fieldset div {
padding: 10px;
}
#play_pong_button {
font-family: "Bit5x3";
color: rgb(245, 245, 245);
background-color: #333333;
font-size: x-large;
padding: 10px;
}
canvas {
background-color: #333333;
max-width: 75vw;
/* max-height: 100vh; */
width: 80%;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import Header from '../pieces/Header.svelte';
import MatchListElem from "../pieces/MatchListElem.svelte";
let arr = [
{
id: "match-01",
playerOneUsername: "toto",
playerTwoUsername: "bruno",
},
{
id: "match-02",
playerOneUsername: "bertand",
playerTwoUsername: "cassandre",
},
{
id: "match-03",
playerOneUsername: "madeleine",
playerTwoUsername: "jack",
},
];
</script>
<!-- -->
<Header/>
<menu>
{#each arr as match}
<MatchListElem match={match}/>
{/each}
</menu>
<!-- -->
<style>
</style>

View File

@@ -1,88 +1,313 @@
<script>
import "public/game/pong.js"
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Header from '../../pieces/Header.svelte';
import { fade, fly } from 'svelte/transition';
import * as pong from "./client/pong";
// Pour Chérif: variables indiquant l'état du match
import { matchEnded, matchAbort } from "./client/ws";
//user's stuff
let user;
let allUsers;
//Game's stuff
let optionsAreNotSet = true;
const options = new pong.InitOptions();
//Game's stuff client side only
const gameAreaId = "game_area";
//html boolean for pages
let showWaitPage = false;
let showInvitations = false;
let showGameOption = true;
let showError = false;
let hiddenGame = true;
let isThereAnyInvitation = false;
let invitations = [];
let waitingMessage = "Please wait..."
let errorMessageWhenAttemptingToGetATicket = "";
onMount( async() => {
user = await fetch('http://transcendance:8080/api/v2/user')
.then( x => x.json() );
allUsers = await fetch('http://transcendance:8080/api/v2/user/all')
.then( x => x.json() );
options.playerOneUsername = user.username;
})
onDestroy( async() => {
pong.destroy();
})
const initGame = async() =>
{
optionsAreNotSet = false;
showWaitPage = true;
const matchOptions = pong.computeMatchOptions(options);
const responseWhenGrantToken = fetch("http://transcendance:8080/api/v2/game/ticket", {
method : "POST",
headers : {'Content-Type': 'application/json'},
body : JSON.stringify({
playerOneUsername : options.playerOneUsername,
playerTwoUsername : options.playerTwoUsername,
gameOptions : matchOptions,
isGameIsWithInvitation : options.isSomeoneIsInvited
})
})
const responseFromServer = await responseWhenGrantToken;
const responseInjson = await responseFromServer.json();
const token : string = responseInjson.token;
showWaitPage = false;
if (!responseFromServer.ok)
{
errorMessageWhenAttemptingToGetATicket = responseInjson.message;
showError = true;
setTimeout(() => {
showError = false;
showWaitPage = false
optionsAreNotSet = true
options.playerTwoUsername = "";
}, 5000);
}
else if (token)
{
options.isInvitedPerson = false
pong.init(options, gameAreaId, token);
hiddenGame = false;
}
}
// Pour Cherif: renommer en un truc du genre "initGameForInvitedPlayer" ?
const initGameForPrivateParty = async(invitation : any) =>
{
optionsAreNotSet = false
showWaitPage = true
console.log("invitation : ")
console.log(invitation)
if (invitation.token)
{
options.playerOneUsername = invitation.playerOneUsername;
options.playerTwoUsername = invitation.playerTwoUsername;
options.isSomeoneIsInvited = true;
options.isInvitedPerson = true
pong.init(options, gameAreaId, invitation.token);
showWaitPage = false
hiddenGame = false;
}
}
const showOptions = () => {
showGameOption = true
showInvitations = false
}
const showInvitation = async() => {
showGameOption = false;
showInvitations = true;
invitations = await fetch("http://transcendance:8080/api/v2/game/invitations")
.then(x => x.json())
invitations.length !== 0 ? isThereAnyInvitation = true : isThereAnyInvitation = false
}
const rejectInvitation = async(invitation) => {
await fetch("http://transcendance:8080/api/v2/game/decline",{
method: "POST",
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
token : invitation.token
})
})
.then(x => x.json())
.catch(error => console.log(error))
showInvitation()
}
const acceptInvitation = async(invitation : any) => {
await fetch("http://transcendance:8080/api/v2/game/accept",{
method: "POST",
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
token : invitation.token
})
})
.then(x => x.json())
.catch(error => {
console.log(error)
})
showInvitation() // maybe useless
initGameForPrivateParty(invitation)
}
</script>
<Header />
<!-- <div id="game_page"> Replacement for <body>.
Might become useless after CSS rework. -->
<div id="game_page">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<div id="canvas_container" hidden={hiddenGame}>
<canvas id={gameAreaId}/>
</div>
<body>
<div id="preload_font">.</div>
<div id="div_game_options">
{#if showError === true}
<div id="div_game" in:fly="{{ y: 10, duration: 1000 }}">
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls">
<label for="multi_balls">multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls">
<label for="moving_walls">moving walls</label>
</div>
<div>
<label>sound :</label>
<input type="radio" id="sound_on" name="sound_selector" checked>
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector">
<label for="sound_off">off</label>
</div>
<div>
<button id="play_pong_button">PLAY</button>
</div>
<legend>Error</legend>
<p>{errorMessageWhenAttemptingToGetATicket}</p>
<button id="pong_button">Retry</button>
</fieldset>
</div>
</div>
{/if}
<div id="canvas_container">
<!-- <p> =) </p> -->
</div>
{#if showWaitPage === true}
<div id="div_game" in:fly="{{ y: 10, duration: 1000 }}">
<fieldset>
<legend>Connecting to the game...</legend>
<p>{waitingMessage}</p>
</fieldset>
</div>
{/if}
<script src="public/game/pong.js" type="module" defer></script>
</body>
{#if optionsAreNotSet}
{#if showGameOption === true}
<div id="game_option">
<form on:submit|preventDefault={() => initGame()}>
<div id="div_game">
<button id="pong_button" on:click={showInvitation}>Show invitations</button>
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls" bind:checked={options.multi_balls}>
<label for="multi_balls">Multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls" bind:checked={options.moving_walls}>
<label for="moving_walls">Moving walls</label>
</div>
<div>
<p>sound :</p>
<input type="radio" id="sound_on" name="sound_selector" bind:group={options.sound} value="on">
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector" bind:group={options.sound} value="off">
<label for="sound_off">off</label>
</div>
<div>
<input type="checkbox" id="isSomeoneIsInvited" bind:checked={options.isSomeoneIsInvited}>
<label for="moving_walls">Invite a friend</label>
</div>
{#if options.isSomeoneIsInvited === true}
<select bind:value={options.playerTwoUsername}>
{#each allUsers as user }
<option value={user.username}>{user.username}</option>
{/each}
</select>
{/if}
<div>
<button id="pong_button" >PLAY</button>
</div>
</fieldset>
</div>
</form>
</div>
{/if}
{#if showInvitations}
<div id="invitations_options" in:fly="{{ y: 10, duration: 1000 }}">
<div id="div_game">
<button id="pong_button" on:click={showOptions}>Play a Game</button>
<fieldset>
<legend>Current invitation(s)</legend>
{#if isThereAnyInvitation}
{#each invitations as invitation }
<div>
{invitation.playerOneUsername} has invited you to play a pong !
<button id="pong_button" on:click={() => acceptInvitation(invitation)}>V</button>
<button id="pong_button" on:click={() => rejectInvitation(invitation)}>X</button>
</div>
{/each}
{/if}
{#if isThereAnyInvitation === false}
<p>Currently, no one asked to play with you.</p>
<button id="pong_button" on:click={showInvitation}>Reload</button>
{/if}
</fieldset>
</div>
</div>
{/if}
{/if}
</div> <!-- div "game_page" -->
<style>
@font-face {
font-family: "Bit5x3";
src: url("/fonts/Bit5x3.woff2") format("woff2"),local("Bit5x3"), url("/fonts/Bit5x3.woff") format("woff");
src:
url("/fonts/Bit5x3.woff2") format("woff2"),
local("Bit5x3"),
url("/fonts/Bit5x3.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
#preload_font {
font-family: "Bit5x3";
opacity:0;
height:0;
width:0;
display:inline-block;
}
body {
#game_page {
margin: 0;
background-color: #222425;
position: relative;
width: 100%;
height: 100%;
}
#canvas_container {
margin-top: 20px;
text-align: center;
/* border: dashed rgb(245, 245, 245) 5px; */
/* max-height: 80vh; */
/* overflow: hidden; */
}
#div_game_options {
#users_name {
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#div_game_options fieldset {
#div_game {
margin-top: 20px;
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#error_notification {
text-align: center;
display: block;
font-family: "Bit5x3";
color: rgb(143, 19, 19);
font-size: x-large;
}
#div_game fieldset {
max-width: 50vw;
width: auto;
margin: 0 auto;
}
#div_game_options fieldset div {
#div_game fieldset div {
padding: 10px;
}
#play_pong_button {
#pong_button {
font-family: "Bit5x3";
color: rgb(245, 245, 245);
background-color: #333333;
@@ -90,6 +315,7 @@ body {
padding: 10px;
}
canvas {
/* background-color: #ff0000; */
background-color: #333333;
max-width: 75vw;
/* max-height: 100vh; */

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Header from '../../pieces/Header.svelte';
import { fade, fly } from 'svelte/transition';
import * as pongSpectator from "./client/pongSpectator";
// Pour Chérif: variables indiquant l'état du match
import { matchEnded, matchAbort } from "./client/ws";
//user's stuff
let user;
let allUsers;
//Game's stuff client side only
const gameAreaId = "game_area";
//html boolean for pages
let hiddenGame = true;
onMount( async() => {
user = await fetch('http://transcendance:8080/api/v2/user')
.then( x => x.json() );
allUsers = await fetch('http://transcendance:8080/api/v2/user/all')
.then( x => x.json() );
})
onDestroy( async() => {
pongSpectator.destroy();
})
const initGameSpectator = async() => {
let gameSessionId = "ID_PLACEHOLDER"; // PLACEHOLDER
let matchOptions = 0; // PLACEHOLDER
let sound = "off"; // PLACEHOLDER
pongSpectator.init(matchOptions, sound, gameAreaId, gameSessionId);
hiddenGame = false;
};
</script>
<Header />
<!-- <div id="game_page"> Replacement for <body>.
Might become useless after CSS rework. -->
<div id="game_page">
<div id="canvas_container" hidden={hiddenGame}>
<canvas id={gameAreaId}/>
</div>
</div> <!-- div "game_page" -->
<style>
</style>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Header from "../../pieces/Header.svelte";
//user's stuff
let currentUser;
let allUsers = [];
let idInterval;
onMount( async() => {
currentUser = await fetch('http://transcendance:8080/api/v2/user')
.then( x => x.json() );
allUsers = await fetch('http://transcendance:8080/api/v2/game/ranking')
.then( x => x.json() );
idInterval = setInterval(fetchScores, 10000);
})
onDestroy( async() => {
clearInterval(idInterval);
})
function fetchScores() {
fetch('http://transcendance:8080/api/v2/game/ranking')
.then( x => x.json() )
.then( x => allUsers = x );
}
</script>
<Header />
<div class="container">
<div class="row">
<div class="col-12">
<h1>Ranking</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Win</th>
<th scope="col">Lose</th>
<th scope="col">Draw</th>
<th scope="col">Games Played</th>
</tr>
</thead>
<tbody>
{#each allUsers as user, i}
<tr>
<th scope="row">{i + 1}</th>
{#if user.username === currentUser.username}
<td><b>You ({user.username})</b></td>
{:else}
<td>{user.username}</td>
{/if}
<td>{user.stats.winGame}</td>
<td>{user.stats.loseGame}</td>
<td>{user.stats.drawGame}</td>
<td>{user.stats.totalGame}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<style>
</style>

View File

@@ -2,12 +2,17 @@
import * as c from "./constants.js"
export const soundPongArr: HTMLAudioElement[] = [];
export const soundRoblox = new Audio("http://localhost:8080/sound/roblox-oof.ogg");
export const soundRoblox = new Audio("http://transcendance:8080/sound/roblox-oof.ogg");
export function initAudio(muteFlag: boolean)
export function initAudio(sound: string)
{
let muteFlag = true;
if (sound === "on") {
muteFlag = false;
}
for (let i = 0; i <= 32; i++) {
soundPongArr.push(new Audio("http://localhost:8080/sound/pong/"+i+".ogg"));
soundPongArr.push(new Audio("http://transcendance:8080/sound/pong/"+i+".ogg"));
soundPongArr[i].volume = c.soundPongVolume;
soundPongArr[i].muted = muteFlag;
}

View File

@@ -1,21 +1,25 @@
import * as c from ".././constants.js"
class GameArea {
export class GameArea {
keys: string[] = [];
handleInputInterval: number = 0;
gameLoopInterval: number = 0;
drawLoopInterval: number = 0;
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
constructor() {
this.canvas = document.createElement("canvas");
constructor(canvas_id: string) {
const canvas = document.getElementById("game_area");
if (canvas && canvas instanceof HTMLCanvasElement) {
this.canvas = canvas;
}
else {
console.log("GameArea init error, invalid canvas_id");
return;
}
this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
this.canvas.width = c.CanvasWidth;
this.canvas.height = c.CanvasWidth / c.CanvasRatio;
let container = document.getElementById("canvas_container");
if (container)
container.insertBefore(this.canvas, container.childNodes[0]);
}
addKey(key: string) {
key = key.toLowerCase();
@@ -34,5 +38,3 @@ class GameArea {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
export {GameArea}

View File

@@ -5,7 +5,7 @@ import { Vector, VectorInteger } from "../../shared_js/class/Vector.js";
import { TextElem, TextNumericValue } from "./Text.js";
import { RectangleClient, MovingRectangleClient, RacketClient, BallClient, Line } from "./RectangleClient.js";
import { GameComponents } from "../../shared_js/class/GameComponents.js";
import { MovingRectangle } from "../../shared_js/class/Rectangle.js";
import type { MovingRectangle } from "../../shared_js/class/Rectangle.js";
class GameComponentsExtensionForClient extends GameComponents {
wallTop: RectangleClient | MovingRectangleClient;
@@ -62,12 +62,13 @@ class GameComponentsExtensionForClient extends GameComponents {
}
}
class GameComponentsClient extends GameComponentsExtensionForClient {
export class GameComponentsClient extends GameComponentsExtensionForClient {
midLine: Line;
scoreLeft: TextNumericValue;
scoreRight: TextNumericValue;
text1: TextElem;
text2: TextElem;
text3: TextElem;
w_grid_mid: RectangleClient;
w_grid_u1: RectangleClient;
@@ -90,6 +91,8 @@ class GameComponentsClient extends GameComponentsExtensionForClient {
// Text
pos.assign(0, c.h_mid);
this.text1 = new TextElem(pos, Math.floor(c.w/8), ctx, "white");
this.text2 = new TextElem(pos, Math.floor(c.w/24), ctx, "white");
this.text3 = new TextElem(pos, Math.floor(c.w/24), ctx, "white");
// Dotted Midline
pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize);
@@ -110,5 +113,3 @@ class GameComponentsClient extends GameComponentsExtensionForClient {
this.h_grid_d1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen");
}
}
export {GameComponentsClient}

View File

@@ -0,0 +1,10 @@
export class InitOptions {
sound = "off";
multi_balls = false;
moving_walls = false;
isSomeoneIsInvited = false;
isInvitedPerson = false;
playerOneUsername = "";
playerTwoUsername = "";
}

Some files were not shown because too many files have changed in this diff Show More