From add08c216f091de47ef64474516bdd2c8df1c4dc Mon Sep 17 00:00:00 2001 From: LuckyLaszlo Date: Mon, 21 Nov 2022 19:46:25 +0100 Subject: [PATCH] authoritative server OK + TODO actual matchmaking --- src/client/class/GameComponents.ts | 33 ++++++ src/client/class/GameComponentsClient.ts | 44 ++++---- src/client/class/RectangleClient.ts | 131 +++++++++++++++++++++ src/client/class/Text.ts | 14 ++- src/client/constants.ts | 3 + src/client/draw.ts | 22 ++-- src/client/gameLoop.ts | 46 +------- src/client/handleInput.ts | 23 ++-- src/client/pong.ts | 20 ++-- src/client/ws.ts | 48 ++++++-- src/server/class/GameComponentsServer.ts | 15 ++- src/server/class/GameSession.ts | 138 ++++++++++++++++++++--- src/server/constants.ts | 3 + src/server/wsServer.ts | 35 ++++-- src/shared_js/class/Event.ts | 23 +++- src/shared_js/class/GameComponents.ts | 21 ++-- src/shared_js/class/Rectangle.ts | 88 ++------------- src/shared_js/class/interface.ts | 11 +- src/shared_js/constants.ts | 3 + src/shared_js/enums.ts | 4 +- 20 files changed, 485 insertions(+), 240 deletions(-) create mode 100644 src/client/class/GameComponents.ts create mode 100644 src/client/class/RectangleClient.ts diff --git a/src/client/class/GameComponents.ts b/src/client/class/GameComponents.ts new file mode 100644 index 00000000..c820b260 --- /dev/null +++ b/src/client/class/GameComponents.ts @@ -0,0 +1,33 @@ + +import * as c from "../constants.js" +import {VectorInteger} from "../../shared_js/class/Vector.js"; +import {RectangleClient, RacketClient, BallClient} from "./RectangleClient.js"; + +class GameComponentsForClient { + wallTop: RectangleClient; + wallBottom: RectangleClient; + playerLeft: RacketClient; + playerRight: RacketClient; + ball: BallClient; + constructor(ctx: CanvasRenderingContext2D) + { + let pos = new VectorInteger; + + pos.assign(0, 0); + this.wallTop = new RectangleClient(pos, c.w, c.wallSize, ctx, "grey"); + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new RectangleClient(pos, c.w, c.wallSize, ctx, "grey"); + + pos.assign(0+c.pw, c.h_mid-c.ph/2); + this.playerLeft = new RacketClient(pos, c.pw, c.ph, c.playerSpeed, ctx, "white"); + pos.assign(c.w-c.pw-c.pw, c.h_mid-c.ph/2); + this.playerRight = new RacketClient(pos, c.pw, c.ph, c.playerSpeed, ctx, "white"); + + // pos.assign(c.w_mid-c.ballSize/2, c.h_mid-c.ballSize/2); // center the ball + pos.assign(-c.ballSize, -c.ballSize); // ball out =) + this.ball = new BallClient(pos, c.ballSize, c.ballSpeed, ctx, "white"); + this.ball.dir.assign(-0.8, +0.2); + } +} + +export {GameComponentsForClient} diff --git a/src/client/class/GameComponentsClient.ts b/src/client/class/GameComponentsClient.ts index 83143c06..0826554b 100644 --- a/src/client/class/GameComponentsClient.ts +++ b/src/client/class/GameComponentsClient.ts @@ -1,50 +1,50 @@ 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"; +import { GameComponentsForClient } from "./GameComponents.js"; +import { RectangleClient, Line } from "./RectangleClient.js"; -class GameComponentsClient extends GameComponents { +class GameComponentsClient extends GameComponentsForClient { midLine: Line; - score1: TextNumericValue; - score2: TextNumericValue; + scoreLeft: TextNumericValue; + scoreRight: 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; + w_grid_mid: RectangleClient; + w_grid_u1: RectangleClient; + w_grid_d1: RectangleClient; + h_grid_mid: RectangleClient; + h_grid_u1: RectangleClient; + h_grid_d1: RectangleClient; 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); + this.scoreLeft = new TextNumericValue(pos, c.scoreSize, ctx, "white"); 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; + this.scoreRight = new TextNumericValue(pos, c.scoreSize, ctx, "white"); + this.scoreLeft.value = 0; + this.scoreRight.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); + this.midLine = new Line(pos, c.midLineSize, c.h-c.wallSize*2, ctx, "white", 15); // Grid pos.assign(0, c.h_mid); - this.w_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + this.w_grid_mid = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); pos.assign(0, c.h/4); - this.w_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + this.w_grid_u1 = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); pos.assign(0, c.h-c.h/4); - this.w_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.w, c.gridSize); + this.w_grid_d1 = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); pos.assign(c.w_mid, 0); - this.h_grid_mid = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + this.h_grid_mid = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); pos.assign(c.w/4, 0); - this.h_grid_u1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + this.h_grid_u1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); pos.assign(c.w-c.w/4, 0); - this.h_grid_d1 = new Rectangle(ctx, pos, "darkgreen", c.gridSize, c.h); + this.h_grid_d1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); } } diff --git a/src/client/class/RectangleClient.ts b/src/client/class/RectangleClient.ts new file mode 100644 index 00000000..e0e83133 --- /dev/null +++ b/src/client/class/RectangleClient.ts @@ -0,0 +1,131 @@ + +import {Vector, VectorInteger} from "../../shared_js/class/Vector.js"; +import {Component, GraphicComponent, Moving} from "../../shared_js/class/interface.js"; +import { Rectangle, MovingRectangle, Racket, Ball } from "../../shared_js/class/Rectangle.js"; + +function updateRectangle(this: RectangleClient) { + this.ctx.fillStyle = this.color; + this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height); +} + +function clearRectangle(this: RectangleClient, pos?: VectorInteger) { + if (pos) + this.ctx.clearRect(pos.x, pos.y, this.width, this.height); + else + this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); +} + +class RectangleClient extends Rectangle implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } + // update() { + // this.ctx.fillStyle = this.color; + // this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height); + // } + // clear(pos?: VectorInteger) { + // if (pos) + // this.ctx.clearRect(pos.x, pos.y, this.width, this.height); + // else + // this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); + // } +} + +class MovingRectangleClient extends MovingRectangle implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height, baseSpeed); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } +} + +class RacketClient extends Racket implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height, baseSpeed); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } +} + +class BallClient extends Ball implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, size: number, baseSpeed: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, size, baseSpeed); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } +} + +function updateLine(this: Line) { + this.ctx.fillStyle = this.color; + let pos: VectorInteger = new VectorInteger; + let i = 0; + while (i < this.segmentCount) + { + // for Horizontal Line + // pos.y = this.pos.y; + // pos.x = this.pos.x + this.segmentWidth * i; + pos.x = this.pos.x; + pos.y = this.pos.y + this.segmentHeight * i; + this.ctx.fillRect(pos.x, pos.y, this.segmentWidth, this.segmentHeight); + i += 2; + } +} + +class Line extends RectangleClient { + gapeCount: number = 0; + segmentCount: number; + segmentWidth: number; + segmentHeight: number; + constructor(pos: VectorInteger, width: number, height: number, + ctx: CanvasRenderingContext2D, color: string, gapeCount?: number) + { + super(pos, width, height, ctx, color); + this.update = updateLine; + if (gapeCount) + this.gapeCount = gapeCount; + this.segmentCount = this.gapeCount * 2 + 1; + + this.segmentWidth = this.width; + this.segmentHeight = this.height / this.segmentCount; + + // for Horizontal Line + // this.segmentWidth = this.width / this.segmentCount; + // this.segmentHeight = this.height; + } +} + +export {RectangleClient, MovingRectangleClient, RacketClient, BallClient, Line} diff --git a/src/client/class/Text.ts b/src/client/class/Text.ts index a3588ac3..e6ac3708 100644 --- a/src/client/class/Text.ts +++ b/src/client/class/Text.ts @@ -10,11 +10,13 @@ class TextElem implements Component { size: number; font: string; text: string = ""; - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font: string = "Bit5x3") { - this.ctx = ctx; + constructor(pos: VectorInteger, size: number, + ctx: CanvasRenderingContext2D, color: string, font: string = "Bit5x3") + { this.pos = Object.assign({}, pos); - this.color = color; this.size = size; + this.ctx = ctx; + this.color = color; this.font = font; } update() { @@ -38,8 +40,10 @@ class TextElem implements Component { class TextNumericValue extends TextElem { private _value: number = 0; - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font?: string) { - super(ctx, pos, color, size, font); + constructor(pos: VectorInteger, size: number, + ctx: CanvasRenderingContext2D, color: string, font?: string) + { + super(pos, size, ctx, color, font); } get value() { return this._value; diff --git a/src/client/constants.ts b/src/client/constants.ts index b4aae131..df5c8e68 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -5,3 +5,6 @@ export * from "../shared_js/constants.js" export const midLineSize = Math.floor(w/150); export const scoreSize = Math.floor(w/16); export const gridSize = Math.floor(w/500); + +// min interval on Firefox seems to be 15. Chrome can go lower. +export const gameLoopIntervalMS = 15; // millisecond diff --git a/src/client/draw.ts b/src/client/draw.ts index ac0c94ce..716e5459 100644 --- a/src/client/draw.ts +++ b/src/client/draw.ts @@ -3,12 +3,18 @@ import {gridDisplay} from "./handleInput.js"; function draw() { + pong.clear(); + + drawStatic(); + if (gridDisplay) { drawGrid(); } - gc.midLine.update(); - gc.score1.update(); - gc.score2.update(); + gc.scoreLeft.update(); + gc.scoreRight.update(); + gc.playerLeft.update(); + gc.playerRight.update(); + gc.ball.update(); } function drawStatic() @@ -18,14 +24,6 @@ function drawStatic() gc.midLine.update(); } -function drawInit() -{ - pong.clear(); - drawStatic(); - gc.playerLeft.update(); - gc.playerRight.update(); -} - function drawGrid() { gc.w_grid_mid.update(); @@ -37,4 +35,4 @@ function drawGrid() gc.h_grid_d1.update(); } -export {draw, drawStatic, drawInit, drawGrid} +export {draw, drawStatic, drawGrid} diff --git a/src/client/gameLoop.ts b/src/client/gameLoop.ts index a6ba7f54..a6028ec3 100644 --- a/src/client/gameLoop.ts +++ b/src/client/gameLoop.ts @@ -1,9 +1,6 @@ -import {pong, gc, clientInfo} from "./global.js" import * as d from "./draw.js"; -import {random} from "./utils.js"; import {handleInput} from "./handleInput.js"; -let ballInPlay = false; let actual_time: number = Date.now(); let last_time: number; let delta_time: number; @@ -21,47 +18,10 @@ function gameLoop() handleInput(delta_time); - if (ballInPlay) - { - gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]); - if (gc.ball.pos.x > pong.canvas.width) { - ballInPlay = false; - gc.score1.clear(); - ++gc.score1.value; - setTimeout(newRound, 1500); - } - else if (gc.ball.pos.x < 0 - gc.ball.width) { - ballInPlay = false; - gc.score2.clear(); - ++gc.score2.value; - setTimeout(newRound, 1500); - } - } + // prediction + // gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]); d.draw(); } -function newRound() -{ - // https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches - if (gc.score1.value >= 11 - || gc.score2.value >= 11) - { - if (Math.abs(gc.score1.value - gc.score2.value) >= 2) - { - if (gc.score1.value > gc.score2.value) { - alert("Player 1 WIN"); - } - else { - alert("Player 2 WIN"); - } - return; - } - } - 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; -} - -export {gameLoop, newRound} +export {gameLoop} diff --git a/src/client/handleInput.ts b/src/client/handleInput.ts index 1e8d55de..9eef3dad 100644 --- a/src/client/handleInput.ts +++ b/src/client/handleInput.ts @@ -4,7 +4,7 @@ import { socket } from "./ws.js"; import {InputEnum} from "../shared_js/enums.js" import {EventInput} from "../shared_js/class/Event.js" -let gridDisplay = false; +export let gridDisplay = false; function handleInput(delta: number) { @@ -27,25 +27,24 @@ function handleInput(delta: number) function playerMove(delta: number, keys: string[]) { - // 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; } - // gc.playerLeft.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); - gc.playerRight.dir.y = 0; - if (keys.indexOf("ArrowUp".toLowerCase()) != -1) { - gc.playerRight.dir.y += -1; + // prediction +/* const racket = clientInfo.racket; + racket.dir.y = 0; + if (keys.indexOf("w") != -1) { + racket.dir.y += -1; } - if (keys.indexOf("ArrowDown".toLowerCase()) != -1) { - gc.playerRight.dir.y += 1; + if (keys.indexOf("s") != -1) { + racket.dir.y += 1; } - gc.playerRight.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); + racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); */ + } -export {handleInput, gridDisplay} +export {handleInput} diff --git a/src/client/pong.ts b/src/client/pong.ts index 14c834ea..2c9550d6 100644 --- a/src/client/pong.ts +++ b/src/client/pong.ts @@ -1,11 +1,13 @@ import {GameArea} from "./class/GameArea.js"; import * as d from "./draw.js"; -import {gameLoop, newRound} from "./gameLoop.js" -// import * as c from "./constants.js" +import {gameLoop} from "./gameLoop.js" +import * as c from "./constants.js" import { GameComponentsClient } from "./class/GameComponentsClient.js"; import {countdown} from "./utils.js"; +import {socket} from "./ws.js"; socket; // no-op + /* Keys Racket 1: W/S Racket 2: Up/Down @@ -23,24 +25,24 @@ function matchmaking() function matchmakingComplete() { console.log("Match Found !"); // PLACEHOLDER, TODO draw on canvas - countdown(3, startGame); } -function startGame() +function startGame() { + countdown(c.matchStartDelay/1000, resumeGame); +} + +function resumeGame() { - // 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); + pong.interval = window.setInterval(gameLoop, c.gameLoopIntervalMS); } ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// -export {matchmaking, matchmakingComplete} +export {matchmaking, matchmakingComplete, startGame} diff --git a/src/client/ws.ts b/src/client/ws.ts index fa7ce9b3..b299d12f 100644 --- a/src/client/ws.ts +++ b/src/client/ws.ts @@ -1,9 +1,10 @@ import {pong, gc} from "./global.js" import * as ev from "../shared_js/class/Event.js" -import {matchmaking, matchmakingComplete} from "./pong.js"; +import {matchmaking, matchmakingComplete, startGame} from "./pong.js"; import * as en from "../shared_js/enums.js" import { Racket } from "../shared_js/class/Rectangle.js"; +import { sleep } from "./utils.js"; const wsPort = 8042; const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; @@ -45,11 +46,14 @@ function preMatchListener(this: WebSocket, event: MessageEvent) { 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; + case en.EventTypes.matchStart: + socket.removeEventListener("message", preMatchListener); + socket.addEventListener("message", inGameListener); + startGame(); + break; } } @@ -59,23 +63,43 @@ function inGameListener(event: MessageEvent) switch (data.type) { case en.EventTypes.gameUpdate: console.log("gameUpdate"); - serverGameUpdate(data as ev.EventGameUpdate); + gameUpdate(data as ev.EventGameUpdate); break; - case en.EventTypes.matchNewRound: - console.log("matchNewRound//WIP"); + case en.EventTypes.scoreUpdate: + console.log("scoreUpdate"); + scoreUpdate(data as ev.EventScoreUpdate); + break; + case en.EventTypes.matchEnd: + console.log("matchEnd"); + matchEnd(data as ev.EventMatchEnd); break; } } -function serverGameUpdate(data: ev.EventGameUpdate) +async function gameUpdate(data: ev.EventGameUpdate) { - gc.playerLeft.clear(); + // await sleep(1000); // artificial latency for testing purpose gc.playerLeft.pos.y = Math.floor(data.playerLeft.y); - gc.playerLeft.update(); - - gc.playerRight.clear(); gc.playerRight.pos.y = Math.floor(data.playerRight.y); - gc.playerRight.update(); + gc.ball.pos.x = Math.floor(data.ball.x); + gc.ball.pos.y = Math.floor(data.ball.y); + gc.ball.speed = Math.floor(data.ball.speed); +} + +function scoreUpdate(data: ev.EventScoreUpdate) +{ + gc.scoreLeft.value = data.scoreLeft; + gc.scoreRight.value = data.scoreRight; +} + +function matchEnd(data: ev.EventMatchEnd) +{ + if (data.winner === clientInfo.side) { + alert("WIN"); // placeholder TODO draw + } + else { + alert("LOSE"); // placeholder TODO draw + } } export {socket} diff --git a/src/server/class/GameComponentsServer.ts b/src/server/class/GameComponentsServer.ts index 0dd6c514..84b185c4 100644 --- a/src/server/class/GameComponentsServer.ts +++ b/src/server/class/GameComponentsServer.ts @@ -2,15 +2,22 @@ import * as c from "../constants.js" import { GameComponents } from "../../shared_js/class/GameComponents.js"; +// DONT WORK AS EXPECTED. I might try again later. // Empty object replacement to the web-API (web-API useless on server-side) -class CanvasRenderingContext2D {} -const mockCTX = new CanvasRenderingContext2D(); +// class CanvasRenderingContext2D {} +// const mockCTX = new CanvasRenderingContext2D(); class GameComponentsServer extends GameComponents { + scoreLeft: number; + scoreRight: number; + ballInPlay: boolean; constructor() { - // @ts-ignore - super(mockCTX); + // super(mockCTX); + super(); + this.scoreLeft = 0; + this.scoreRight = 0; + this.ballInPlay = false; } } diff --git a/src/server/class/GameSession.ts b/src/server/class/GameSession.ts index 8ad29d57..aaf7b611 100644 --- a/src/server/class/GameSession.ts +++ b/src/server/class/GameSession.ts @@ -1,29 +1,141 @@ 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) +import { GameComponents } from "../../shared_js/class/GameComponents.js"; +import { clientInputListener } from "../wsServer.js"; +import * as c from "../constants.js" +import { GameComponentsServer } from "./GameComponentsServer.js"; +import { random } from "../../shared_js/utils.js"; +import * as en from "../../shared_js/enums.js" +import * as ev from "../../shared_js/class/Event.js" +/* + Arg "s: GameSession" replace "this: GameSession" for use with setTimeout(), + because "this" is equal to "this: Timeout" +*/ class GameSession { id: string; // url ? playersMap: Map; unreadyPlayersMap: Map; - updateInterval: NodeJS.Timer; - // gc: GameComponentsServer; - // updateInterval: NodeJS.Timer; + gameLoopInterval: NodeJS.Timer | number; + clientsUpdateInterval: NodeJS.Timer | number; + components: GameComponentsServer; + + actual_time: number; + last_time: number; + delta_time: number; + constructor(id: string) { this.id = id; this.playersMap = new Map(); this.unreadyPlayersMap = new Map(); - // this.gc = new GameComponentsServer(); + this.components = new GameComponentsServer(); } start() { - this.updateInterval = setInterval( () => { - const update = gameUpdate(); - this.playersMap.forEach( (client) => { - client.socket.send(JSON.stringify(update)); - }); - }, 500); + setTimeout(this.resume, c.matchStartDelay, this); + setTimeout(this._newRound, c.matchStartDelay + c.newRoundDelay, this); + } + resume(s: GameSession) { + s.playersMap.forEach( (client) => { + client.socket.on("message", clientInputListener); + }); + + s.actual_time = Date.now(); + s.gameLoopInterval = setInterval(s._gameLoop, c.gameLoopIntervalMS, s); + s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s); + } + pause(s: GameSession) { + s.playersMap.forEach( (client) => { + client.socket.off("message", clientInputListener); + }); + + clearInterval(s.gameLoopInterval); + clearInterval(s.clientsUpdateInterval); + } + handleInput(client: ClientPlayer, input: en.InputEnum) { + const gc = this.components; + client.racket.dir.y = 0; + if (input === en.InputEnum.up) { + client.racket.dir.y += -1; + } + else if (input === en.InputEnum.down) { + client.racket.dir.y += 1; + } + client.racket.moveAndCollide(this.delta_time, [gc.wallTop, gc.wallBottom]); + /* how to handle Delta time correctly in handleInput ? */ + } + 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; + + const gc = s.components; + if (gc.ballInPlay) + { + gc.ball.moveAndBounce(s.delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]); + if (gc.ball.pos.x > c.w) { + gc.ballInPlay = false; + ++gc.scoreLeft; + s.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight))); + }); + setTimeout(s._newRound, c.newRoundDelay, s); + } + else if (gc.ball.pos.x < 0 - gc.ball.width) { + gc.ballInPlay = false; + ++gc.scoreRight; + + s.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight))); + }); + setTimeout(s._newRound, c.newRoundDelay, s); + } + } + } + private _clientsUpdate(s: GameSession) { + const gc = s.components; + const update: ev.EventGameUpdate = { + type: en.EventTypes.gameUpdate, + playerLeft: {y: gc.playerLeft.pos.y}, + playerRight: {y: gc.playerRight.pos.y}, + ball: {x: gc.ball.pos.x, y: gc.ball.pos.y, speed: gc.ball.speed} + }; + s.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(update)); + }); + } + private _newRound(s: GameSession) { + const gc = s.components; + // https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches + if (gc.scoreLeft >= 11 + || gc.scoreRight >= 11) + { + if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2) + { + s._matchEnd(s); + return; + } + } + gc.ball.pos.x = c.w_mid; + gc.ball.pos.y = Math.floor((c.h * 0.1) + random() * (c.h * 0.8)); + gc.ball.speed = gc.ball.baseSpeed; + gc.ballInPlay = true; + } + private _matchEnd(s: GameSession) { + 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)); + }); } } diff --git a/src/server/constants.ts b/src/server/constants.ts index 12beeabc..974263ff 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,2 +1,5 @@ export * from "../shared_js/constants.js" + +export const gameLoopIntervalMS = 15; // millisecond +export const clientsUpdateIntervalMS = 100; // millisecond diff --git a/src/server/wsServer.ts b/src/server/wsServer.ts index ea7619e7..54e5eddf 100644 --- a/src/server/wsServer.ts +++ b/src/server/wsServer.ts @@ -13,18 +13,18 @@ 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; +// 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 (unique Client) -const gameSessionsMap: Map = new Map; // GameSession.id(url)/GameSession (duplicates GameSession) +const clientsMap: Map = new Map; // socket.id/Client +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(); @@ -37,13 +37,14 @@ function connectionListener(socket: WebSocket, request: IncomingMessage) console.log("%i: client %s is alive", Date.now(), client.id); }); - socket.on("message", function log(data) { + socket.on("message", function log(data: string) { console.log("data: " + data); }); socket.once("message", clientAnnounceListener); } + function clientAnnounceListener(this: WebSocket, data: string) { try { @@ -67,6 +68,7 @@ function clientAnnounceListener(this: WebSocket, data: string) this.once("message", clientAnnounceListener); } + function matchmaking(socket: WebSocket) { // TODO Actual Matchmaking @@ -82,13 +84,15 @@ function matchmaking(socket: WebSocket) gameSession.playersMap.set(socket.id, player); gameSession.unreadyPlayersMap.set(socket.id, player); + player.racket = gameSession.components.playerLeft; 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 { @@ -98,6 +102,9 @@ function playerReadyConfirmationListener(this: WebSocket, data: string) 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(); } } @@ -112,12 +119,15 @@ function playerReadyConfirmationListener(this: WebSocket, data: string) this.once("message", playerReadyConfirmationListener); } -function clientInputListener(this: WebSocket, data: string) + +export function clientInputListener(this: WebSocket, data: string) { try { - const input: ev.ClientEvent = JSON.parse(data); + // const input: ev.ClientEvent = JSON.parse(data); + const input: ev.EventInput = JSON.parse(data); if (input.type === en.EventTypes.clientInput) { - console.log("Valid EventInput"); + const client = clientsMap.get(this.id); + client.gameSession.handleInput(client as ClientPlayer, input.input); } else { console.log("Invalid EventInput"); @@ -144,12 +154,13 @@ const pingInterval = setInterval( () => { }); }, 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 6b8bf2e4..3d27ebb6 100644 --- a/src/shared_js/class/Event.ts +++ b/src/shared_js/class/Event.ts @@ -29,11 +29,28 @@ class EventGameUpdate extends ServerEvent { playerLeft = {y: 0}; playerRight = {y: 0}; ball = {x: 0, y: 0, speed: 0}; - constructor() { + constructor() { // TODO: constructor that take GameComponentsServer maybe ? super(en.EventTypes.gameUpdate); } } +class EventScoreUpdate extends ServerEvent { + scoreLeft: number; + scoreRight: number; + constructor(scoreLeft: number, scoreRight: number) { + super(en.EventTypes.scoreUpdate); + this.scoreLeft = scoreLeft; + this.scoreRight = scoreRight; + } +} + +class EventMatchEnd extends ServerEvent { + winner: en.PlayerSide; + constructor(winner: en.PlayerSide) { + super(en.EventTypes.matchEnd); + } +} + /* From Client */ class ClientEvent { @@ -49,6 +66,7 @@ class ClientAnnounce extends ClientEvent { constructor(role: en.ClientRole, id: string = "") { super(en.EventTypes.clientAnnounce); this.role = role; + this.id = id; } } @@ -61,6 +79,7 @@ class EventInput extends ClientEvent { } export { - ServerEvent, EventAssignId, EventMatchmakingComplete, EventGameUpdate, + ServerEvent, EventAssignId, EventMatchmakingComplete, + EventGameUpdate, EventScoreUpdate, EventMatchEnd, ClientEvent, ClientAnnounce, EventInput } diff --git a/src/shared_js/class/GameComponents.ts b/src/shared_js/class/GameComponents.ts index aa135e14..0c9309c2 100644 --- a/src/shared_js/class/GameComponents.ts +++ b/src/shared_js/class/GameComponents.ts @@ -1,31 +1,34 @@ - +/* + No more shared. Dont know how to implemente this. + For the moment, this code is only used by the server. +*/ 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) + constructor() { let pos = new VectorInteger; pos.assign(0, 0); - this.wallTop = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); + this.wallTop = new Rectangle(pos, c.w, c.wallSize); pos.assign(0, c.h-c.wallSize); - this.wallBottom = new Rectangle(ctx, pos, "grey", c.w, c.wallSize); + this.wallBottom = new Rectangle(pos, 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); + this.playerLeft = new Racket(pos, 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); + this.playerRight = new Racket(pos, 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); + // pos.assign(c.w_mid-c.ballSize/2, c.h_mid-c.ballSize/2); // center the ball + pos.assign(-c.ballSize, -c.ballSize); // ball out =) + this.ball = new Ball(pos, c.ballSize, c.ballSpeed); this.ball.dir.assign(-0.8, +0.2); } } diff --git a/src/shared_js/class/Rectangle.ts b/src/shared_js/class/Rectangle.ts index 8220dca0..5049403a 100644 --- a/src/shared_js/class/Rectangle.ts +++ b/src/shared_js/class/Rectangle.ts @@ -3,28 +3,14 @@ import {Vector, VectorInteger} from "./Vector.js"; import {Component, Moving} from "./interface.js"; class Rectangle implements Component { - ctx: CanvasRenderingContext2D; pos: VectorInteger; - color: string; width: number; height: number; - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number) { - this.ctx = ctx; + constructor(pos: VectorInteger, width: number, height: number) { this.pos = Object.assign({}, pos); - this.color = color; this.width = width; this.height = height; } - update() { - this.ctx.fillStyle = this.color; - this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height); - } - clear(pos?: VectorInteger) { - if (pos) - this.ctx.clearRect(pos.x, pos.y, this.width, this.height); - else - this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); - } collision(collider: Rectangle): boolean { // Collision WIP. To redo var myleft = this.pos.x; var myright = this.pos.x + (this.width); @@ -49,8 +35,8 @@ class MovingRectangle extends Rectangle implements Moving { dir: Vector = new Vector(0,0); speed: number; readonly baseSpeed: number; - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { - super(ctx, pos, color, width, height); + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height); this.baseSpeed = baseSpeed; this.speed = baseSpeed; } @@ -69,23 +55,18 @@ class MovingRectangle extends Rectangle implements Moving { this.pos.x = oldPos.x; this.pos.y = oldPos.y; } - else - { - this.clear(oldPos); - this.update(); - } } } class Racket extends MovingRectangle { - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { - super(ctx, pos, color, width, height, baseSpeed); + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height, baseSpeed); } } class Ball extends MovingRectangle { - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, baseSpeed: number) { - super(ctx, pos, color, size, size, baseSpeed); + constructor(pos: VectorInteger, size: number, baseSpeed: number) { + super(pos, size, size, baseSpeed); } bounce(collider?: Rectangle) { /* Could be more generic, but testing only Racket is enough, @@ -98,7 +79,6 @@ class Ball extends MovingRectangle { } } moveAndBounce(delta: number, colliderArr: Rectangle[]) { - let oldPos = Object.assign({}, this.pos); this.move(delta); let i = colliderArr.findIndex(this.collision, this); if (i != -1) @@ -106,8 +86,6 @@ class Ball extends MovingRectangle { this.bounce(colliderArr[i]); this.move(delta); } - this.clear(oldPos); - this.update(); } private _bounceWall() { // Should be enough for Wall this.dir.y = this.dir.y * -1; @@ -119,54 +97,4 @@ class Ball extends MovingRectangle { } } -class Line extends Rectangle { - gapeCount: number = 0; - segmentCount: number; - segmentWidth: number; - segmentHeight: number; - constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, gapeCount?: number) { - super(ctx, pos, color, width, height); - if (gapeCount) - this.gapeCount = gapeCount; - this.segmentCount = this.gapeCount * 2 + 1; - - this.segmentWidth = this.width; - this.segmentHeight = this.height / this.segmentCount; - - // for Horizontal Line - // this.segmentWidth = this.width / this.segmentCount; - // this.segmentHeight = this.height; - } - update() { - this.ctx.fillStyle = this.color; - let pos: VectorInteger = new VectorInteger; - let i = 0; - while (i < this.segmentCount) - { - // for Horizontal Line - // pos.y = this.pos.y; - // pos.x = this.pos.x + this.segmentWidth * i; - pos.x = this.pos.x; - pos.y = this.pos.y + this.segmentHeight * i; - this.ctx.fillRect(pos.x, pos.y, this.segmentWidth, this.segmentHeight); - i += 2; - } - } -} - -export {Rectangle, MovingRectangle, Racket, Ball, Line} - -// How to handle const export in initGame ? -// example for class Rectangle -/* constructor(ctx?: CanvasRenderingContext2D, pos?: VectorInteger, color?: string, width?: number, height?: number) { - if (ctx && pos && color && width && height) - this.init(ctx, pos, color, width, height); -} -// constructor() {} -init(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number) { - this.ctx = ctx; - this.pos = Object.assign({}, pos); - this.color = color; - this.width = width; - this.height = height; -} */ +export {Rectangle, MovingRectangle, Racket, Ball} diff --git a/src/shared_js/class/interface.ts b/src/shared_js/class/interface.ts index 3670d48d..5fdb8315 100644 --- a/src/shared_js/class/interface.ts +++ b/src/shared_js/class/interface.ts @@ -3,10 +3,13 @@ import {Vector, VectorInteger} from "./Vector.js"; interface Component { pos: VectorInteger; - color: string; +} + +interface GraphicComponent extends Component { ctx: CanvasRenderingContext2D; - update(): void; - clear(): void; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; } interface Moving { @@ -15,4 +18,4 @@ interface Moving { move(delta: number): void; } -export {Component, Moving} +export {Component, GraphicComponent, Moving} diff --git a/src/shared_js/constants.ts b/src/shared_js/constants.ts index b8602bcc..f014e9cd 100644 --- a/src/shared_js/constants.ts +++ b/src/shared_js/constants.ts @@ -13,3 +13,6 @@ export const ballSize = pw; export const wallSize = Math.floor(w/100); export const playerSpeed = Math.floor(w/1.5); // pixel per second export const ballSpeed = Math.floor(w/1.5); // pixel per second + +export const matchStartDelay = 3000; // millisecond +export const newRoundDelay = 1500; // millisecond diff --git a/src/shared_js/enums.ts b/src/shared_js/enums.ts index 882193f9..1acf2219 100644 --- a/src/shared_js/enums.ts +++ b/src/shared_js/enums.ts @@ -2,13 +2,15 @@ enum EventTypes { // Class Implemented gameUpdate = 1, + scoreUpdate, + matchEnd, assignId, matchmakingComplete, // Generic matchmakingInProgress, + matchStart, matchNewRound, // unused - matchStart, // unused matchPause, // unused matchResume, // unused