diff --git a/package-lock.json b/package-lock.json index f8a273e7..807c07eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,14 @@ "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.8.4" } }, @@ -15,6 +21,21 @@ "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.8.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", @@ -27,6 +48,34 @@ "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": { @@ -36,11 +85,37 @@ "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.8.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "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/package.json b/package.json index 2d65b5c7..f8f23534 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,12 @@ "type": "module", "devDependencies": { "@types/node": "^18.11.5", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.5.3", "typescript": "^4.8.4" + }, + "dependencies": { + "uuid": "^9.0.0", + "ws": "^8.10.0" } } diff --git a/src/client/class/GameArea.ts b/src/client/class/GameArea.ts index fc775c58..8a1e6ce4 100644 --- a/src/client/class/GameArea.ts +++ b/src/client/class/GameArea.ts @@ -25,8 +25,9 @@ class GameArea { deleteKey(key: string) { key = key.toLowerCase(); var i = this.keys.indexOf(key); - if (i != -1) + if (i != -1) { this.keys.splice(i, 1); + } } clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); diff --git a/src/client/class/Rectangle.ts b/src/client/class/Rectangle.ts index 44b532ef..2fe93e90 100644 --- a/src/client/class/Rectangle.ts +++ b/src/client/class/Rectangle.ts @@ -55,7 +55,7 @@ class MovingRectangle extends Rectangle implements Moving { this.speed = baseSpeed; } move(delta: number) { // Math.floor WIP until VectorInteger debug - console.log("delta: "+ delta); + // console.log("delta: "+ delta); // console.log("speed: "+ this.speed); // console.log("speed*delta: "+ this.speed * delta); this.pos.x += Math.floor(this.dir.x * this.speed * delta); diff --git a/src/client/pong.html b/src/client/pong.html index 9da09617..0e443c3b 100644 --- a/src/client/pong.html +++ b/src/client/pong.html @@ -24,7 +24,7 @@ } #canvas-container { text-align: center; - /* border: dashed red 5px; */ + border: dashed red 5px; /* max-height: 80vh; */ /* overflow: hidden; */ } @@ -32,7 +32,7 @@ background-color: #333333; max-width: 75vw; /* max-height: 100vh; */ - /* width: 80%; */ + width: 80%; } diff --git a/src/client/pong.ts b/src/client/pong.ts index 71da8e91..4bbb2e7d 100644 --- a/src/client/pong.ts +++ b/src/client/pong.ts @@ -6,6 +6,8 @@ import {TextElem, TextNumericValue} from "./class/Text.js"; import * as d from "./draw.js"; import {gameLoop, newRound} from "./gameLoop.js" import {random} from "./utils.js"; +import {socket} from "./ws.js"; + /* Keys Player 1: W/S @@ -31,10 +33,56 @@ export let h_grid_mid: Rectangle; export let h_grid_u1: Rectangle; export let h_grid_d1: Rectangle; +function init() +{ + initGame(); + initGameClientOnly(); + console.log("socket state %i", socket.readyState); +} + function startGame() { - pong = new GameArea(); + // Start + d.drawInit(); + window.addEventListener('keydown', function (e) { + pong.addKey(e.key); + }); + window.addEventListener('keyup', function (e) { + pong.deleteKey(e.key); + }); + pong.interval = window.setInterval(gameLoop, 15); // min interval on Firefox seems to be 15. Chrome can go lower. + setTimeout(newRound, 1000); +} +function initGameClientOnly() +{ + let pos = new VectorInteger; + + // Const + const w = pong.canvas.width; + const h = pong.canvas.height; + const w_mid = Math.floor(w/2); + const h_mid = Math.floor(h/2); + const gridSize = Math.floor(w/500); + + // Grid + pos.assign(0, h_mid); + w_grid_mid = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); + pos.assign(0, h/4); + w_grid_u1 = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); + pos.assign(0, h-h/4); + w_grid_d1 = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); + pos.assign(w_mid, 0); + h_grid_mid = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); + pos.assign(w/4, 0); + h_grid_u1 = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); + pos.assign(w-w/4, 0); + h_grid_d1 = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); +} + +function initGame() +{ + pong = new GameArea(); // Const const w = pong.canvas.width; const h = pong.canvas.height; @@ -46,7 +94,6 @@ function startGame() const scoreSize = Math.floor(w/16); const midLineSize = Math.floor(w/150); const wallSize = Math.floor(w/100); - const gridSize = Math.floor(w/500); const playerSpeed = Math.floor(w/1.5); // pixel per second const ballSpeed = Math.floor(w/1.5); // pixel per second @@ -75,34 +122,11 @@ function startGame() pos.assign(w_mid-midLineSize/2, 0+wallSize); midLine = new Line(pong.ctx, pos, "white", midLineSize, h-wallSize*2, 15); - - // Grid - pos.assign(0, h_mid); - w_grid_mid = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); - pos.assign(0, h/4); - w_grid_u1 = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); - pos.assign(0, h-h/4); - w_grid_d1 = new Rectangle(pong.ctx, pos, "darkgreen", w, gridSize); - pos.assign(w_mid, 0); - h_grid_mid = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); - pos.assign(w/4, 0); - h_grid_u1 = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); - pos.assign(w-w/4, 0); - h_grid_d1 = new Rectangle(pong.ctx, pos, "darkgreen", gridSize, h); - - // Start - d.drawInit(); - window.addEventListener('keydown', function (e) { - pong.addKey(e.key); - }); - window.addEventListener('keyup', function (e) { - pong.deleteKey(e.key); - }); - pong.interval = window.setInterval(gameLoop, 15); // min interval on Firefox seems to be 15. Chrome can go lower. - setTimeout(newRound, 1000); } ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// -startGame(); +init(); + +export {startGame} diff --git a/src/client/utils.ts b/src/client/utils.ts index da00fd74..b1746e9e 100644 --- a/src/client/utils.ts +++ b/src/client/utils.ts @@ -3,4 +3,8 @@ function random(min: number = 0, max: number = 1) { return Math.random() * (max - min) + min; } -export {random} +function sleep (ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export {random, sleep} diff --git a/src/client/ws.ts b/src/client/ws.ts new file mode 100644 index 00000000..71f53ef4 --- /dev/null +++ b/src/client/ws.ts @@ -0,0 +1,81 @@ + +import * as g from "./pong.js" +import {startGame} from "./pong.js"; + +const wsPort = 8042; +const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; +const socket = new WebSocket(wsUrl, "json"); + +enum EventTypes { + gameUpdate = 1, + start, + pause, + resume +} + +interface EventData { + type: EventTypes; +} + +class EventGameUpdate implements EventData { + type: EventTypes = EventTypes.gameUpdate; + player1 = {y: 0}; + player2 = {y: 0}; + ball = {x: 0, y: 0, speed: 0}; +} + +socket.addEventListener("message", logListener); +socket.addEventListener("message", matchmakingListener); + + +function logListener(event: MessageEvent) { + console.log("data: " + event.data + " | [" + Date.now() + "]"); +} + +function matchmakingListener(event: MessageEvent) +{ + console.log("matchmakingListener"); + const data: EventData = JSON.parse(event.data); + if (data.type == EventTypes.start) + { + console.log("Event type = start"); + socket.removeEventListener("message", matchmakingListener); + socket.addEventListener("message", inGameListener); + startGame(); + } +} + +function inGameListener(event: MessageEvent) +{ + console.log("inGameListener"); + const data: EventData = JSON.parse(event.data); + switch (data.type) { + case EventTypes.gameUpdate: + console.log("Event type = gameUpdate"); + serverGameUpdate(data as EventGameUpdate); + break; + case EventTypes.pause: + console.log("Event type = pause"); + break; + case EventTypes.resume: + console.log("Event type = resume"); + break; + } +} + +function serverGameUpdate(data: EventGameUpdate) +{ + g.player1.clear(); + g.player1.pos.y = Math.floor(data.player1.y); + g.player1.update(); + + g.player2.clear(); + g.player2.pos.y = Math.floor(data.player2.y); + g.player2.update(); +} + +// socket.addEventListener("open", (event) => { +// console.log("socket state %i", socket.readyState); +// }); + +export {socket} diff --git a/src/server/server.ts b/src/server/server.ts index e73c5486..bb8585e5 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,17 +1,16 @@ -// var http = require("http"); -// var url = require("url"); -// var fs = require("fs"); -// var path = require("path"); import http from "http"; import url from "url"; import fs from "fs"; import path from "path"; - +import { WebSocketServer, WebSocket } from "ws"; +import { v4 as uuidv4 } from 'uuid'; +import { random } from "../client/utils.js"; const hostname = "localhost"; const port = 8080; -const root = "/mnt/c/Users/Lucky/Desktop/code/ft_transcendence/www/"; +const wsPort = 8042; // pas indispensable d'avoir un autre port si le WebSocket est limité à certaines routes +const root = "../../www/"; const server = http.createServer((req, res) => { // var q = new URL(req.url, `http://${req.getHeaders().host}`) @@ -36,4 +35,79 @@ const server = http.createServer((req, res) => { server.listen(port, hostname, () => { console.log(`Pong running at http://${hostname}:${port}/pong.html`); -}); \ No newline at end of file +}); + + +const wsServer = new WebSocketServer({port: wsPort, path: "/pong"}); + +class Client { + socket: WebSocket; + id: string; + isAlive: boolean = true; + constructor(socket: WebSocket, id: string) { + this.socket = socket; + this.id = id; + } +} + +const clientsArr: Client[] = []; + + +wsServer.on("connection", (socket, request) => { + + const id = uuidv4(); + const client = new Client(socket, id); + clientsArr.push(client); + socket.on("pong", function heartbeat() { + client.isAlive = true; + console.log("client %s alive at %i", client.id, Date.now()); + }); + + socket.on("message", function message(data) { + console.log("received: %s", data); + }); + + socket.send(JSON.stringify({type: 2})); // start + // socket.send("connection success, bravo client " + id); + // socket.send("start"); + // socket.send("json/20"); +}); + +function deleteClient(client: Client) +{ + var i = clientsArr.indexOf(client); + if (i != -1) { + clientsArr.splice(i, 1); + } +} + +const pingInterval = setInterval( () => { + clientsArr.forEach( (client) => { + if (client.isAlive === false) { + client.socket.terminate(); + console.log("client %s is no more at %i :'(", client.id, Date.now()); + deleteClient(client); + return; + } + client.isAlive = false; + client.socket.ping(); + }); +}, 5000); + +const gameUpdateInterval = setInterval( () => { + clientsArr.forEach( (client) => { + const update = { + type: 1, + player1: {y: random(50, 650)}, + player2: {y: random(50, 650)}, + ball: {x: 0, y: 0, speed: 0} + }; + client.socket.send(JSON.stringify(update)); + }); +}, 500); + + +wsServer.on('close', () => { + clearInterval(pingInterval); + clearInterval(gameUpdateInterval); +});