authoritative server OK

+ TODO actual matchmaking
This commit is contained in:
LuckyLaszlo
2022-11-21 19:46:25 +01:00
parent 48665cfe30
commit add08c216f
20 changed files with 485 additions and 240 deletions

View File

@@ -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}

View File

@@ -1,50 +1,50 @@
import * as c from "../constants.js" import * as c from "../constants.js"
import {Vector, VectorInteger} from "../../shared_js/class/Vector.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 {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; midLine: Line;
score1: TextNumericValue; scoreLeft: TextNumericValue;
score2: TextNumericValue; scoreRight: TextNumericValue;
w_grid_mid: Rectangle; w_grid_mid: RectangleClient;
w_grid_u1: Rectangle; w_grid_u1: RectangleClient;
w_grid_d1: Rectangle; w_grid_d1: RectangleClient;
h_grid_mid: Rectangle; h_grid_mid: RectangleClient;
h_grid_u1: Rectangle; h_grid_u1: RectangleClient;
h_grid_d1: Rectangle; h_grid_d1: RectangleClient;
constructor(ctx: CanvasRenderingContext2D) constructor(ctx: CanvasRenderingContext2D)
{ {
super(ctx); super(ctx);
let pos = new VectorInteger; let pos = new VectorInteger;
// Scores // Scores
pos.assign(c.w_mid-c.scoreSize*1.6, c.scoreSize*1.5); 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); pos.assign(c.w_mid+c.scoreSize*1.1, c.scoreSize*1.5);
this.score2 = new TextNumericValue(ctx, pos, "white", c.scoreSize); this.scoreRight = new TextNumericValue(pos, c.scoreSize, ctx, "white");
this.score1.value = 0; this.scoreLeft.value = 0;
this.score2.value = 0; this.scoreRight.value = 0;
// Dotted Midline // Dotted Midline
pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize); 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 // Grid
pos.assign(0, c.h_mid); 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); 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); 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); 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); 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); 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");
} }
} }

View File

@@ -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}

View File

@@ -10,11 +10,13 @@ class TextElem implements Component {
size: number; size: number;
font: string; font: string;
text: string = ""; text: string = "";
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font: string = "Bit5x3") { constructor(pos: VectorInteger, size: number,
this.ctx = ctx; ctx: CanvasRenderingContext2D, color: string, font: string = "Bit5x3")
{
this.pos = Object.assign({}, pos); this.pos = Object.assign({}, pos);
this.color = color;
this.size = size; this.size = size;
this.ctx = ctx;
this.color = color;
this.font = font; this.font = font;
} }
update() { update() {
@@ -38,8 +40,10 @@ class TextElem implements Component {
class TextNumericValue extends TextElem { class TextNumericValue extends TextElem {
private _value: number = 0; private _value: number = 0;
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, font?: string) { constructor(pos: VectorInteger, size: number,
super(ctx, pos, color, size, font); ctx: CanvasRenderingContext2D, color: string, font?: string)
{
super(pos, size, ctx, color, font);
} }
get value() { get value() {
return this._value; return this._value;

View File

@@ -5,3 +5,6 @@ export * from "../shared_js/constants.js"
export const midLineSize = Math.floor(w/150); export const midLineSize = Math.floor(w/150);
export const scoreSize = Math.floor(w/16); export const scoreSize = Math.floor(w/16);
export const gridSize = Math.floor(w/500); export const gridSize = Math.floor(w/500);
// min interval on Firefox seems to be 15. Chrome can go lower.
export const gameLoopIntervalMS = 15; // millisecond

View File

@@ -3,12 +3,18 @@ import {gridDisplay} from "./handleInput.js";
function draw() function draw()
{ {
pong.clear();
drawStatic();
if (gridDisplay) { if (gridDisplay) {
drawGrid(); drawGrid();
} }
gc.midLine.update(); gc.scoreLeft.update();
gc.score1.update(); gc.scoreRight.update();
gc.score2.update(); gc.playerLeft.update();
gc.playerRight.update();
gc.ball.update();
} }
function drawStatic() function drawStatic()
@@ -18,14 +24,6 @@ function drawStatic()
gc.midLine.update(); gc.midLine.update();
} }
function drawInit()
{
pong.clear();
drawStatic();
gc.playerLeft.update();
gc.playerRight.update();
}
function drawGrid() function drawGrid()
{ {
gc.w_grid_mid.update(); gc.w_grid_mid.update();
@@ -37,4 +35,4 @@ function drawGrid()
gc.h_grid_d1.update(); gc.h_grid_d1.update();
} }
export {draw, drawStatic, drawInit, drawGrid} export {draw, drawStatic, drawGrid}

View File

@@ -1,9 +1,6 @@
import {pong, gc, clientInfo} from "./global.js"
import * as d from "./draw.js"; import * as d from "./draw.js";
import {random} from "./utils.js";
import {handleInput} from "./handleInput.js"; import {handleInput} from "./handleInput.js";
let ballInPlay = false;
let actual_time: number = Date.now(); let actual_time: number = Date.now();
let last_time: number; let last_time: number;
let delta_time: number; let delta_time: number;
@@ -21,47 +18,10 @@ function gameLoop()
handleInput(delta_time); handleInput(delta_time);
if (ballInPlay) // prediction
{ // gc.ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
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);
}
}
d.draw(); d.draw();
} }
function newRound() export {gameLoop}
{
// 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}

View File

@@ -4,7 +4,7 @@ import { socket } from "./ws.js";
import {InputEnum} from "../shared_js/enums.js" import {InputEnum} from "../shared_js/enums.js"
import {EventInput} from "../shared_js/class/Event.js" import {EventInput} from "../shared_js/class/Event.js"
let gridDisplay = false; export let gridDisplay = false;
function handleInput(delta: number) function handleInput(delta: number)
{ {
@@ -27,25 +27,24 @@ function handleInput(delta: number)
function playerMove(delta: number, keys: string[]) function playerMove(delta: number, keys: string[])
{ {
// gc.playerLeft.dir.y = 0;
if (keys.indexOf("w") != -1) { if (keys.indexOf("w") != -1) {
socket.send(JSON.stringify(new EventInput(InputEnum.up))); socket.send(JSON.stringify(new EventInput(InputEnum.up)));
// gc.playerLeft.dir.y += -1;
} }
if (keys.indexOf("s") != -1) { if (keys.indexOf("s") != -1) {
socket.send(JSON.stringify(new EventInput(InputEnum.down))); 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; // prediction
if (keys.indexOf("ArrowUp".toLowerCase()) != -1) { /* const racket = clientInfo.racket;
gc.playerRight.dir.y += -1; racket.dir.y = 0;
if (keys.indexOf("w") != -1) {
racket.dir.y += -1;
} }
if (keys.indexOf("ArrowDown".toLowerCase()) != -1) { if (keys.indexOf("s") != -1) {
gc.playerRight.dir.y += 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}

View File

@@ -1,11 +1,13 @@
import {GameArea} from "./class/GameArea.js"; import {GameArea} from "./class/GameArea.js";
import * as d from "./draw.js"; import * as d from "./draw.js";
import {gameLoop, newRound} from "./gameLoop.js" import {gameLoop} from "./gameLoop.js"
// import * as c from "./constants.js" import * as c from "./constants.js"
import { GameComponentsClient } from "./class/GameComponentsClient.js"; import { GameComponentsClient } from "./class/GameComponentsClient.js";
import {countdown} from "./utils.js"; import {countdown} from "./utils.js";
import {socket} from "./ws.js"; socket; // no-op
/* Keys /* Keys
Racket 1: W/S Racket 1: W/S
Racket 2: Up/Down Racket 2: Up/Down
@@ -23,24 +25,24 @@ function matchmaking()
function matchmakingComplete() function matchmakingComplete()
{ {
console.log("Match Found !"); // PLACEHOLDER, TODO draw on canvas 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) { window.addEventListener('keydown', function (e) {
pong.addKey(e.key); pong.addKey(e.key);
}); });
window.addEventListener('keyup', function (e) { window.addEventListener('keyup', function (e) {
pong.deleteKey(e.key); pong.deleteKey(e.key);
}); });
pong.interval = window.setInterval(gameLoop, 15); // min interval on Firefox seems to be 15. Chrome can go lower. pong.interval = window.setInterval(gameLoop, c.gameLoopIntervalMS);
setTimeout(newRound, 1000);
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
export {matchmaking, matchmakingComplete} export {matchmaking, matchmakingComplete, startGame}

View File

@@ -1,9 +1,10 @@
import {pong, gc} from "./global.js" import {pong, gc} from "./global.js"
import * as ev from "../shared_js/class/Event.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 * as en from "../shared_js/enums.js"
import { Racket } from "../shared_js/class/Rectangle.js"; import { Racket } from "../shared_js/class/Rectangle.js";
import { sleep } from "./utils.js";
const wsPort = 8042; const wsPort = 8042;
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; 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) { else if (clientInfo.side === en.PlayerSide.right) {
clientInfo.racket = gc.playerRight; clientInfo.racket = gc.playerRight;
} }
socket.removeEventListener("message", preMatchListener);
socket.addEventListener("message", inGameListener);
socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) )); socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) ));
matchmakingComplete(); matchmakingComplete();
break; 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) { switch (data.type) {
case en.EventTypes.gameUpdate: case en.EventTypes.gameUpdate:
console.log("gameUpdate"); console.log("gameUpdate");
serverGameUpdate(data as ev.EventGameUpdate); gameUpdate(data as ev.EventGameUpdate);
break; break;
case en.EventTypes.matchNewRound: case en.EventTypes.scoreUpdate:
console.log("matchNewRound//WIP"); console.log("scoreUpdate");
scoreUpdate(data as ev.EventScoreUpdate);
break;
case en.EventTypes.matchEnd:
console.log("matchEnd");
matchEnd(data as ev.EventMatchEnd);
break; 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.pos.y = Math.floor(data.playerLeft.y);
gc.playerLeft.update();
gc.playerRight.clear();
gc.playerRight.pos.y = Math.floor(data.playerRight.y); 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} export {socket}

View File

@@ -2,15 +2,22 @@
import * as c from "../constants.js" import * as c from "../constants.js"
import { GameComponents } from "../../shared_js/class/GameComponents.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) // Empty object replacement to the web-API (web-API useless on server-side)
class CanvasRenderingContext2D {} // class CanvasRenderingContext2D {}
const mockCTX = new CanvasRenderingContext2D(); // const mockCTX = new CanvasRenderingContext2D();
class GameComponentsServer extends GameComponents { class GameComponentsServer extends GameComponents {
scoreLeft: number;
scoreRight: number;
ballInPlay: boolean;
constructor() constructor()
{ {
// @ts-ignore // super(mockCTX);
super(mockCTX); super();
this.scoreLeft = 0;
this.scoreRight = 0;
this.ballInPlay = false;
} }
} }

View File

@@ -1,29 +1,141 @@
import { ClientPlayer } from "./Client"; import { ClientPlayer } from "./Client";
import {gameUpdate} from "../gameUpdate.js" import {gameUpdate} from "../gameUpdate.js"
import { GameComponentsServer } from "./GameComponentsServer"; import { GameComponents } from "../../shared_js/class/GameComponents.js";
import { clientInputListener } from "../wsServer.js";
// Empty object replacement to the web-API (web-API useless on server-side) 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 { class GameSession {
id: string; // url ? id: string; // url ?
playersMap: Map<string, ClientPlayer>; playersMap: Map<string, ClientPlayer>;
unreadyPlayersMap: Map<string, ClientPlayer>; unreadyPlayersMap: Map<string, ClientPlayer>;
updateInterval: NodeJS.Timer; gameLoopInterval: NodeJS.Timer | number;
// gc: GameComponentsServer; clientsUpdateInterval: NodeJS.Timer | number;
// updateInterval: NodeJS.Timer; components: GameComponentsServer;
actual_time: number;
last_time: number;
delta_time: number;
constructor(id: string) { constructor(id: string) {
this.id = id; this.id = id;
this.playersMap = new Map(); this.playersMap = new Map();
this.unreadyPlayersMap = new Map(); this.unreadyPlayersMap = new Map();
// this.gc = new GameComponentsServer(); this.components = new GameComponentsServer();
} }
start() { start() {
this.updateInterval = setInterval( () => { setTimeout(this.resume, c.matchStartDelay, this);
const update = gameUpdate(); setTimeout(this._newRound, c.matchStartDelay + c.newRoundDelay, this);
this.playersMap.forEach( (client) => { }
client.socket.send(JSON.stringify(update)); resume(s: GameSession) {
}); s.playersMap.forEach( (client) => {
}, 500); 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));
});
} }
} }

View File

@@ -1,2 +1,5 @@
export * from "../shared_js/constants.js" export * from "../shared_js/constants.js"
export const gameLoopIntervalMS = 15; // millisecond
export const clientsUpdateIntervalMS = 100; // millisecond

View File

@@ -13,18 +13,18 @@ import * as ev from "../shared_js/class/Event.js"
import {Client, ClientPlayer} from "./class/Client.js" import {Client, ClientPlayer} from "./class/Client.js"
import {GameSession} from "./class/GameSession.js" import {GameSession} from "./class/GameSession.js"
// pas indispensable d'avoir un autre port si le WebSocket est limité à certaines routes // pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ?
// (et relié à un serveur http préexistant ?) const wsPort = 8042;
const wsPort = 8042;
export const wsServer = new WebSocketServer<WebSocket>({port: wsPort, path: "/pong"}); export const wsServer = new WebSocketServer<WebSocket>({port: wsPort, path: "/pong"});
const clientsMap: Map<string, Client> = new Map; // socket.id/Client (unique Client) const clientsMap: Map<string, Client> = new Map; // socket.id/Client
const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession (duplicates GameSession) const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession
wsServer.on("connection", connectionListener); wsServer.on("connection", connectionListener);
wsServer.on("error", errorListener); wsServer.on("error", errorListener);
wsServer.on("close", closeListener); wsServer.on("close", closeListener);
function connectionListener(socket: WebSocket, request: IncomingMessage) function connectionListener(socket: WebSocket, request: IncomingMessage)
{ {
const id = uuidv4(); const id = uuidv4();
@@ -37,13 +37,14 @@ function connectionListener(socket: WebSocket, request: IncomingMessage)
console.log("%i: client %s is alive", Date.now(), client.id); 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); console.log("data: " + data);
}); });
socket.once("message", clientAnnounceListener); socket.once("message", clientAnnounceListener);
} }
function clientAnnounceListener(this: WebSocket, data: string) function clientAnnounceListener(this: WebSocket, data: string)
{ {
try { try {
@@ -67,6 +68,7 @@ function clientAnnounceListener(this: WebSocket, data: string)
this.once("message", clientAnnounceListener); this.once("message", clientAnnounceListener);
} }
function matchmaking(socket: WebSocket) function matchmaking(socket: WebSocket)
{ {
// TODO Actual Matchmaking // TODO Actual Matchmaking
@@ -82,13 +84,15 @@ function matchmaking(socket: WebSocket)
gameSession.playersMap.set(socket.id, player); gameSession.playersMap.set(socket.id, player);
gameSession.unreadyPlayersMap.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.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) ));
socket.once("message", playerReadyConfirmationListener); socket.once("message", playerReadyConfirmationListener);
// socket.on("message", clientInputListener);
// setinterval gameloop
} }
function playerReadyConfirmationListener(this: WebSocket, data: string) function playerReadyConfirmationListener(this: WebSocket, data: string)
{ {
try { try {
@@ -98,6 +102,9 @@ function playerReadyConfirmationListener(this: WebSocket, data: string)
const gameSession = client.gameSession; const gameSession = client.gameSession;
gameSession.unreadyPlayersMap.delete(this.id); gameSession.unreadyPlayersMap.delete(this.id);
if (gameSession.unreadyPlayersMap.size === 0) { if (gameSession.unreadyPlayersMap.size === 0) {
gameSession.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
});
gameSession.start(); gameSession.start();
} }
} }
@@ -112,12 +119,15 @@ function playerReadyConfirmationListener(this: WebSocket, data: string)
this.once("message", playerReadyConfirmationListener); this.once("message", playerReadyConfirmationListener);
} }
function clientInputListener(this: WebSocket, data: string)
export function clientInputListener(this: WebSocket, data: string)
{ {
try { 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) { 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 { else {
console.log("Invalid EventInput"); console.log("Invalid EventInput");
@@ -144,12 +154,13 @@ const pingInterval = setInterval( () => {
}); });
}, 5000); }, 5000);
function closeListener() function closeListener()
{ {
clearInterval(pingInterval); clearInterval(pingInterval);
// clearInterval(gameUpdateInterval); // TODO: Per Game Session
} }
function errorListener(error: Error) function errorListener(error: Error)
{ {
console.log("Error: " + JSON.stringify(error)); console.log("Error: " + JSON.stringify(error));

View File

@@ -29,11 +29,28 @@ class EventGameUpdate extends ServerEvent {
playerLeft = {y: 0}; playerLeft = {y: 0};
playerRight = {y: 0}; playerRight = {y: 0};
ball = {x: 0, y: 0, speed: 0}; ball = {x: 0, y: 0, speed: 0};
constructor() { constructor() { // TODO: constructor that take GameComponentsServer maybe ?
super(en.EventTypes.gameUpdate); 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 */ /* From Client */
class ClientEvent { class ClientEvent {
@@ -49,6 +66,7 @@ class ClientAnnounce extends ClientEvent {
constructor(role: en.ClientRole, id: string = "") { constructor(role: en.ClientRole, id: string = "") {
super(en.EventTypes.clientAnnounce); super(en.EventTypes.clientAnnounce);
this.role = role; this.role = role;
this.id = id;
} }
} }
@@ -61,6 +79,7 @@ class EventInput extends ClientEvent {
} }
export { export {
ServerEvent, EventAssignId, EventMatchmakingComplete, EventGameUpdate, ServerEvent, EventAssignId, EventMatchmakingComplete,
EventGameUpdate, EventScoreUpdate, EventMatchEnd,
ClientEvent, ClientAnnounce, EventInput ClientEvent, ClientAnnounce, EventInput
} }

View File

@@ -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 * as c from "../constants.js"
import {VectorInteger} from "./Vector.js"; import {VectorInteger} from "./Vector.js";
import {Rectangle, Racket, Ball} from "./Rectangle.js"; import {Rectangle, Racket, Ball} from "./Rectangle.js";
class GameComponents { class GameComponents {
wallTop: Rectangle; wallTop: Rectangle;
wallBottom: Rectangle; wallBottom: Rectangle;
playerLeft: Racket; playerLeft: Racket;
playerRight: Racket; playerRight: Racket;
ball: Ball; ball: Ball;
constructor(ctx?: CanvasRenderingContext2D) constructor()
{ {
let pos = new VectorInteger; let pos = new VectorInteger;
pos.assign(0, 0); 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); 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); 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); 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); // pos.assign(c.w_mid-c.ballSize/2, c.h_mid-c.ballSize/2); // center the ball
this.ball = new Ball(ctx, pos, "white", c.ballSize, c.ballSpeed); 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); this.ball.dir.assign(-0.8, +0.2);
} }
} }

View File

@@ -3,28 +3,14 @@ import {Vector, VectorInteger} from "./Vector.js";
import {Component, Moving} from "./interface.js"; import {Component, Moving} from "./interface.js";
class Rectangle implements Component { class Rectangle implements Component {
ctx: CanvasRenderingContext2D;
pos: VectorInteger; pos: VectorInteger;
color: string;
width: number; width: number;
height: number; height: number;
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number) { constructor(pos: VectorInteger, width: number, height: number) {
this.ctx = ctx;
this.pos = Object.assign({}, pos); this.pos = Object.assign({}, pos);
this.color = color;
this.width = width; this.width = width;
this.height = height; 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 collision(collider: Rectangle): boolean { // Collision WIP. To redo
var myleft = this.pos.x; var myleft = this.pos.x;
var myright = this.pos.x + (this.width); var myright = this.pos.x + (this.width);
@@ -49,8 +35,8 @@ class MovingRectangle extends Rectangle implements Moving {
dir: Vector = new Vector(0,0); dir: Vector = new Vector(0,0);
speed: number; speed: number;
readonly baseSpeed: number; readonly baseSpeed: number;
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(ctx, pos, color, width, height); super(pos, width, height);
this.baseSpeed = baseSpeed; this.baseSpeed = baseSpeed;
this.speed = baseSpeed; this.speed = baseSpeed;
} }
@@ -69,23 +55,18 @@ class MovingRectangle extends Rectangle implements Moving {
this.pos.x = oldPos.x; this.pos.x = oldPos.x;
this.pos.y = oldPos.y; this.pos.y = oldPos.y;
} }
else
{
this.clear(oldPos);
this.update();
}
} }
} }
class Racket extends MovingRectangle { class Racket extends MovingRectangle {
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, width: number, height: number, baseSpeed: number) { constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(ctx, pos, color, width, height, baseSpeed); super(pos, width, height, baseSpeed);
} }
} }
class Ball extends MovingRectangle { class Ball extends MovingRectangle {
constructor(ctx: CanvasRenderingContext2D, pos: VectorInteger, color: string, size: number, baseSpeed: number) { constructor(pos: VectorInteger, size: number, baseSpeed: number) {
super(ctx, pos, color, size, size, baseSpeed); super(pos, size, size, baseSpeed);
} }
bounce(collider?: Rectangle) { bounce(collider?: Rectangle) {
/* Could be more generic, but testing only Racket is enough, /* Could be more generic, but testing only Racket is enough,
@@ -98,7 +79,6 @@ class Ball extends MovingRectangle {
} }
} }
moveAndBounce(delta: number, colliderArr: Rectangle[]) { moveAndBounce(delta: number, colliderArr: Rectangle[]) {
let oldPos = Object.assign({}, this.pos);
this.move(delta); this.move(delta);
let i = colliderArr.findIndex(this.collision, this); let i = colliderArr.findIndex(this.collision, this);
if (i != -1) if (i != -1)
@@ -106,8 +86,6 @@ class Ball extends MovingRectangle {
this.bounce(colliderArr[i]); this.bounce(colliderArr[i]);
this.move(delta); this.move(delta);
} }
this.clear(oldPos);
this.update();
} }
private _bounceWall() { // Should be enough for Wall private _bounceWall() { // Should be enough for Wall
this.dir.y = this.dir.y * -1; this.dir.y = this.dir.y * -1;
@@ -119,54 +97,4 @@ class Ball extends MovingRectangle {
} }
} }
class Line extends Rectangle { export {Rectangle, MovingRectangle, Racket, Ball}
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;
} */

View File

@@ -3,10 +3,13 @@ import {Vector, VectorInteger} from "./Vector.js";
interface Component { interface Component {
pos: VectorInteger; pos: VectorInteger;
color: string; }
interface GraphicComponent extends Component {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
update(): void; color: string;
clear(): void; update: () => void;
clear: (pos?: VectorInteger) => void;
} }
interface Moving { interface Moving {
@@ -15,4 +18,4 @@ interface Moving {
move(delta: number): void; move(delta: number): void;
} }
export {Component, Moving} export {Component, GraphicComponent, Moving}

View File

@@ -13,3 +13,6 @@ export const ballSize = pw;
export const wallSize = Math.floor(w/100); export const wallSize = Math.floor(w/100);
export const playerSpeed = Math.floor(w/1.5); // pixel per second export const playerSpeed = Math.floor(w/1.5); // pixel per second
export const ballSpeed = 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

View File

@@ -2,13 +2,15 @@
enum EventTypes { enum EventTypes {
// Class Implemented // Class Implemented
gameUpdate = 1, gameUpdate = 1,
scoreUpdate,
matchEnd,
assignId, assignId,
matchmakingComplete, matchmakingComplete,
// Generic // Generic
matchmakingInProgress, matchmakingInProgress,
matchStart,
matchNewRound, // unused matchNewRound, // unused
matchStart, // unused
matchPause, // unused matchPause, // unused
matchResume, // unused matchResume, // unused