From 48665cfe30b7aa4ae5eefb38bac6a19aeb7393c9 Mon Sep 17 00:00:00 2001 From: LuckyLaszlo Date: Sun, 20 Nov 2022 15:46:45 +0100 Subject: [PATCH] =?UTF-8?q?WIP,=20tout=20est=20en=20chantier,=20tr=C3=A8s?= =?UTF-8?q?=20content=20:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/class/GameComponentsClient.ts | 51 ++++++++ src/client/draw.ts | 32 ++--- src/client/gameLoop.ts | 32 ++--- src/client/global.ts | 6 +- src/client/handleInput.ts | 25 ++-- src/client/initGameClientOnly.ts | 48 ------- src/client/pong.ts | 26 ++-- src/client/utils.ts | 15 +++ src/client/ws.ts | 97 ++++++++------ src/server/class/Client.ts | 32 +++++ src/server/class/GameComponentsServer.ts | 17 +++ src/server/class/GameSession.ts | 30 +++++ src/server/constants.ts | 2 + src/server/gameUpdate.ts | 26 ++++ src/server/server.ts | 95 +------------- src/server/wsServer.ts | 156 +++++++++++++++++++++++ src/shared_js/class/Event.ts | 71 ++++++++--- src/shared_js/class/GameComponents.ts | 33 +++++ src/shared_js/class/Rectangle.ts | 14 +- src/shared_js/enums.ts | 37 ++++++ src/shared_js/initGame.ts | 31 ----- tsconfig.json | 6 +- 22 files changed, 580 insertions(+), 302 deletions(-) create mode 100644 src/client/class/GameComponentsClient.ts delete mode 100644 src/client/initGameClientOnly.ts create mode 100644 src/client/utils.ts create mode 100644 src/server/class/Client.ts create mode 100644 src/server/class/GameComponentsServer.ts create mode 100644 src/server/class/GameSession.ts create mode 100644 src/server/constants.ts create mode 100644 src/server/gameUpdate.ts create mode 100644 src/server/wsServer.ts create mode 100644 src/shared_js/class/GameComponents.ts create mode 100644 src/shared_js/enums.ts delete mode 100644 src/shared_js/initGame.ts diff --git a/src/client/class/GameComponentsClient.ts b/src/client/class/GameComponentsClient.ts new file mode 100644 index 00000000..83143c06 --- /dev/null +++ b/src/client/class/GameComponentsClient.ts @@ -0,0 +1,51 @@ + +import * as c from "../constants.js" +import {Vector, VectorInteger} from "../../shared_js/class/Vector.js"; +import {Rectangle, Line} from "../../shared_js/class/Rectangle.js"; +import {TextElem, TextNumericValue} from "./Text.js"; +import { GameComponents } from "../../shared_js/class/GameComponents.js"; + +class GameComponentsClient extends GameComponents { + midLine: Line; + score1: TextNumericValue; + score2: TextNumericValue; + + w_grid_mid: Rectangle; + w_grid_u1: Rectangle; + w_grid_d1: Rectangle; + h_grid_mid: Rectangle; + h_grid_u1: Rectangle; + h_grid_d1: Rectangle; + constructor(ctx: CanvasRenderingContext2D) + { + super(ctx); + let pos = new VectorInteger; + // Scores + pos.assign(c.w_mid-c.scoreSize*1.6, c.scoreSize*1.5); + this.score1 = new TextNumericValue(ctx, pos, "white", c.scoreSize); + pos.assign(c.w_mid+c.scoreSize*1.1, c.scoreSize*1.5); + this.score2 = new TextNumericValue(ctx, pos, "white", c.scoreSize); + this.score1.value = 0; + this.score2.value = 0; + + // Dotted Midline + pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize); + this.midLine = new Line(ctx, pos, "white", c.midLineSize, c.h-c.wallSize*2, 15); + + // Grid + pos.assign(0, c.h_mid); + this.w_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + pos.assign(0, c.h/4); + this.w_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + pos.assign(0, c.h-c.h/4); + this.w_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + pos.assign(c.w_mid, 0); + this.h_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + pos.assign(c.w/4, 0); + this.h_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + pos.assign(c.w-c.w/4, 0); + this.h_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + } +} + +export {GameComponentsClient} diff --git a/src/client/draw.ts b/src/client/draw.ts index 65f2447d..ac0c94ce 100644 --- a/src/client/draw.ts +++ b/src/client/draw.ts @@ -1,4 +1,4 @@ -import * as g from "./global.js" +import {pong, gc, clientInfo} from "./global.js" import {gridDisplay} from "./handleInput.js"; function draw() @@ -6,35 +6,35 @@ function draw() if (gridDisplay) { drawGrid(); } - g.midLine.update(); - g.score1.update(); - g.score2.update(); + gc.midLine.update(); + gc.score1.update(); + gc.score2.update(); } function drawStatic() { - g.wall_top.update(); - g.wall_bottom.update(); - g.midLine.update(); + gc.wallTop.update(); + gc.wallBottom.update(); + gc.midLine.update(); } function drawInit() { - g.pong.clear(); + pong.clear(); drawStatic(); - g.player1.update(); - g.player2.update(); + gc.playerLeft.update(); + gc.playerRight.update(); } function drawGrid() { - g.w_grid_mid.update(); - g.w_grid_u1.update(); - g.w_grid_d1.update(); + gc.w_grid_mid.update(); + gc.w_grid_u1.update(); + gc.w_grid_d1.update(); - g.h_grid_mid.update(); - g.h_grid_u1.update(); - g.h_grid_d1.update(); + gc.h_grid_mid.update(); + gc.h_grid_u1.update(); + gc.h_grid_d1.update(); } export {draw, drawStatic, drawInit, drawGrid} diff --git a/src/client/gameLoop.ts b/src/client/gameLoop.ts index ae008272..a6ba7f54 100644 --- a/src/client/gameLoop.ts +++ b/src/client/gameLoop.ts @@ -1,6 +1,6 @@ -import * as g from "./global.js" +import {pong, gc, clientInfo} from "./global.js" import * as d from "./draw.js"; -import {random} from "../shared_js/utils.js"; +import {random} from "./utils.js"; import {handleInput} from "./handleInput.js"; let ballInPlay = false; @@ -23,17 +23,17 @@ function gameLoop() if (ballInPlay) { - g.ball.moveAndBounce(delta_time, [g.wall_top, g.wall_bottom, g.player1, g.player2]); - if (g.ball.pos.x > g.pong.canvas.width) { + gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]); + if (gc.ball.pos.x > pong.canvas.width) { ballInPlay = false; - g.score1.clear(); - ++g.score1.value; + gc.score1.clear(); + ++gc.score1.value; setTimeout(newRound, 1500); } - else if (g.ball.pos.x < 0 - g.ball.width) { + else if (gc.ball.pos.x < 0 - gc.ball.width) { ballInPlay = false; - g.score2.clear(); - ++g.score2.value; + gc.score2.clear(); + ++gc.score2.value; setTimeout(newRound, 1500); } } @@ -44,12 +44,12 @@ function gameLoop() function newRound() { // https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches - if (g.score1.value >= 11 - || g.score2.value >= 11) + if (gc.score1.value >= 11 + || gc.score2.value >= 11) { - if (Math.abs(g.score1.value - g.score2.value) >= 2) + if (Math.abs(gc.score1.value - gc.score2.value) >= 2) { - if (g.score1.value > g.score2.value) { + if (gc.score1.value > gc.score2.value) { alert("Player 1 WIN"); } else { @@ -58,9 +58,9 @@ function newRound() return; } } - g.ball.pos.x = Math.floor(g.pong.canvas.width/2); - g.ball.pos.y = Math.floor((g.pong.canvas.height * 0.1) + random() * (g.pong.canvas.height * 0.8)); - g.ball.speed = g.ball.baseSpeed; + gc.ball.pos.x = Math.floor(pong.canvas.width/2); + gc.ball.pos.y = Math.floor((pong.canvas.height * 0.1) + random() * (pong.canvas.height * 0.8)); + gc.ball.speed = gc.ball.baseSpeed; ballInPlay = true; } diff --git a/src/client/global.ts b/src/client/global.ts index 2141d189..eeea8765 100644 --- a/src/client/global.ts +++ b/src/client/global.ts @@ -1,3 +1,3 @@ -export * from "../shared_js/initGame.js" -export * from "./initGameClientOnly.js" -export {pong} from "./pong.js" + +export {pong, gc} from "./pong.js" +export {clientInfo} from "./ws.js" diff --git a/src/client/handleInput.ts b/src/client/handleInput.ts index e67f0cfa..1e8d55de 100644 --- a/src/client/handleInput.ts +++ b/src/client/handleInput.ts @@ -1,13 +1,14 @@ -import * as g from "./global.js" +import {pong, gc, clientInfo} from "./global.js" import * as d from "./draw.js"; import { socket } from "./ws.js"; -import {InputEnum, EventInput} from "../shared_js/class/Event.js" +import {InputEnum} from "../shared_js/enums.js" +import {EventInput} from "../shared_js/class/Event.js" let gridDisplay = false; function handleInput(delta: number) { - var keys = g.pong.keys; + var keys = pong.keys; if (keys.length == 0) return; @@ -15,34 +16,36 @@ function handleInput(delta: number) { if (gridDisplay) { - g.pong.clear(); + pong.clear(); d.drawStatic(); } gridDisplay = !gridDisplay; - g.pong.deleteKey("g"); + pong.deleteKey("g"); } playerMove(delta, keys); } function playerMove(delta: number, keys: string[]) { - g.player1.dir.y = 0; + // gc.playerLeft.dir.y = 0; if (keys.indexOf("w") != -1) { socket.send(JSON.stringify(new EventInput(InputEnum.up))); + // gc.playerLeft.dir.y += -1; } if (keys.indexOf("s") != -1) { socket.send(JSON.stringify(new EventInput(InputEnum.down))); + // gc.playerLeft.dir.y += 1; } - g.player1.moveAndCollide(delta, [g.wall_top, g.wall_bottom]); + // gc.playerLeft.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); - g.player2.dir.y = 0; + gc.playerRight.dir.y = 0; if (keys.indexOf("ArrowUp".toLowerCase()) != -1) { - g.player2.dir.y += -1; + gc.playerRight.dir.y += -1; } if (keys.indexOf("ArrowDown".toLowerCase()) != -1) { - g.player2.dir.y += 1; + gc.playerRight.dir.y += 1; } - g.player2.moveAndCollide(delta, [g.wall_top, g.wall_bottom]); + gc.playerRight.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); } export {handleInput, gridDisplay} diff --git a/src/client/initGameClientOnly.ts b/src/client/initGameClientOnly.ts deleted file mode 100644 index 160210bd..00000000 --- a/src/client/initGameClientOnly.ts +++ /dev/null @@ -1,48 +0,0 @@ - -import * as c from "./constants.js" -import {Vector, VectorInteger} from "../shared_js/class/Vector.js"; -import {Rectangle, MovingRectangle, Player, Ball, Line} from "../shared_js/class/Rectangle.js"; -import {TextElem, TextNumericValue} from "./class/Text.js"; - -export let midLine: Line; -export let score1: TextNumericValue; -export let score2: TextNumericValue; - -export let w_grid_mid: Rectangle; -export let w_grid_u1: Rectangle; -export let w_grid_d1: Rectangle; -export let h_grid_mid: Rectangle; -export let h_grid_u1: Rectangle; -export let h_grid_d1: Rectangle; - -function initGameClientOnly(ctx: CanvasRenderingContext2D) -{ - let pos = new VectorInteger; - // Scores - pos.assign(c.w_mid-c.scoreSize*1.6, c.scoreSize*1.5); - score1 = new TextNumericValue(ctx, pos, "white", c.scoreSize); - pos.assign(c.w_mid+c.scoreSize*1.1, c.scoreSize*1.5); - score2 = new TextNumericValue(ctx, pos, "white", c.scoreSize); - score1.value = 0; - score2.value = 0; - - // Dotted Midline - pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize); - midLine = new Line(ctx, pos, "white", c.midLineSize, c.h-c.wallSize*2, 15); - - // Grid - pos.assign(0, c.h_mid); - w_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); - pos.assign(0, c.h/4); - w_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); - pos.assign(0, c.h-c.h/4); - w_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); - pos.assign(c.w_mid, 0); - h_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); - pos.assign(c.w/4, 0); - h_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); - pos.assign(c.w-c.w/4, 0); - h_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); -} - -export {initGameClientOnly} diff --git a/src/client/pong.ts b/src/client/pong.ts index 986d1522..14c834ea 100644 --- a/src/client/pong.ts +++ b/src/client/pong.ts @@ -2,24 +2,28 @@ import {GameArea} from "./class/GameArea.js"; import * as d from "./draw.js"; import {gameLoop, newRound} from "./gameLoop.js" -// import {random} from "../shared_js/utils.js"; -// import {socket} from "./ws.js"; // import * as c from "./constants.js" -import {initGame} from "../shared_js/initGame.js"; -import {initGameClientOnly} from "./initGameClientOnly.js"; +import { GameComponentsClient } from "./class/GameComponentsClient.js"; +import {countdown} from "./utils.js"; /* Keys - Player 1: W/S - Player 2: Up/Down + Racket 1: W/S + Racket 2: Up/Down Grid On-Off: G */ export const pong = new GameArea(); +export const gc = new GameComponentsClient(pong.ctx); -function init() +function matchmaking() { - initGame(pong.ctx); - initGameClientOnly(pong.ctx); + console.log("Searching an opponent..."); // PLACEHOLDER, todo draw +} + +function matchmakingComplete() +{ + console.log("Match Found !"); // PLACEHOLDER, TODO draw on canvas + countdown(3, startGame); } function startGame() @@ -39,6 +43,4 @@ function startGame() ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// -init(); - -export {startGame} +export {matchmaking, matchmakingComplete} diff --git a/src/client/utils.ts b/src/client/utils.ts new file mode 100644 index 00000000..58cad90f --- /dev/null +++ b/src/client/utils.ts @@ -0,0 +1,15 @@ + +export * from "../shared_js/utils.js" + +function countdown(count: number, callback?: () => void) +{ + console.log("countdown ", count); // PLACEHOLDER, TODO draw on canvas + if (count > 0) { + setTimeout(countdown, 1000, --count, callback); + } + else if (callback) { + callback(); + } +} + +export {countdown} diff --git a/src/client/ws.ts b/src/client/ws.ts index 51fdb61a..fa7ce9b3 100644 --- a/src/client/ws.ts +++ b/src/client/ws.ts @@ -1,64 +1,81 @@ -import * as g from "./global.js" // temp -import {startGame} from "./pong.js"; -import {EventTypes, EventData, EventGameUpdate} from "../shared_js/class/Event.js" +import {pong, gc} from "./global.js" +import * as ev from "../shared_js/class/Event.js" +import {matchmaking, matchmakingComplete} from "./pong.js"; +import * as en from "../shared_js/enums.js" +import { Racket } from "../shared_js/class/Rectangle.js"; const wsPort = 8042; const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; const socket = new WebSocket(wsUrl, "json"); -socket.addEventListener("message", logListener); -socket.addEventListener("message", matchmakingListener); - - -function logListener(event: MessageEvent) { - console.log("data: " + event.data + " | [" + Date.now() + "]"); +class ClientInfo { + id = ""; + side: en.PlayerSide; + racket: Racket; } -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(); +export const clientInfo = new ClientInfo(); + +socket.addEventListener("open", (event) => { + socket.send(JSON.stringify( new ev.ClientAnnounce(en.ClientRole.player, clientInfo.id) )); +}); + +socket.addEventListener("message", logListener); +socket.addEventListener("message", preMatchListener); + +function logListener(this: WebSocket, event: MessageEvent) { + console.log("%i: " + event.data, Date.now()); +} + +function preMatchListener(this: WebSocket, event: MessageEvent) { + const data: ev.ServerEvent = JSON.parse(event.data); + switch (data.type) { + case en.EventTypes.assignId: + clientInfo.id = (data).id; + break; + case en.EventTypes.matchmakingInProgress: + matchmaking(); + break; + case en.EventTypes.matchmakingComplete: + clientInfo.side = (data).side; + if (clientInfo.side === en.PlayerSide.left) { + clientInfo.racket = gc.playerLeft; + } + else if (clientInfo.side === en.PlayerSide.right) { + clientInfo.racket = gc.playerRight; + } + socket.removeEventListener("message", preMatchListener); + socket.addEventListener("message", inGameListener); + socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) )); + matchmakingComplete(); + break; } } function inGameListener(event: MessageEvent) { - console.log("inGameListener"); - const data: EventData = JSON.parse(event.data); + const data: ev.ServerEvent = JSON.parse(event.data); switch (data.type) { - case EventTypes.gameUpdate: - console.log("Event type = gameUpdate"); - serverGameUpdate(data as EventGameUpdate); + case en.EventTypes.gameUpdate: + console.log("gameUpdate"); + serverGameUpdate(data as ev.EventGameUpdate); break; - case EventTypes.pause: - console.log("Event type = pause"); - break; - case EventTypes.resume: - console.log("Event type = resume"); + case en.EventTypes.matchNewRound: + console.log("matchNewRound//WIP"); break; } } -function serverGameUpdate(data: EventGameUpdate) +function serverGameUpdate(data: ev.EventGameUpdate) { - g.player1.clear(); - g.player1.pos.y = Math.floor(data.player1.y); - g.player1.update(); + gc.playerLeft.clear(); + gc.playerLeft.pos.y = Math.floor(data.playerLeft.y); + gc.playerLeft.update(); - g.player2.clear(); - g.player2.pos.y = Math.floor(data.player2.y); - g.player2.update(); + gc.playerRight.clear(); + gc.playerRight.pos.y = Math.floor(data.playerRight.y); + gc.playerRight.update(); } -// socket.addEventListener("open", (event) => { -// console.log("socket state %i", socket.readyState); -// }); - export {socket} diff --git a/src/server/class/Client.ts b/src/server/class/Client.ts new file mode 100644 index 00000000..e6fcfd1d --- /dev/null +++ b/src/server/class/Client.ts @@ -0,0 +1,32 @@ + +import {WebSocket} from "ws"; +import {Racket} from "../../shared_js/class/Rectangle.js"; +import { GameSession } from "./GameSession.js"; + +class Client { + socket: WebSocket; + id: string; // Pas indispensable si "socket" a une copie de "id" + isAlive: boolean; + gameSession: GameSession; + constructor(socket: WebSocket, id: string) { + this.socket = socket; + this.id = id; + this.isAlive = true; + } +} + +class ClientPlayer extends Client { + racket: Racket; + constructor(socket: WebSocket, id: string, racket: Racket) { + super(socket, id); + this.racket = racket; + } +} + +class ClientSpectator extends Client { // Wip, unused + constructor(socket: WebSocket, id: string) { + super(socket, id); + } +} + +export {Client, ClientPlayer, ClientSpectator} diff --git a/src/server/class/GameComponentsServer.ts b/src/server/class/GameComponentsServer.ts new file mode 100644 index 00000000..0dd6c514 --- /dev/null +++ b/src/server/class/GameComponentsServer.ts @@ -0,0 +1,17 @@ + +import * as c from "../constants.js" +import { GameComponents } from "../../shared_js/class/GameComponents.js"; + +// Empty object replacement to the web-API (web-API useless on server-side) +class CanvasRenderingContext2D {} +const mockCTX = new CanvasRenderingContext2D(); + +class GameComponentsServer extends GameComponents { + constructor() + { + // @ts-ignore + super(mockCTX); + } +} + +export {GameComponentsServer} diff --git a/src/server/class/GameSession.ts b/src/server/class/GameSession.ts new file mode 100644 index 00000000..8ad29d57 --- /dev/null +++ b/src/server/class/GameSession.ts @@ -0,0 +1,30 @@ +import { ClientPlayer } from "./Client"; +import {gameUpdate} from "../gameUpdate.js" +import { GameComponentsServer } from "./GameComponentsServer"; + +// Empty object replacement to the web-API (web-API useless on server-side) + +class GameSession { + id: string; // url ? + playersMap: Map; + unreadyPlayersMap: Map; + updateInterval: NodeJS.Timer; + // gc: GameComponentsServer; + // updateInterval: NodeJS.Timer; + constructor(id: string) { + this.id = id; + this.playersMap = new Map(); + this.unreadyPlayersMap = new Map(); + // this.gc = new GameComponentsServer(); + } + start() { + this.updateInterval = setInterval( () => { + const update = gameUpdate(); + this.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(update)); + }); + }, 500); + } +} + +export {GameSession} diff --git a/src/server/constants.ts b/src/server/constants.ts new file mode 100644 index 00000000..12beeabc --- /dev/null +++ b/src/server/constants.ts @@ -0,0 +1,2 @@ + +export * from "../shared_js/constants.js" diff --git a/src/server/gameUpdate.ts b/src/server/gameUpdate.ts new file mode 100644 index 00000000..a7672922 --- /dev/null +++ b/src/server/gameUpdate.ts @@ -0,0 +1,26 @@ + +import {EventTypes} from "../shared_js/enums.js" +import {EventGameUpdate} from "../shared_js/class/Event.js" +import { random } from "../shared_js/utils.js"; // temp + +/* +import {Rectangle, MovingRectangle, Racket, Ball, Line} from "../shared_js/class/Rectangle.js"; +import { Vector } from "../shared_js/class/Vector.js"; +class CanvasRenderingContext2D {} // Empty object replacement to the web-API (web-API useless on server-side) +const mockCtx = new CanvasRenderingContext2D; + // @ts-ignore +const playerLeft = new Racket(mockCtx, new Vector(), "white", 1, 1, 1); + */ + +function gameUpdate() : EventGameUpdate +{ + const update: EventGameUpdate = { + type: EventTypes.gameUpdate, + playerLeft: {y: random(50, 650)}, + playerRight: {y: random(50, 650)}, + ball: {x: 0, y: 0, speed: 0} + }; + return update; +} + +export {gameUpdate} \ No newline at end of file diff --git a/src/server/server.ts b/src/server/server.ts index d3494696..5df88098 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -3,25 +3,13 @@ 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 "../shared_js/utils.js"; - -import {EventTypes, EventData, EventGameUpdate} from "../shared_js/class/Event.js" -import {Rectangle, MovingRectangle, Player, Ball, Line} from "../shared_js/class/Rectangle.js"; -import { Vector } from "../shared_js/class/Vector.js"; +import {wsServer} from "./wsServer.js"; wsServer; // no-op const hostname = "localhost"; const port = 8080; -const wsPort = 8042; // pas indispensable d'avoir un autre port si le WebSocket est limité à certaines routes const root = "../../www/"; -class CanvasRenderingContext2D {} // Empty object replacement to the web-API (web-API useless on server-side) -const mockCtx = new CanvasRenderingContext2D; - // @ts-ignore -const player1 = new Player(mockCtx, new Vector(), "white", 1, 1, 1); - const server = http.createServer((req, res) => { // var q = new URL(req.url, `http://${req.getHeaders().host}`) // @ts-ignore @@ -46,84 +34,3 @@ const server = http.createServer((req, res) => { server.listen(port, hostname, () => { console.log(`Pong running at http://${hostname}:${port}/pong.html`); }); - - -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 log(data) { - console.log("received: %s", data); - }); - - socket.on("message", clientInputListener); - socket.send(JSON.stringify({type: EventTypes.start})); - // socket.send("connection success, bravo client " + id); - // socket.send("start"); - // socket.send("json/20"); -}); - -function clientInputListener(data: string) -{ - return; -} - -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: EventTypes.gameUpdate, - 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); -}); diff --git a/src/server/wsServer.ts b/src/server/wsServer.ts new file mode 100644 index 00000000..ea7619e7 --- /dev/null +++ b/src/server/wsServer.ts @@ -0,0 +1,156 @@ + +import { WebSocketServer, WebSocket as BaseLibWebSocket } from "ws"; + +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} from "./class/Client.js" +import {GameSession} from "./class/GameSession.js" + +// pas indispensable d'avoir un autre port si le WebSocket est limité à certaines routes +// (et 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 (unique Client) +const gameSessionsMap: Map = new Map; // GameSession.id(url)/GameSession (duplicates 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("%i: client %s is alive", Date.now(), client.id); + }); + + socket.on("message", function log(data) { + 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.id + // TODO: spectator/player distinction with msg.type + + this.send(JSON.stringify( new ev.EventAssignId(this.id) )) + this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )) + matchmaking(this); + } + else { + console.log("Invalid ClientAnnounce"); + } + return; + } + catch (e) { + console.log("Invalid JSON"); + } + this.once("message", clientAnnounceListener); +} + +function matchmaking(socket: WebSocket) +{ + // TODO Actual Matchmaking + + // TODO: Only once + const id = uuidv4(); + const gameSession = new GameSession(id); + gameSessionsMap.set(id, gameSession); + + // TODO: Per player + const player: ClientPlayer = clientsMap.get(socket.id) as ClientPlayer; + player.gameSession = gameSession; + gameSession.playersMap.set(socket.id, player); + gameSession.unreadyPlayersMap.set(socket.id, player); + + socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) )); + socket.once("message", playerReadyConfirmationListener); + // socket.on("message", clientInputListener); + // setinterval gameloop + +} + +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.start(); + } + } + else { + console.log("Invalid PlayerReady confirmation"); + } + return; + } + catch (e) { + console.log("Invalid JSON"); + } + this.once("message", playerReadyConfirmationListener); +} + +function clientInputListener(this: WebSocket, data: string) +{ + try { + const input: ev.ClientEvent = JSON.parse(data); + if (input.type === en.EventTypes.clientInput) { + console.log("Valid EventInput"); + } + else { + console.log("Invalid EventInput"); + } + } + catch (e) { + console.log("Invalid JSON"); + } +} + +//////////// +//////////// + +const pingInterval = setInterval( () => { + clientsMap.forEach( (client, key, map) => { + if (client.isAlive === false) { + client.socket.terminate(); + map.delete(key); + console.log("%i: client %s is no more :'(", Date.now(), key); + return; + } + client.isAlive = false; + client.socket.ping(); + }); +}, 5000); + +function closeListener() +{ + clearInterval(pingInterval); + // clearInterval(gameUpdateInterval); // TODO: Per Game Session +} + +function errorListener(error: Error) +{ + console.log("Error: " + JSON.stringify(error)); +} diff --git a/src/shared_js/class/Event.ts b/src/shared_js/class/Event.ts index 2096ae7a..6b8bf2e4 100644 --- a/src/shared_js/class/Event.ts +++ b/src/shared_js/class/Event.ts @@ -1,37 +1,66 @@ -/* Server */ -enum EventTypes { - gameUpdate = 1, - start, - pause, - resume +import * as en from "../enums.js" + +/* From Server */ +class ServerEvent { + type: en.EventTypes; + constructor(type: en.EventTypes = 0) { + this.type = type; + } } -interface EventData { - type: EventTypes; +class EventAssignId extends ServerEvent { + id: string; + constructor(id: string) { + super(en.EventTypes.assignId); + this.id = id; + } } -class EventGameUpdate implements EventData { - type: EventTypes = EventTypes.gameUpdate; - player1 = {y: 0}; - player2 = {y: 0}; +class EventMatchmakingComplete extends ServerEvent { + side: en.PlayerSide; + constructor(side: en.PlayerSide) { + super(en.EventTypes.matchmakingComplete); + this.side = side; + } +} + +class EventGameUpdate extends ServerEvent { + playerLeft = {y: 0}; + playerRight = {y: 0}; ball = {x: 0, y: 0, speed: 0}; + constructor() { + super(en.EventTypes.gameUpdate); + } } -/* Client */ -enum InputEnum { - up = 1, - down + +/* From Client */ +class ClientEvent { + type: en.EventTypes; + constructor(type: en.EventTypes = 0) { + this.type = type; + } } -class EventInput { - input: InputEnum; - constructor(input: InputEnum = 0) { +class ClientAnnounce extends ClientEvent { + role: en.ClientRole; + id: string; + constructor(role: en.ClientRole, id: string = "") { + super(en.EventTypes.clientAnnounce); + this.role = role; + } +} + +class EventInput extends ClientEvent { + input: en.InputEnum; + constructor(input: en.InputEnum = 0) { + super(en.EventTypes.clientInput); this.input = input; } } export { - EventTypes, EventData, EventGameUpdate, - InputEnum, EventInput + ServerEvent, EventAssignId, EventMatchmakingComplete, EventGameUpdate, + ClientEvent, ClientAnnounce, EventInput } diff --git a/src/shared_js/class/GameComponents.ts b/src/shared_js/class/GameComponents.ts new file mode 100644 index 00000000..aa135e14 --- /dev/null +++ b/src/shared_js/class/GameComponents.ts @@ -0,0 +1,33 @@ + +import * as c from "../constants.js" +import {VectorInteger} from "./Vector.js"; +import {Rectangle, Racket, Ball} from "./Rectangle.js"; + + +class GameComponents { + wallTop: Rectangle; + wallBottom: Rectangle; + playerLeft: Racket; + playerRight: Racket; + ball: Ball; + constructor(ctx?: CanvasRenderingContext2D) + { + let pos = new VectorInteger; + + pos.assign(0, 0); + this.wallTop = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); + + pos.assign(0+c.pw, c.h_mid-c.ph/2); + this.playerLeft = new Racket(ctx, pos, "white", c.pw, c.ph, c.playerSpeed); + pos.assign(c.w-c.pw-c.pw, c.h_mid-c.ph/2); + this.playerRight = new Racket(ctx, pos, "white", c.pw, c.ph, c.playerSpeed); + + pos.assign(c.w_mid-c.ballSize/2, c.h_mid-c.ballSize/2); + this.ball = new Ball(ctx, pos, "white", c.ballSize, c.ballSpeed); + this.ball.dir.assign(-0.8, +0.2); + } +} + +export {GameComponents} diff --git a/src/shared_js/class/Rectangle.ts b/src/shared_js/class/Rectangle.ts index 9087c5a7..8220dca0 100644 --- a/src/shared_js/class/Rectangle.ts +++ b/src/shared_js/class/Rectangle.ts @@ -77,7 +77,7 @@ class MovingRectangle extends Rectangle implements Moving { } } -class Player extends MovingRectangle { +class Racket extends MovingRectangle { constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { super(ctx, pos, color, width, height, baseSpeed); } @@ -88,9 +88,9 @@ class Ball extends MovingRectangle { super(ctx, pos, color, size, size, baseSpeed); } bounce(collider?: Rectangle) { - /* Could be more generic, but testing only player is enough, - because in Pong collider can only be Player or Wall. */ - if (collider instanceof Player) { + /* 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._bouncePlayer(collider); } else { @@ -112,8 +112,8 @@ class Ball extends MovingRectangle { private _bounceWall() { // Should be enough for Wall this.dir.y = this.dir.y * -1; } - private _bouncePlayer(collider: Player) { // WIP - // Bounce for Player need to be more complexe than this + private _bouncePlayer(collider: Racket) { // WIP + // Bounce for Racket need to be more complexe than this this.speed += this.baseSpeed/20; this.dir.x = this.dir.x * -1; } @@ -154,7 +154,7 @@ class Line extends Rectangle { } } -export {Rectangle, MovingRectangle, Player, Ball, Line} +export {Rectangle, MovingRectangle, Racket, Ball, Line} // How to handle const export in initGame ? // example for class Rectangle diff --git a/src/shared_js/enums.ts b/src/shared_js/enums.ts new file mode 100644 index 00000000..882193f9 --- /dev/null +++ b/src/shared_js/enums.ts @@ -0,0 +1,37 @@ + +enum EventTypes { + // Class Implemented + gameUpdate = 1, + assignId, + matchmakingComplete, + + // Generic + matchmakingInProgress, + matchNewRound, // unused + matchStart, // unused + matchPause, // unused + matchResume, // unused + + // Client + clientAnnounce, + clientPlayerReady, + clientInput, + +} + +enum InputEnum { + up = 1, + down +} + +enum PlayerSide { + left = 1, + right +} + +enum ClientRole { + player = 1, + spectator +} + +export {EventTypes, InputEnum, PlayerSide, ClientRole} diff --git a/src/shared_js/initGame.ts b/src/shared_js/initGame.ts deleted file mode 100644 index 9ef5dac1..00000000 --- a/src/shared_js/initGame.ts +++ /dev/null @@ -1,31 +0,0 @@ - -import * as c from "./constants.js" -import {Vector, VectorInteger} from "./class/Vector.js"; -import {Rectangle, MovingRectangle, Player, Ball, Line} from "./class/Rectangle.js"; - -export let wall_top: Rectangle; -export let wall_bottom: Rectangle; -export let player1: Player; -export let player2: Player; -export let ball: Ball; - -function initGame(ctx: CanvasRenderingContext2D) -{ - let pos = new VectorInteger; - // Component - pos.assign(0, 0); - wall_top = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); - pos.assign(0, c.h-c.wallSize); - wall_bottom = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); - - pos.assign(0+c.pw, c.h_mid-c.ph/2); - player1 = new Player(ctx, pos, "white", c.pw, c.ph, c.playerSpeed); - pos.assign(c.w-c.pw-c.pw, c.h_mid-c.ph/2); - player2 = new Player(ctx, pos, "white", c.pw, c.ph, c.playerSpeed); - - pos.assign(c.w_mid-c.ballSize/2, c.h_mid-c.ballSize/2); - ball = new Ball(ctx, pos, "white", c.ballSize, c.ballSpeed); - ball.dir.assign(-0.8, +0.2); -} - -export {initGame} diff --git a/tsconfig.json b/tsconfig.json index 338cf3cc..988126ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,8 @@ // "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'. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.gc. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.gc. '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. */ @@ -82,7 +82,7 @@ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */