diff --git a/srcs/docker-compose.yml b/srcs/docker-compose.yml index 62dc9479..44810255 100644 --- a/srcs/docker-compose.yml +++ b/srcs/docker-compose.yml @@ -16,6 +16,18 @@ services: - postgresql - redis + game_server: + build: + context: ./requirements/game_server + dockerfile: Dockerfile + volumes: + - ./requirements/game_server/game_back:/usr/app/src + environment: + NODE_ENV: "${NODE_ENV}" + restart: unless-stopped + depends_on: + - backend_dev + frontend_dev: build: context: ./requirements/svelte diff --git a/srcs/requirements/game_server/Dockerfile b/srcs/requirements/game_server/Dockerfile index c410cee6..ba94307a 100644 --- a/srcs/requirements/game_server/Dockerfile +++ b/srcs/requirements/game_server/Dockerfile @@ -1,3 +1,15 @@ -FROM node:alpine +FROM node:alpine AS build + +WORKDIR /usr/app + +COPY ./game_back ./ + +RUN npx tsc + +WORKDIR /usr/app/src/server + +RUN ls -la + +CMD [ "node", "wsServer.js"] diff --git a/srcs/requirements/game_server/game_back/jsconfig.json b/srcs/requirements/game_server/game_back/jsconfig.json new file mode 100644 index 00000000..347bf03f --- /dev/null +++ b/srcs/requirements/game_server/game_back/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2020", + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/srcs/requirements/game_server/game_back/package-lock.json b/srcs/requirements/game_server/game_back/package-lock.json new file mode 100644 index 00000000..414d8d22 --- /dev/null +++ b/srcs/requirements/game_server/game_back/package-lock.json @@ -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": {} + } + } +} diff --git a/srcs/requirements/game_server/game_back/package.json b/srcs/requirements/game_server/game_back/package.json new file mode 100644 index 00000000..b9702186 --- /dev/null +++ b/srcs/requirements/game_server/game_back/package.json @@ -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" + } +} diff --git a/srcs/requirements/game_server/game_back/src/server/class/Client.js b/srcs/requirements/game_server/game_back/src/server/class/Client.js new file mode 100644 index 00000000..4225a2d1 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/Client.js @@ -0,0 +1,52 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ClientSpectator = exports.ClientPlayer = exports.Client = void 0; +const ev = __importStar(require("../../shared_js/class/Event.js")); +class Client { + constructor(socket, id) { + this.isAlive = true; + this.gameSession = null; + this.socket = socket; + this.id = id; + } +} +exports.Client = Client; +class ClientPlayer extends Client { + constructor(socket, id, racket) { + super(socket, id); + this.matchOptions = 0; + this.inputBuffer = new ev.EventInput(); + this.lastInputId = 0; + this.racket = racket; + } +} +exports.ClientPlayer = ClientPlayer; +class ClientSpectator extends Client { + constructor(socket, id) { + super(socket, id); + } +} +exports.ClientSpectator = ClientSpectator; diff --git a/srcs/requirements/game_server/game_back/src/server/class/Client.ts b/srcs/requirements/game_server/game_back/src/server/class/Client.ts new file mode 100644 index 00000000..c6e4defa --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/Client.ts @@ -0,0 +1,34 @@ + +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 { + 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); + } +} diff --git a/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.js b/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.js new file mode 100644 index 00000000..27e8019a --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GameComponentsServer = void 0; +const GameComponents_js_1 = require("../../shared_js/class/GameComponents.js"); +class GameComponentsServer extends GameComponents_js_1.GameComponents { + constructor(options) { + super(options); + this.scoreLeft = 0; + this.scoreRight = 0; + } +} +exports.GameComponentsServer = GameComponentsServer; diff --git a/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.ts b/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.ts new file mode 100644 index 00000000..691a3991 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/GameComponentsServer.ts @@ -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); + } +} diff --git a/srcs/requirements/game_server/game_back/src/server/class/GameSession.js b/srcs/requirements/game_server/game_back/src/server/class/GameSession.js new file mode 100644 index 00000000..00660040 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/GameSession.js @@ -0,0 +1,239 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GameSession = void 0; +const en = __importStar(require("../../shared_js/enums.js")); +const ev = __importStar(require("../../shared_js/class/Event.js")); +const c = __importStar(require("../constants.js")); +const GameComponentsServer_js_1 = require("./GameComponentsServer.js"); +const wsServer_js_1 = require("../wsServer.js"); +const utils_js_1 = require("../utils.js"); +const wallsMovement_js_1 = require("../../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" +*/ +class GameSession { + constructor(id, matchOptions) { + this.playersMap = new Map(); + this.unreadyPlayersMap = new Map(); + this.spectatorsMap = new Map(); + this.gameLoopInterval = 0; + this.playersUpdateInterval = 0; + this.spectatorsUpdateInterval = 0; + this.matchEnded = false; + this.id = id; + this.matchOptions = matchOptions; + this.components = new GameComponentsServer_js_1.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) { + s.playersMap.forEach((client) => { + client.socket.on("message", wsServer_js_1.clientInputListener); + }); + s.actual_time = Date.now(); + 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) { + s.playersMap.forEach((client) => { + client.socket.off("message", wsServer_js_1.clientInputListener); + }); + clearInterval(s.gameLoopInterval); + clearInterval(s.playersUpdateInterval); + clearInterval(s.spectatorsUpdateInterval); + } + instantInputDebug(client) { + this._handleInput(c.fixedDeltaTime, client); + } + _handleInput(delta, client) { + // 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; + } + _gameLoop(s) { + /* 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) { + (0, wallsMovement_js_1.wallsMovements)(s.delta_time, gc); + } + } + _ballMovement(delta, 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); + } + } + } + _scoreUpdate(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))); + }); + } + _playersUpdate(s) { + const gameState = s._gameStateSnapshot(); + s.playersMap.forEach((client) => { + gameState.lastInputId = client.lastInputId; + client.socket.send(JSON.stringify(gameState)); + }); + } + _spectatorsUpdate(s) { + const gameState = s._gameStateSnapshot(); + s.spectatorsMap.forEach((client) => { + client.socket.send(JSON.stringify(gameState)); + }); + } + _gameStateSnapshot() { + 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); + } + _newRound(s, 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) { + s._matchEnd(s); + return; + } + } + ball.pos.x = c.w_mid; + ball.pos.y = (0, utils_js_1.random)(c.h * 0.3, c.h * 0.7); + ball.speed = ball.baseSpeed; + ball.ballInPlay = true; + } + _checkDisconnexions() { + if (this.playersMap.size !== 2) { + this.matchEnded = true; + if (this.playersMap.size != 0) { + const gc = this.components; + const luckyWinner = this.playersMap.values().next().value; + let eventEnd; + if (luckyWinner.racket === gc.playerLeft) { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.left, true); + console.log("Player Left WIN (by forfeit)"); + } + else { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.right, true); + console.log("Player Right WIN (by forfeit)"); + } + luckyWinner.socket.send(JSON.stringify(eventEnd)); + this.spectatorsMap.forEach((client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + } + return true; + } + return false; + } + _matchEnd(s) { + s.matchEnded = true; + const gc = s.components; + let eventEnd; + if (gc.scoreLeft > gc.scoreRight) { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.left); + console.log("Player Left WIN"); + } + else { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.right); + console.log("Player Right WIN"); + } + s.playersMap.forEach((client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + s.spectatorsMap.forEach((client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + } +} +exports.GameSession = GameSession; diff --git a/srcs/requirements/game_server/game_back/src/server/class/GameSession.ts b/srcs/requirements/game_server/game_back/src/server/class/GameSession.ts new file mode 100644 index 00000000..55879135 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/class/GameSession.ts @@ -0,0 +1,241 @@ + +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 } 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 = new Map(); + unreadyPlayersMap: Map = new Map(); + spectatorsMap: Map = new Map(); + gameLoopInterval: NodeJS.Timer | number = 0; + playersUpdateInterval: NodeJS.Timer | number = 0; + spectatorsUpdateInterval: NodeJS.Timer | number = 0; + components: GameComponentsServer; + matchOptions: en.MatchOptions; + matchEnded: boolean = false; + + actual_time: number; + last_time: number; + delta_time: number; + + constructor(id: string, matchOptions: en.MatchOptions) { + this.id = id; + this.matchOptions = matchOptions; + 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.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) { + const gameState: ev.EventGameUpdate = s._gameStateSnapshot(); + s.playersMap.forEach( (client) => { + gameState.lastInputId = client.lastInputId; + client.socket.send(JSON.stringify(gameState)); + }); + } + private _spectatorsUpdate(s: GameSession) { + const gameState = s._gameStateSnapshot(); + s.spectatorsMap.forEach( (client) => { + client.socket.send(JSON.stringify(gameState)); + }); + } + 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) + { + s._matchEnd(s); + 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) + { + const gc = this.components; + const luckyWinner: ClientPlayer = this.playersMap.values().next().value; + let eventEnd: ev.EventMatchEnd; + if (luckyWinner.racket === gc.playerLeft) { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.left, true); + console.log("Player Left WIN (by forfeit)"); + } + else { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.right, true); + console.log("Player Right WIN (by forfeit)"); + } + luckyWinner.socket.send(JSON.stringify(eventEnd)); + this.spectatorsMap.forEach( (client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + } + return true; + } + return false; + } + private _matchEnd(s: GameSession) { + s.matchEnded = true; + const gc = s.components; + + let eventEnd: ev.EventMatchEnd; + if (gc.scoreLeft > gc.scoreRight) { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.left); + console.log("Player Left WIN"); + } + else { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.right); + console.log("Player Right WIN"); + } + + s.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + s.spectatorsMap.forEach( (client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + } +} diff --git a/srcs/requirements/game_server/game_back/src/server/constants.js b/srcs/requirements/game_server/game_back/src/server/constants.js new file mode 100644 index 00000000..b9ede1dc --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/constants.js @@ -0,0 +1,24 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.spectatorsUpdateIntervalMS = exports.playersUpdateIntervalMS = exports.fixedDeltaTime = exports.serverGameLoopIntervalMS = void 0; +__exportStar(require("../shared_js/constants.js"), exports); +// 15ms == 1000/66.666 +exports.serverGameLoopIntervalMS = 15; // millisecond +exports.fixedDeltaTime = exports.serverGameLoopIntervalMS / 1000; // second +// 33.333ms == 1000/30 +exports.playersUpdateIntervalMS = 1000 / 30; // millisecond +exports.spectatorsUpdateIntervalMS = 1000 / 30; // millisecond diff --git a/srcs/requirements/game_server/game_back/src/server/constants.ts b/srcs/requirements/game_server/game_back/src/server/constants.ts new file mode 100644 index 00000000..b7efffd3 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/constants.ts @@ -0,0 +1,10 @@ + +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 diff --git a/srcs/requirements/game_server/game_back/src/server/server.js b/srcs/requirements/game_server/game_back/src/server/server.js new file mode 100644 index 00000000..2ac805e5 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/server.js @@ -0,0 +1,42 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const http_1 = __importDefault(require("http")); +const url_1 = __importDefault(require("url")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const wsServer_js_1 = require("./wsServer.js"); +wsServer_js_1.wsServer; // no-op, just for loading +const hostname = "localhost"; +const port = 8080; +const root = "../../www/"; +const server = http_1.default.createServer((req, res) => { + // let q = new URL(req.url, `http://${req.getHeaders().host}`) + let q = url_1.default.parse(req.url, true); + let filename = root + q.pathname; + fs_1.default.readFile(filename, (err, data) => { + if (err) { + res.writeHead(404, { "Content-Type": "text/html" }); + return res.end("404 Not Found"); + } + if (path_1.default.extname(filename) === ".html") { + res.writeHead(200, { "Content-Type": "text/html" }); + } + else if (path_1.default.extname(filename) === ".js") { + res.writeHead(200, { "Content-Type": "application/javascript" }); + } + else if (path_1.default.extname(filename) === ".mp3") { + res.writeHead(200, { "Content-Type": "audio/mpeg" }); + } + else if (path_1.default.extname(filename) === ".ogg") { + res.writeHead(200, { "Content-Type": "audio/ogg" }); + } + res.write(data); + return res.end(); + }); +}); +server.listen(port, hostname, () => { + console.log(`Pong running at http://${hostname}:${port}/pong.html`); +}); diff --git a/srcs/requirements/game_server/game_back/src/server/server.ts b/srcs/requirements/game_server/game_back/src/server/server.ts new file mode 100644 index 00000000..20801d7f --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/server.ts @@ -0,0 +1,41 @@ + +import http from "http"; +import url from "url"; +import fs from "fs"; +import path from "path"; + +import {wsServer} from "./wsServer.js"; wsServer; // no-op, just for loading + +const hostname = "localhost"; +const port = 8080; +const root = "../../www/"; + +const server = http.createServer((req, res) => { + // let q = new URL(req.url, `http://${req.getHeaders().host}`) + let q = url.parse(req.url, true); + let filename = root + q.pathname; + fs.readFile(filename, (err, data) => { + if (err) { + res.writeHead(404, {"Content-Type": "text/html"}); + return res.end("404 Not Found"); + } + if (path.extname(filename) === ".html") { + res.writeHead(200, {"Content-Type": "text/html"}); + } + else if (path.extname(filename) === ".js") { + res.writeHead(200, {"Content-Type": "application/javascript"}); + } + else if (path.extname(filename) === ".mp3") { + res.writeHead(200, {"Content-Type": "audio/mpeg"}); + } + else if (path.extname(filename) === ".ogg") { + res.writeHead(200, {"Content-Type": "audio/ogg"}); + } + res.write(data); + return res.end(); + }); +}); + +server.listen(port, hostname, () => { + console.log(`Pong running at http://${hostname}:${port}/pong.html`); +}); diff --git a/srcs/requirements/game_server/game_back/src/server/utils.js b/srcs/requirements/game_server/game_back/src/server/utils.js new file mode 100644 index 00000000..abffeb16 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/utils.js @@ -0,0 +1,22 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.shortId = void 0; +__exportStar(require("../shared_js/utils.js"), exports); +function shortId(id) { + return id.substring(0, id.indexOf("-")); +} +exports.shortId = shortId; diff --git a/srcs/requirements/game_server/game_back/src/server/utils.ts b/srcs/requirements/game_server/game_back/src/server/utils.ts new file mode 100644 index 00000000..3cd0a4a5 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/utils.ts @@ -0,0 +1,6 @@ + +export * from "../shared_js/utils.js" + +export function shortId(id: string): string { + return id.substring(0, id.indexOf("-")); +} diff --git a/srcs/requirements/game_server/game_back/src/server/wsServer.js b/srcs/requirements/game_server/game_back/src/server/wsServer.js new file mode 100644 index 00000000..a34cd129 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/wsServer.js @@ -0,0 +1,236 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.clientInputListener = exports.wsServer = exports.WebSocket = void 0; +const ws_1 = require("ws"); +class WebSocket extends ws_1.WebSocket { +} +exports.WebSocket = WebSocket; +const uuid_1 = require("uuid"); +const en = __importStar(require("../shared_js/enums.js")); +const ev = __importStar(require("../shared_js/class/Event.js")); +const Client_js_1 = require("./class/Client.js"); +const GameSession_js_1 = require("./class/GameSession.js"); +const utils_js_1 = require("./utils.js"); +// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ? +const wsPort = 8042; +exports.wsServer = new ws_1.WebSocketServer({ port: wsPort, path: "/pong" }); +const clientsMap = new Map; // socket.id/Client +const matchmakingPlayersMap = new Map; // socket.id/ClientPlayer (duplicates with clientsMap) +const gameSessionsMap = new Map; // GameSession.id(url)/GameSession +exports.wsServer.on("connection", connectionListener); +exports.wsServer.on("error", errorListener); +exports.wsServer.on("close", closeListener); +function connectionListener(socket, request) { + const id = (0, uuid_1.v4)(); + const client = new Client_js_1.Client(socket, id); + clientsMap.set(id, client); + socket.id = id; + socket.on("pong", function heartbeat() { + client.isAlive = true; + console.log(`client ${(0, utils_js_1.shortId)(client.id)} is alive`); + }); + socket.on("message", function log(data) { + try { + const event = JSON.parse(data); + if (event.type === en.EventTypes.clientInput) { + return; + } + } + catch (e) { } + console.log("data: " + data); + }); + socket.once("message", clientAnnounceListener); +} +function clientAnnounceListener(data) { + try { + const msg = 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 = msg; + const player = clientsMap.get(this.id); + player.matchOptions = announce.matchOptions; + this.send(JSON.stringify(new ev.EventAssignId(this.id))); + this.send(JSON.stringify(new ev.ServerEvent(en.EventTypes.matchmakingInProgress))); + matchmaking(player); + } + else if (msg.role === en.ClientRole.spectator) { + const announce = msg; + const gameSession = gameSessionsMap.get(announce.gameSessionId); + if (!gameSession) { + // WIP: send "invalid game session" + return; + } + const spectator = clientsMap.get(this.id); + 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 matchmaking(player) { + const minPlayersNumber = 2; + const maxPlayersNumber = 2; + const matchOptions = player.matchOptions; + matchmakingPlayersMap.set(player.id, player); + const compatiblePlayers = []; + for (const [id, client] of matchmakingPlayersMap) { + if (client.matchOptions === matchOptions) { + compatiblePlayers.push(client); + if (compatiblePlayers.length === maxPlayersNumber) { + break; + } + } + } + if (compatiblePlayers.length < minPlayersNumber) { + return; + } + // const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR + const id = (0, uuid_1.v4)(); + const gameSession = new GameSession_js_1.GameSession(id, matchOptions); + gameSessionsMap.set(id, gameSession); + compatiblePlayers.forEach((client) => { + matchmakingPlayersMap.delete(client.id); + client.gameSession = gameSession; + gameSession.playersMap.set(client.id, client); + gameSession.unreadyPlayersMap.set(client.id, client); + }); + // WIP: Not pretty, hardcoded two players. + // Could be done in gameSession maybe ? + compatiblePlayers[0].racket = gameSession.components.playerRight; + compatiblePlayers[1].racket = gameSession.components.playerLeft; + compatiblePlayers.forEach((client) => { + client.socket.once("message", playerReadyConfirmationListener); + }); + compatiblePlayers[0].socket.send(JSON.stringify(new ev.EventMatchmakingComplete(en.PlayerSide.right))); + compatiblePlayers[1].socket.send(JSON.stringify(new ev.EventMatchmakingComplete(en.PlayerSide.left))); + setTimeout(function abortMatch() { + if (gameSession.unreadyPlayersMap.size !== 0) { + gameSessionsMap.delete(gameSession.id); + gameSession.playersMap.forEach((client) => { + client.socket.send(JSON.stringify(new ev.ServerEvent(en.EventTypes.matchAbort))); + client.gameSession = null; + clientTerminate(client); + }); + } + }, 5000); +} +function playerReadyConfirmationListener(data) { + try { + const msg = 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) { + 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); +} +function clientInputListener(data) { + try { + // const input: ev.ClientEvent = JSON.parse(data); + const input = JSON.parse(data); + if (input.type === en.EventTypes.clientInput) { + const client = clientsMap.get(this.id); + client.inputBuffer = input; + client.gameSession.instantInputDebug(client); // wip + } + else { + console.log("Invalid clientInput"); + } + } + catch (e) { + console.log("Invalid JSON (clientInputListener)"); + } +} +exports.clientInputListener = clientInputListener; +//////////// +//////////// +const pingInterval = setInterval(() => { + let deleteLog = ""; + clientsMap.forEach((client) => { + if (!client.isAlive) { + clientTerminate(client); + deleteLog += ` ${(0, utils_js_1.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("matchmakingPlayersMap size: " + matchmakingPlayersMap.size); + console.log(""); +}, 4200); +function clientTerminate(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 (matchmakingPlayersMap.has(client.id)) { + matchmakingPlayersMap.delete(client.id); + } +} +function closeListener() { + clearInterval(pingInterval); +} +function errorListener(error) { + console.log("Error: " + JSON.stringify(error)); +} diff --git a/srcs/requirements/game_server/game_back/src/server/wsServer.ts b/srcs/requirements/game_server/game_back/src/server/wsServer.ts new file mode 100644 index 00000000..93a9623c --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/server/wsServer.ts @@ -0,0 +1,265 @@ + +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 { Client, ClientPlayer, ClientSpectator } from "./class/Client.js" +import { GameSession } from "./class/GameSession.js" +import { shortId } from "./utils.js"; +import { gameSessionIdPLACEHOLDER } from "./constants.js"; + +// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ? +const wsPort = 8042; +export const wsServer = new WebSocketServer({port: wsPort, path: "/pong"}); +const clientsMap: Map = new Map; // socket.id/Client +const matchmakingPlayersMap: Map = new Map; // socket.id/ClientPlayer (duplicates with clientsMap) +const gameSessionsMap: Map = 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); +} + + +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 = msg; + const player = clientsMap.get(this.id) as ClientPlayer; + player.matchOptions = announce.matchOptions; + this.send(JSON.stringify( new ev.EventAssignId(this.id) )); + this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )); + matchmaking(player); + } + else if (msg.role === en.ClientRole.spectator) + { + const announce: ev.ClientAnnounceSpectator = msg; + const gameSession = gameSessionsMap.get(announce.gameSessionId); + if (!gameSession) { + // WIP: send "invalid game session" + 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 matchmaking(player: ClientPlayer) +{ + const minPlayersNumber = 2; + const maxPlayersNumber = 2; + const matchOptions = player.matchOptions; + matchmakingPlayersMap.set(player.id, player); + + const compatiblePlayers: ClientPlayer[] = []; + for (const [id, client] of matchmakingPlayersMap) + { + if (client.matchOptions === matchOptions) + { + compatiblePlayers.push(client); + if (compatiblePlayers.length === maxPlayersNumber) { + break; + } + } + } + + if (compatiblePlayers.length < minPlayersNumber) { + return; + } + + // const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR + const id = uuidv4(); + const gameSession = new GameSession(id, matchOptions); + gameSessionsMap.set(id, gameSession); + + compatiblePlayers.forEach((client) => { + matchmakingPlayersMap.delete(client.id); + client.gameSession = gameSession; + gameSession.playersMap.set(client.id, client); + gameSession.unreadyPlayersMap.set(client.id, client); + }); + + // WIP: Not pretty, hardcoded two players. + // Could be done in gameSession maybe ? + compatiblePlayers[0].racket = gameSession.components.playerRight; + compatiblePlayers[1].racket = gameSession.components.playerLeft; + + compatiblePlayers.forEach((client) => { + client.socket.once("message", playerReadyConfirmationListener); + }); + + compatiblePlayers[0].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) )); + compatiblePlayers[1].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) )); + + setTimeout(function abortMatch() { + if (gameSession.unreadyPlayersMap.size !== 0) + { + gameSessionsMap.delete(gameSession.id); + gameSession.playersMap.forEach((client) => { + client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchAbort) )); + client.gameSession = null; + clientTerminate(client); + }); + } + }, 5000); +} + + +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) { + 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("matchmakingPlayersMap size: " + matchmakingPlayersMap.size); + console.log(""); +}, 4200); + + +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 (matchmakingPlayersMap.has(client.id)) { + matchmakingPlayersMap.delete(client.id); + } +} + + +function closeListener() +{ + clearInterval(pingInterval); +} + + +function errorListener(error: Error) +{ + console.log("Error: " + JSON.stringify(error)); +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Event.js b/srcs/requirements/game_server/game_back/src/shared_js/class/Event.js new file mode 100644 index 00000000..d9312f90 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Event.js @@ -0,0 +1,121 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EventInput = exports.ClientAnnounceSpectator = exports.ClientAnnouncePlayer = exports.ClientAnnounce = exports.ClientEvent = exports.EventMatchEnd = exports.EventScoreUpdate = exports.EventGameUpdate = exports.EventMatchmakingComplete = exports.EventAssignId = exports.ServerEvent = void 0; +const en = __importStar(require("../enums.js")); +/* From Server */ +class ServerEvent { + constructor(type = 0) { + this.type = type; + } +} +exports.ServerEvent = ServerEvent; +class EventAssignId extends ServerEvent { + constructor(id) { + super(en.EventTypes.assignId); + this.id = id; + } +} +exports.EventAssignId = EventAssignId; +class EventMatchmakingComplete extends ServerEvent { + constructor(side) { + super(en.EventTypes.matchmakingComplete); + this.side = side; + } +} +exports.EventMatchmakingComplete = EventMatchmakingComplete; +class EventGameUpdate extends ServerEvent { + constructor() { + super(en.EventTypes.gameUpdate); + this.playerLeft = { + y: 0 + }; + this.playerRight = { + y: 0 + }; + this.ballsArr = []; + this.wallTop = { + y: 0 + }; + this.wallBottom = { + y: 0 + }; + this.lastInputId = 0; + } +} +exports.EventGameUpdate = EventGameUpdate; +class EventScoreUpdate extends ServerEvent { + constructor(scoreLeft, scoreRight) { + super(en.EventTypes.scoreUpdate); + this.scoreLeft = scoreLeft; + this.scoreRight = scoreRight; + } +} +exports.EventScoreUpdate = EventScoreUpdate; +class EventMatchEnd extends ServerEvent { + constructor(winner, forfeit = false) { + super(en.EventTypes.matchEnd); + this.winner = winner; + this.forfeit = forfeit; + } +} +exports.EventMatchEnd = EventMatchEnd; +/* From Client */ +class ClientEvent { + constructor(type = 0) { + this.type = type; + } +} +exports.ClientEvent = ClientEvent; +class ClientAnnounce extends ClientEvent { + constructor(role) { + super(en.EventTypes.clientAnnounce); + this.role = role; + } +} +exports.ClientAnnounce = ClientAnnounce; +class ClientAnnouncePlayer extends ClientAnnounce { + constructor(matchOptions, clientId = "") { + super(en.ClientRole.player); + this.clientId = clientId; + this.matchOptions = matchOptions; + } +} +exports.ClientAnnouncePlayer = ClientAnnouncePlayer; +class ClientAnnounceSpectator extends ClientAnnounce { + constructor(gameSessionId) { + super(en.ClientRole.spectator); + this.gameSessionId = gameSessionId; + } +} +exports.ClientAnnounceSpectator = ClientAnnounceSpectator; +class EventInput extends ClientEvent { + constructor(input = en.InputEnum.noInput, id = 0) { + super(en.EventTypes.clientInput); + this.input = input; + this.id = id; + } +} +exports.EventInput = EventInput; diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Event.ts b/srcs/requirements/game_server/game_back/src/shared_js/class/Event.ts new file mode 100644 index 00000000..992827e4 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Event.ts @@ -0,0 +1,117 @@ + +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; + } +} + + +/* 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; + matchOptions: en.MatchOptions; + constructor(matchOptions: en.MatchOptions, clientId: string = "") { + super(en.ClientRole.player); + this.clientId = clientId; + this.matchOptions = matchOptions; + } +} + +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; + } +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.js b/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.js new file mode 100644 index 00000000..24126820 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.js @@ -0,0 +1,78 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.GameComponents = void 0; +const c = __importStar(require("../constants.js")); +const en = __importStar(require("../../shared_js/enums.js")); +const Vector_js_1 = require("./Vector.js"); +const Rectangle_js_1 = require("./Rectangle.js"); +const utils_js_1 = require("../utils.js"); +class GameComponents { + constructor(options) { + this.ballsArr = []; + const pos = new Vector_js_1.VectorInteger; + // Rackets + pos.assign(0 + c.pw, c.h_mid - c.ph / 2); + this.playerLeft = new Rectangle_js_1.Racket(pos, c.pw, c.ph, c.racketSpeed); + pos.assign(c.w - c.pw - c.pw, c.h_mid - c.ph / 2); + this.playerRight = new Rectangle_js_1.Racket(pos, c.pw, c.ph, c.racketSpeed); + // Balls + let ballsCount = 1; + if (options & en.MatchOptions.multiBalls) { + ballsCount = c.multiBallsCount; + } + pos.assign(-c.ballSize, -c.ballSize); // ball out =) + while (this.ballsArr.length < ballsCount) { + this.ballsArr.push(new Rectangle_js_1.Ball(pos, c.ballSize, c.ballSpeed, c.ballSpeedIncrease)); + } + this.ballsArr.forEach((ball) => { + ball.dir.x = 1; + if ((0, utils_js_1.random)() > 0.5) { + ball.dir.x *= -1; + } + ball.dir.y = (0, utils_js_1.random)(0, 0.2); + if ((0, utils_js_1.random)() > 0.5) { + ball.dir.y *= -1; + } + ball.dir = ball.dir.normalized(); + }); + // Walls + if (options & en.MatchOptions.movingWalls) { + pos.assign(0, 0); + this.wallTop = new Rectangle_js_1.MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + this.wallTop.dir.y = -1; + pos.assign(0, c.h - c.wallSize); + this.wallBottom = new Rectangle_js_1.MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + this.wallBottom.dir.y = 1; + } + else { + pos.assign(0, 0); + this.wallTop = new Rectangle_js_1.Rectangle(pos, c.w, c.wallSize); + pos.assign(0, c.h - c.wallSize); + this.wallBottom = new Rectangle_js_1.Rectangle(pos, c.w, c.wallSize); + } + } +} +exports.GameComponents = GameComponents; diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.ts b/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.ts new file mode 100644 index 00000000..ec36f15f --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/GameComponents.ts @@ -0,0 +1,63 @@ + +import * as c from "../constants.js" +import * as en from "../../shared_js/enums.js" +import { VectorInteger } from "./Vector.js"; +import { Rectangle, MovingRectangle, Racket, Ball } from "./Rectangle.js"; +import { random } from "../utils.js"; + +export class GameComponents { + wallTop: Rectangle | MovingRectangle; + wallBottom: Rectangle | MovingRectangle; + playerLeft: Racket; + playerRight: Racket; + ballsArr: Ball[] = []; + constructor(options: en.MatchOptions) + { + const pos = new VectorInteger; + + // Rackets + pos.assign(0+c.pw, c.h_mid-c.ph/2); + this.playerLeft = new Racket(pos, c.pw, c.ph, c.racketSpeed); + pos.assign(c.w-c.pw-c.pw, c.h_mid-c.ph/2); + this.playerRight = new Racket(pos, c.pw, c.ph, c.racketSpeed); + + // Balls + let ballsCount = 1; + if (options & en.MatchOptions.multiBalls) { + ballsCount = c.multiBallsCount; + } + pos.assign(-c.ballSize, -c.ballSize); // ball out =) + while (this.ballsArr.length < ballsCount) { + this.ballsArr.push(new Ball(pos, c.ballSize, c.ballSpeed, c.ballSpeedIncrease)) + } + this.ballsArr.forEach((ball) => { + ball.dir.x = 1; + if (random() > 0.5) { + ball.dir.x *= -1; + } + + ball.dir.y = random(0, 0.2); + if (random() > 0.5) { + ball.dir.y *= -1; + } + + ball.dir = ball.dir.normalized(); + }); + + // Walls + if (options & en.MatchOptions.movingWalls) { + pos.assign(0, 0); + this.wallTop = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + (this.wallTop).dir.y = -1; + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + (this.wallBottom).dir.y = 1; + } + else { + pos.assign(0, 0); + this.wallTop = new Rectangle(pos, c.w, c.wallSize); + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new Rectangle(pos, c.w, c.wallSize); + } + } +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.js b/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.js new file mode 100644 index 00000000..5f73582c --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.js @@ -0,0 +1,154 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Ball = exports.Racket = exports.MovingRectangle = exports.Rectangle = void 0; +const Vector_js_1 = require("./Vector.js"); +const c = __importStar(require("../constants.js")); +class Rectangle { + constructor(pos, width, height) { + this.pos = new Vector_js_1.VectorInteger(pos.x, pos.y); + this.width = width; + this.height = height; + } + collision(collider) { + const thisLeft = this.pos.x; + const thisRight = this.pos.x + this.width; + const thisTop = this.pos.y; + const thisBottom = this.pos.y + this.height; + const colliderLeft = collider.pos.x; + const colliderRight = collider.pos.x + collider.width; + const colliderTop = collider.pos.y; + const colliderBottom = collider.pos.y + collider.height; + if ((thisBottom < colliderTop) + || (thisTop > colliderBottom) + || (thisRight < colliderLeft) + || (thisLeft > colliderRight)) { + return false; + } + else { + return true; + } + } +} +exports.Rectangle = Rectangle; +class MovingRectangle extends Rectangle { + constructor(pos, width, height, baseSpeed) { + super(pos, width, height); + this.dir = new Vector_js_1.Vector(0, 0); + this.baseSpeed = baseSpeed; + this.speed = baseSpeed; + } + move(delta) { + // console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`); + // this.pos.x += Math.floor(this.dir.x * this.speed * delta); + // this.pos.y += Math.floor(this.dir.y * this.speed * delta); + this.pos.x += this.dir.x * this.speed * delta; + this.pos.y += this.dir.y * this.speed * delta; + } + moveAndCollide(delta, colliderArr) { + this._moveAndCollideAlgo(delta, colliderArr); + } + _moveAndCollideAlgo(delta, colliderArr) { + let oldPos = new Vector_js_1.VectorInteger(this.pos.x, this.pos.y); + this.move(delta); + if (colliderArr.some(this.collision, this)) { + this.pos = oldPos; + } + } +} +exports.MovingRectangle = MovingRectangle; +class Racket extends MovingRectangle { + constructor(pos, width, height, baseSpeed) { + super(pos, width, height, baseSpeed); + } + moveAndCollide(delta, colliderArr) { + // let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug + this._moveAndCollideAlgo(delta, colliderArr); + // console.log(`y change: ${this.pos.y - oldPos.y}`); + } +} +exports.Racket = Racket; +class Ball extends MovingRectangle { + constructor(pos, size, baseSpeed, speedIncrease) { + super(pos, size, size, baseSpeed); + this.ballInPlay = false; + this.speedIncrease = speedIncrease; + } + moveAndBounce(delta, colliderArr) { + this.move(delta); + let i = colliderArr.findIndex(this.collision, this); + if (i != -1) { + this.bounce(colliderArr[i]); + this.move(delta); + } + } + bounce(collider) { + this._bounceAlgo(collider); + } + _bounceAlgo(collider) { + /* Could be more generic, but testing only Racket is enough, + because in Pong collider can only be Racket or Wall. */ + if (collider instanceof Racket) { + this._bounceRacket(collider); + } + else { + this._bounceWall(); + } + } + _bounceWall() { + this.dir.y = this.dir.y * -1; + } + _bounceRacket(racket) { + this._bounceRacketAlgo(racket); + } + _bounceRacketAlgo(racket) { + this.speed += this.speedIncrease; + let x = this.dir.x * -1; + const angleFactorDegree = 60; + const angleFactor = angleFactorDegree / 90; + const racketHalf = racket.height / 2; + const ballMid = this.pos.y + this.height / 2; + const racketMid = racket.pos.y + racketHalf; + let impact = ballMid - racketMid; + const horizontalMargin = racketHalf * 0.15; + if (impact < horizontalMargin && impact > -horizontalMargin) { + impact = 0; + } + else if (impact > 0) { + impact = impact - horizontalMargin; + } + else if (impact < 0) { + impact = impact + horizontalMargin; + } + let y = impact / (racketHalf - horizontalMargin) * angleFactor; + this.dir.assign(x, y); + // Normalize Vector (for consistency in speed independent of direction) + if (c.normalizedSpeed) { + this.dir = this.dir.normalized(); + } + // console.log(`x: ${this.dir.x}, y: ${this.dir.y}`); + } +} +exports.Ball = Ball; diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.ts b/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.ts new file mode 100644 index 00000000..bbbb30e5 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Rectangle.ts @@ -0,0 +1,142 @@ + +import { Vector, VectorInteger } from "./Vector.js"; +import { Component, Moving } from "./interface.js"; +import * as c from "../constants.js" + +export class Rectangle implements Component { + pos: VectorInteger; + width: number; + height: number; + constructor(pos: VectorInteger, width: number, height: number) { + this.pos = new VectorInteger(pos.x, pos.y); + this.width = width; + this.height = height; + } + collision(collider: Rectangle): boolean { + const thisLeft = this.pos.x; + const thisRight = this.pos.x + this.width; + const thisTop = this.pos.y; + const thisBottom = this.pos.y + this.height; + const colliderLeft = collider.pos.x; + const colliderRight = collider.pos.x + collider.width; + const colliderTop = collider.pos.y; + const colliderBottom = collider.pos.y + collider.height; + if ((thisBottom < colliderTop) + || (thisTop > colliderBottom) + || (thisRight < colliderLeft) + || (thisLeft > colliderRight)) { + return false; + } + else { + return true; + } + } +} + +export class MovingRectangle extends Rectangle implements Moving { + dir: Vector = new Vector(0,0); + speed: number; + readonly baseSpeed: number; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height); + this.baseSpeed = baseSpeed; + this.speed = baseSpeed; + } + move(delta: number) { // Math.floor WIP until VectorInteger debug + // console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`); + // this.pos.x += Math.floor(this.dir.x * this.speed * delta); + // this.pos.y += Math.floor(this.dir.y * this.speed * delta); + this.pos.x += this.dir.x * this.speed * delta; + this.pos.y += this.dir.y * this.speed * delta; + } + moveAndCollide(delta: number, colliderArr: Rectangle[]) { + this._moveAndCollideAlgo(delta, colliderArr); + } + protected _moveAndCollideAlgo(delta: number, colliderArr: Rectangle[]) { + let oldPos = new VectorInteger(this.pos.x, this.pos.y); + this.move(delta); + if (colliderArr.some(this.collision, this)) { + this.pos = oldPos; + } + } +} + +export class Racket extends MovingRectangle { + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height, baseSpeed); + } + moveAndCollide(delta: number, colliderArr: Rectangle[]) { + // let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug + this._moveAndCollideAlgo(delta, colliderArr); + // console.log(`y change: ${this.pos.y - oldPos.y}`); + } +} + +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); + } + protected _bounceAlgo(collider?: Rectangle) { + /* Could be more generic, but testing only Racket is enough, + because in Pong collider can only be Racket or Wall. */ + if (collider instanceof Racket) { + this._bounceRacket(collider); + } + else { + this._bounceWall(); + } + } + protected _bounceWall() { // Should be enough for Wall + this.dir.y = this.dir.y * -1; + } + protected _bounceRacket(racket: Racket) { + this._bounceRacketAlgo(racket); + } + protected _bounceRacketAlgo(racket: Racket) { + this.speed += this.speedIncrease; + + let x = this.dir.x * -1; + + const angleFactorDegree = 60; + const angleFactor = angleFactorDegree / 90; + const racketHalf = racket.height/2; + const ballMid = this.pos.y + this.height/2; + const racketMid = racket.pos.y + racketHalf; + + let impact = ballMid - racketMid; + const horizontalMargin = racketHalf * 0.15; + if (impact < horizontalMargin && impact > -horizontalMargin) { + impact = 0; + } + else if (impact > 0) { + impact = impact - horizontalMargin; + } + else if (impact < 0) { + impact = impact + horizontalMargin; + } + + let y = impact / (racketHalf - horizontalMargin) * angleFactor; + + this.dir.assign(x, y); + // Normalize Vector (for consistency in speed independent of direction) + if (c.normalizedSpeed) { + this.dir = this.dir.normalized(); + } + // console.log(`x: ${this.dir.x}, y: ${this.dir.y}`); + } +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.js b/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.js new file mode 100644 index 00000000..0b73c84c --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.js @@ -0,0 +1,45 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VectorInteger = exports.Vector = void 0; +class Vector { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + assign(x, y) { + this.x = x; + this.y = y; + } + normalized() { + const normalizationFactor = Math.abs(this.x) + Math.abs(this.y); + return new Vector(this.x / normalizationFactor, this.y / normalizationFactor); + } +} +exports.Vector = Vector; +class VectorInteger extends Vector { +} +exports.VectorInteger = VectorInteger; +/* +export class VectorInteger { + // private _x: number = 0; + // private _y: number = 0; + // constructor(x: number = 0, y: number = 0) { + // this._x = x; + // this._y = y; + // } + // get x(): number { + // return this._x; + // } + // set x(v: number) { + // // this._x = Math.floor(v); + // this._x = v; + // } + // get y(): number { + // return this._y; + // } + // set y(v: number) { + // // this._y = Math.floor(v); + // this._y = v; + // } +} +*/ diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.ts b/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.ts new file mode 100644 index 00000000..fbe121e5 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/Vector.ts @@ -0,0 +1,47 @@ + +export class Vector { + x: number; + y: number; + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + assign(x: number, y: number) { + this.x = x; + this.y = y; + } + normalized() : Vector { + const normalizationFactor = Math.abs(this.x) + Math.abs(this.y); + return new Vector(this.x/normalizationFactor, this.y/normalizationFactor); + } +} + +export class VectorInteger extends Vector { + // PLACEHOLDER + // VectorInteger with set/get dont work (No draw on the screen). Why ? +} + +/* +export class VectorInteger { + // private _x: number = 0; + // private _y: number = 0; + // constructor(x: number = 0, y: number = 0) { + // this._x = x; + // this._y = y; + // } + // get x(): number { + // return this._x; + // } + // set x(v: number) { + // // this._x = Math.floor(v); + // this._x = v; + // } + // get y(): number { + // return this._y; + // } + // set y(v: number) { + // // this._y = Math.floor(v); + // this._y = v; + // } +} +*/ diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/interface.js b/srcs/requirements/game_server/game_back/src/shared_js/class/interface.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/interface.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/srcs/requirements/game_server/game_back/src/shared_js/class/interface.ts b/srcs/requirements/game_server/game_back/src/shared_js/class/interface.ts new file mode 100644 index 00000000..544d54a8 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/class/interface.ts @@ -0,0 +1,19 @@ + +import { Vector, VectorInteger } from "./Vector.js"; + +export interface Component { + pos: VectorInteger; +} + +export interface GraphicComponent extends Component { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; +} + +export interface Moving { + dir: Vector; + speed: number; // pixel per second + move(delta: number): void; +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/constants.js b/srcs/requirements/game_server/game_back/src/shared_js/constants.js new file mode 100644 index 00000000..90e59565 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/constants.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.gameSessionIdPLACEHOLDER = exports.movingWallSpeed = exports.movingWallPosMax = exports.multiBallsCount = exports.newRoundDelay = exports.matchStartDelay = exports.normalizedSpeed = exports.ballSpeedIncrease = exports.ballSpeed = exports.racketSpeed = exports.wallSize = exports.ballSize = exports.ph = exports.pw = exports.h_mid = exports.w_mid = exports.h = exports.w = exports.CanvasRatio = exports.CanvasWidth = void 0; +exports.CanvasWidth = 1500; +exports.CanvasRatio = 1.66666; +/* ratio 5/3 (1.66) */ +exports.w = exports.CanvasWidth; +exports.h = exports.CanvasWidth / exports.CanvasRatio; +exports.w_mid = Math.floor(exports.w / 2); +exports.h_mid = Math.floor(exports.h / 2); +exports.pw = Math.floor(exports.w * 0.017); +exports.ph = exports.pw * 6; +exports.ballSize = exports.pw; +exports.wallSize = Math.floor(exports.w * 0.01); +exports.racketSpeed = Math.floor(exports.w * 0.66); // pixel per second +exports.ballSpeed = Math.floor(exports.w * 0.66); // pixel per second +exports.ballSpeedIncrease = Math.floor(exports.ballSpeed * 0.05); // pixel per second +exports.normalizedSpeed = false; // for consistency in speed independent of direction +exports.matchStartDelay = 3000; // millisecond +exports.newRoundDelay = 1500; // millisecond +// Game Variantes +exports.multiBallsCount = 3; +exports.movingWallPosMax = Math.floor(exports.w * 0.12); +exports.movingWallSpeed = Math.floor(exports.w * 0.08); +exports.gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER +// for testing, force gameSession.id in wsServer.ts->matchmaking() diff --git a/srcs/requirements/game_server/game_back/src/shared_js/constants.ts b/srcs/requirements/game_server/game_back/src/shared_js/constants.ts new file mode 100644 index 00000000..be86ba7b --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/constants.ts @@ -0,0 +1,30 @@ + +export const CanvasWidth = 1500; +export const CanvasRatio = 1.66666; +/* ratio 5/3 (1.66) */ + +export const w = CanvasWidth; +export const h = CanvasWidth / CanvasRatio; +export const w_mid = Math.floor(w/2); +export const h_mid = Math.floor(h/2); +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 ballSpeedIncrease = Math.floor(ballSpeed*0.05); // pixel per second + +export const normalizedSpeed = false; // for consistency in speed independent of direction + +export const matchStartDelay = 3000; // millisecond +export const newRoundDelay = 1500; // millisecond + +// Game Variantes +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() \ No newline at end of file diff --git a/srcs/requirements/game_server/game_back/src/shared_js/enums.js b/srcs/requirements/game_server/game_back/src/shared_js/enums.js new file mode 100644 index 00000000..dbf699a4 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/enums.js @@ -0,0 +1,46 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MatchOptions = exports.ClientRole = exports.PlayerSide = exports.InputEnum = exports.EventTypes = void 0; +var EventTypes; +(function (EventTypes) { + // Class Implemented + EventTypes[EventTypes["gameUpdate"] = 1] = "gameUpdate"; + EventTypes[EventTypes["scoreUpdate"] = 2] = "scoreUpdate"; + EventTypes[EventTypes["matchEnd"] = 3] = "matchEnd"; + EventTypes[EventTypes["assignId"] = 4] = "assignId"; + EventTypes[EventTypes["matchmakingComplete"] = 5] = "matchmakingComplete"; + // Generic + EventTypes[EventTypes["matchmakingInProgress"] = 6] = "matchmakingInProgress"; + EventTypes[EventTypes["matchStart"] = 7] = "matchStart"; + EventTypes[EventTypes["matchAbort"] = 8] = "matchAbort"; + EventTypes[EventTypes["matchNewRound"] = 9] = "matchNewRound"; + EventTypes[EventTypes["matchPause"] = 10] = "matchPause"; + EventTypes[EventTypes["matchResume"] = 11] = "matchResume"; + // Client + EventTypes[EventTypes["clientAnnounce"] = 12] = "clientAnnounce"; + EventTypes[EventTypes["clientPlayerReady"] = 13] = "clientPlayerReady"; + EventTypes[EventTypes["clientInput"] = 14] = "clientInput"; +})(EventTypes = exports.EventTypes || (exports.EventTypes = {})); +var InputEnum; +(function (InputEnum) { + InputEnum[InputEnum["noInput"] = 0] = "noInput"; + InputEnum[InputEnum["up"] = 1] = "up"; + InputEnum[InputEnum["down"] = 2] = "down"; +})(InputEnum = exports.InputEnum || (exports.InputEnum = {})); +var PlayerSide; +(function (PlayerSide) { + PlayerSide[PlayerSide["left"] = 1] = "left"; + PlayerSide[PlayerSide["right"] = 2] = "right"; +})(PlayerSide = exports.PlayerSide || (exports.PlayerSide = {})); +var ClientRole; +(function (ClientRole) { + ClientRole[ClientRole["player"] = 1] = "player"; + ClientRole[ClientRole["spectator"] = 2] = "spectator"; +})(ClientRole = exports.ClientRole || (exports.ClientRole = {})); +var MatchOptions; +(function (MatchOptions) { + // binary flags, can be mixed + MatchOptions[MatchOptions["noOption"] = 0] = "noOption"; + MatchOptions[MatchOptions["multiBalls"] = 1] = "multiBalls"; + MatchOptions[MatchOptions["movingWalls"] = 2] = "movingWalls"; +})(MatchOptions = exports.MatchOptions || (exports.MatchOptions = {})); diff --git a/srcs/requirements/game_server/game_back/src/shared_js/enums.ts b/srcs/requirements/game_server/game_back/src/shared_js/enums.ts new file mode 100644 index 00000000..108f3a66 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/enums.ts @@ -0,0 +1,46 @@ + +export enum EventTypes { + // Class Implemented + gameUpdate = 1, + scoreUpdate, + matchEnd, + assignId, + matchmakingComplete, + + // Generic + matchmakingInProgress, + matchStart, + matchAbort, + matchNewRound, // unused + matchPause, // unused + matchResume, // unused + + // Client + clientAnnounce, + clientPlayerReady, + clientInput, + +} + +export enum InputEnum { + noInput = 0, + up = 1, + down, +} + +export enum PlayerSide { + left = 1, + right +} + +export enum ClientRole { + player = 1, + spectator +} + +export enum MatchOptions { + // binary flags, can be mixed + noOption = 0b0, + multiBalls = 1 << 0, + movingWalls = 1 << 1 +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/utils.js b/srcs/requirements/game_server/game_back/src/shared_js/utils.js new file mode 100644 index 00000000..4e8714c3 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/utils.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.assertMovingRectangle = exports.clamp = exports.sleep = exports.random = void 0; +function random(min = 0, max = 1) { + return Math.random() * (max - min) + min; +} +exports.random = random; +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +exports.sleep = sleep; +function clamp(n, min, max) { + if (n < min) + n = min; + else if (n > max) + n = max; + return (n); +} +exports.clamp = clamp; +// Typescript hack, unused +function assertMovingRectangle(value) { + // if (value !== MovingRectangle) throw new Error("Not a MovingRectangle"); + return; +} +exports.assertMovingRectangle = assertMovingRectangle; diff --git a/srcs/requirements/game_server/game_back/src/shared_js/utils.ts b/srcs/requirements/game_server/game_back/src/shared_js/utils.ts new file mode 100644 index 00000000..6cba06d2 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/utils.ts @@ -0,0 +1,25 @@ + +import { 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; +} diff --git a/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.js b/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.js new file mode 100644 index 00000000..12782b1a --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.js @@ -0,0 +1,40 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.wallsMovements = void 0; +const c = __importStar(require("./constants.js")); +function wallsMovements(delta, gc) { + const wallTop = gc.wallTop; + const wallBottom = gc.wallBottom; + if (wallTop.pos.y <= 0 || wallTop.pos.y >= c.movingWallPosMax) { + wallTop.dir.y *= -1; + } + if (wallBottom.pos.y >= c.h - c.wallSize || wallBottom.pos.y <= c.h - c.movingWallPosMax) { + wallBottom.dir.y *= -1; + } + wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); + wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); +} +exports.wallsMovements = wallsMovements; diff --git a/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.ts b/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.ts new file mode 100644 index 00000000..0f6dbe58 --- /dev/null +++ b/srcs/requirements/game_server/game_back/src/shared_js/wallsMovement.ts @@ -0,0 +1,18 @@ + +import * as c from "./constants.js"; +import { MovingRectangle } from "../shared_js/class/Rectangle.js"; +import { GameComponents } from "./class/GameComponents.js"; + +export function wallsMovements(delta: number, gc: GameComponents) +{ + const wallTop = gc.wallTop; + const wallBottom = gc.wallBottom; + if (wallTop.pos.y <= 0 || wallTop.pos.y >= c.movingWallPosMax) { + wallTop.dir.y *= -1; + } + if (wallBottom.pos.y >= c.h-c.wallSize || wallBottom.pos.y <= c.h-c.movingWallPosMax) { + wallBottom.dir.y *= -1; + } + wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); + wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); +} diff --git a/srcs/requirements/game_server/game_back/tsconfig.json b/srcs/requirements/game_server/game_back/tsconfig.json new file mode 100644 index 00000000..50b7af31 --- /dev/null +++ b/srcs/requirements/game_server/game_back/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + "strictNullChecks": false, + /* 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 ''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. */ + // "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. */ + } +} diff --git a/srcs/requirements/nginx/conf/default.conf b/srcs/requirements/nginx/conf/default.conf index bbaaa8c5..ea872059 100644 --- a/srcs/requirements/nginx/conf/default.conf +++ b/srcs/requirements/nginx/conf/default.conf @@ -1,3 +1,7 @@ +upstream pong { + server http://game_server:8042/pong; +} + server { listen 8080 default_server; listen [::]:8080 default_server; @@ -17,6 +21,13 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://frontend_dev:8080; } + location /pong { + proxy_pass http://pong; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } } server {