feat(pong): reworked pong to use sane system as ttt

This commit is contained in:
Maieul BOYER 2026-01-02 15:53:54 +01:00 committed by Maix0
parent afd79e334c
commit 0b15fd897b
20 changed files with 637 additions and 707 deletions

View file

@ -1,8 +0,0 @@
#!/bin/sh
set -e
set -x
# do anything here
# run the CMD [ ... ] from the dockerfile
exec "$@"

12
src/pong/src/@types/socket.io.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import { type UserId } from '@shared/database/mixin/user';
declare module 'socket.io'
{
interface Socket {
authUser: {
id: UserId;
name: string;
guest: boolean;
}
}
};

View file

@ -6,32 +6,11 @@ import * as auth from '@shared/auth';
import * as swagger from '@shared/swagger';
import * as utils from '@shared/utils';
import { Server, Socket } from 'socket.io';
import { broadcast } from './broadcast';
import type { ClientProfil, ClientMessage } from './chat_types';
import { sendInvite } from './sendInvite';
import { setGameLink } from './setGameLink';
import { UserId } from '@shared/database/mixin/user';
// colors for console.log
export const color = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m',
};
import { newState, State } from './state';
import { ClientToServer, ServerToClient } from './socket';
declare const __SERVICE_NAME: string;
// Global map of clients
// key = socket, value = clientname
interface ClientInfo {
user: string;
lastSeen: number;
}
export const clientChat = new Map<string, ClientInfo>();
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
@ -60,6 +39,7 @@ const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.ready((err) => {
if (err) throw err;
newState(fastify);
onReady(fastify);
});
};
@ -69,266 +49,13 @@ export { app };
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
interface FastifyInstance {
io: Server<{
inviteGame: (data: ClientProfil) => void;
message: (msg: string) => void;
batmove_Left: (direction: 'up' | 'down') => void;
batmove_Right: (direction: 'up' | 'down') => void;
batLeft_update: (y: number) => void;
batRight_update: (y: number) => void;
ballPos_update: (x: number, y: number) => void;
MsgObjectServer: (data: { message: ClientMessage }) => void;
queuJoin: (userID: UserId) => void;
}>;
io: Server<ClientToServer, ServerToClient>;
}
}
function isInRange(x: number, low: number, high: number) {
if (x >= low && x <= high) return true;
return false;
}
async function sendScore(
socket: Socket,
scoreLeft: number,
scoreRight: number,
) {
// idk why, sometimes... it fails?
const msg: ClientMessage = {
destination: 'score-info',
command: '',
user: '',
text: scoreLeft.toString() + ':' + scoreRight.toString(),
SenderWindowID: '',
};
socket.emit('MsgObjectServer', { message: msg });
}
async function onReady(fastify: FastifyInstance) {
// shows address for connection au server transcendance
const session = process.env.SESSION_MANAGER ?? '';
if (session) {
const part = session.split('/')[1];
const machineName = part.split('.')[0];
console.log(
color.yellow,
'Connect at : https://' + machineName + ':8888/app/login',
);
}
// DRAW AREA
// top edge of the field
const TOP_EDGE = 0;
// bottom edge of the field;
const BOTTOM_EDGE = 450;
const LEFT_EDGE = 0;
const RIGHT_EDGE = 800;
void LEFT_EDGE;
// PADDLEs
const PADDLE_HEIGHT = 80;
const PADDLE_WIDTH = 12;
const PADDLE_SPEED = 20;
const PADDLE_X_OFFSET = 4;
// 370
const MAX_PADDLE_Y = BOTTOM_EDGE - PADDLE_HEIGHT;
// 185
const PADDLE_START = BOTTOM_EDGE / 2 - PADDLE_HEIGHT / 2;
// BALL
// widht times 2 bc rounded on moth sides + 4 for border
const BALL_SIZE = 8 * 2 + 4;
const START_BALLX = RIGHT_EDGE / 2 - BALL_SIZE;
const START_BALLY = BOTTOM_EDGE / 2 - BALL_SIZE;
const ACCELERATION_FACTOR = 1.15;
const ABS_MAX_BALL_SPEED = 3;
// val inits
// shared start bat position
let paddleLeft = PADDLE_START;
// shared start bat position
let paddleRight = PADDLE_START;
let ballPosX = START_BALLX;
let ballPosY = START_BALLY;
let ballSpeedX = -1;
let ballSpeedY = -1;
let scoreL = 0;
let scoreR = 0;
// uuid, game uid - if not in game empty string
const games: Record<UserId, string> = {};
fastify.io.on('connection', (socket: Socket) => {
socket.emit('batLeft_update', paddleLeft);
socket.emit('batRight_update', paddleRight);
socket.emit('ballPos_update', ballPosX, ballPosY);
sendScore(socket, scoreL, scoreR);
// GAME
// paddle handling
socket.on('batmove_Left', (direction: 'up' | 'down') => {
if (direction === 'up') {
paddleLeft -= PADDLE_SPEED;
}
if (direction === 'down') {
paddleLeft += PADDLE_SPEED;
}
// position of bat leftplokoplpl
paddleLeft = Math.max(TOP_EDGE, Math.min(MAX_PADDLE_Y, paddleLeft));
console.log('batLeft_update:', paddleLeft);
socket.emit('batLeft_update', paddleLeft);
});
socket.on('batmove_Right', (direction: 'up' | 'down') => {
if (direction === 'up') {
paddleRight -= PADDLE_SPEED;
}
if (direction === 'down') {
paddleRight += PADDLE_SPEED;
}
// position of bat left
paddleRight = Math.max(
TOP_EDGE,
Math.min(MAX_PADDLE_Y, paddleRight),
);
socket.emit('batRight_update', paddleRight);
});
// ball handling:
setInterval(async () => {
const new_ballPosX = ballPosX + ballSpeedX;
const new_ballPosY = ballPosY + ballSpeedY;
if (
((isInRange(
new_ballPosY,
paddleLeft,
paddleLeft + PADDLE_HEIGHT,
) ||
isInRange(
new_ballPosY + BALL_SIZE * 2,
paddleLeft,
paddleLeft + PADDLE_HEIGHT,
)) &&
// y ok ?
isInRange(
new_ballPosX,
PADDLE_X_OFFSET,
PADDLE_X_OFFSET + PADDLE_WIDTH,
) &&
ballSpeedX < 0) ||
// x ok? && ball going toward paddle?
((isInRange(
new_ballPosY,
paddleRight,
paddleRight + PADDLE_HEIGHT,
) ||
isInRange(
new_ballPosY + BALL_SIZE * 2,
paddleRight,
paddleRight + PADDLE_HEIGHT,
)) &&
// right side equations
isInRange(
new_ballPosX + BALL_SIZE * 2,
RIGHT_EDGE - PADDLE_X_OFFSET - PADDLE_WIDTH,
RIGHT_EDGE - PADDLE_X_OFFSET,
) &&
ballSpeedX > 0)
) {
ballSpeedX *= -1;
ballSpeedX *= ACCELERATION_FACTOR;
ballSpeedY *= ACCELERATION_FACTOR;
console.log('bat colision');
}
else if (
new_ballPosX < 0 ||
new_ballPosX + BALL_SIZE * 2 > RIGHT_EDGE
) {
ballPosX = START_BALLX;
ballPosY = START_BALLY;
ballSpeedX = Math.random() - 0.5 < 0 ? -1 : 1;
if (new_ballPosX < 0) {
scoreR += 1;
ballSpeedY = -1;
}
else {
scoreL += 1;
ballSpeedY = 1;
}
if (scoreL >= 5 || scoreR >= 5) {
console.log('game should stop + board reset');
// temp solution
ballSpeedX = 0;
ballSpeedY = 0;
// reset board :D
}
console.log('point scored');
sendScore(socket, scoreL, scoreR);
// TODO: score point + ball reset + spd reset
}
else if (
new_ballPosY < 0 ||
new_ballPosY + BALL_SIZE * 2 > BOTTOM_EDGE
) {
ballSpeedY *= -1;
ballSpeedX *= ACCELERATION_FACTOR;
ballSpeedY *= ACCELERATION_FACTOR;
}
ballSpeedX = Math.max(
-ABS_MAX_BALL_SPEED,
Math.min(ballSpeedX, ABS_MAX_BALL_SPEED),
);
ballSpeedY = Math.max(
-ABS_MAX_BALL_SPEED,
Math.min(ballSpeedY, ABS_MAX_BALL_SPEED),
);
ballPosX += ballSpeedX;
ballPosY += ballSpeedY;
socket.emit('ballPos_update', ballPosX, ballPosY);
}, 16);
// QUEUE HANDL
socket.on('queuJoin', async (uuid: UserId) => {
console.log('queu join recieved for : ', uuid);
if (!(uuid in games.hasOwnProperty)) {
console.log('new user in game search queu');
games[uuid] = '';
}
else if (uuid in games && games[uuid] == '') {
console.log('already searching for game');
}
else {
// (games.hasOwnProperty(uuid) && games[uuid] != "") {
console.log('user alredy in game');
return;
}
// TODO: step2 : sesrch in record<> find guid w/ "" &/ pair them up
// TODO: step3 : move game logic to lifecycle of queu'ed game
});
// other:
socket.on('message', (message: string) => {
const obj: ClientMessage = JSON.parse(message) as ClientMessage;
clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() });
socket.emit('welcome', { msg: 'Welcome to the chat! : ' });
broadcast(fastify, obj, obj.SenderWindowID);
});
socket.on('inviteGame', async (data: string) => {
const clientName: string = clientChat.get(socket.id)?.user || '';
const profilInvite: ClientProfil = JSON.parse(data) || '';
const inviteHtml: string =
'invites you to a game ' + setGameLink('');
if (clientName !== null) {
sendInvite(fastify, inviteHtml, profilInvite);
}
});
fastify.log.info(`Client connected: ${socket.id}`);
State.registerUser(socket);
});
}

View file

@ -1,22 +0,0 @@
import type { ClientMessage } from './chat_types';
import { clientChat, color } from './app';
import { FastifyInstance } from 'fastify';
export function broadcast(fastify: FastifyInstance, data: ClientMessage, sender?: string) {
fastify.io.fetchSockets().then((sockets) => {
for (const socket of sockets) {
// Skip sender's own socket
if (socket.id === sender) continue;
// Get client name from map
const clientInfo = clientChat.get(socket.id);
if (!clientInfo?.user) {
console.log(color.yellow, `Skipping socket ${socket.id} (no user found)`);
continue;
}
// Emit structured JSON object
socket.emit('MsgObjectServer', { message: data });
// Debug logs
// console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`);
}
});
}

View file

@ -1,23 +0,0 @@
export type ClientMessage = {
command: string
destination: string;
user: string;
text: string;
SenderWindowID: string;
};
export type ClientProfil = {
command: string,
destination: string,
type: string,
user: string,
loginName: string,
userID: string,
text: string,
timestamp: number,
SenderWindowID: string,
SenderName: string,
Sendertext: string,
innerHtml?: string,
};

216
src/pong/src/game.ts Normal file
View file

@ -0,0 +1,216 @@
import { UserId } from '@shared/database/mixin/user';
export class Paddle {
public static readonly DEFAULT_SPEED = 20;
public static readonly DEFAULT_HEIGHT = 80;
public static readonly DEFAULT_WIDTH = 12;
public height: number = Paddle.DEFAULT_HEIGHT;
public width: number = Paddle.DEFAULT_WIDTH;
public speed: number = Paddle.DEFAULT_SPEED;
constructor(
// these coordiantes are the topleft corordinates
public x: number,
public y: number,
) { }
public move(dir: 'up' | 'down') {
this.y += (dir === 'up' ? -1 : 1) * this.speed;
}
public clamp(bottom: number, top: number) {
if (this.y <= bottom) this.y = bottom;
if (this.y + this.height >= top) this.y = top - this.height;
}
}
class Ball {
public static readonly DEFAULT_SPEED = 1;
public static readonly DEFAULT_SIZE = 16;
public static readonly DEFAULT_MAX_SPEED = 30;
public static readonly DEFAULT_MIN_SPEED = Ball.DEFAULT_SPEED;
public static readonly DEFAULT_ACCEL_FACTOR = 1.2;
public speed: number = Ball.DEFAULT_SPEED;
public size: number = Ball.DEFAULT_SIZE;
public accel_factor: number = Ball.DEFAULT_ACCEL_FACTOR;
public max_speed: number = Ball.DEFAULT_MAX_SPEED;
public min_speed: number = Ball.DEFAULT_MIN_SPEED;
constructor(
// these coordiantes are the center coordinates
public x: number,
public y: number,
public angle: number,
) { }
public collided(
side: 'left' | 'right' | 'top' | 'bottom',
walls: { [k in typeof side]: number },
) {
// this.speed *= this.accel_factor;
this.speed = Math.max(
Math.min(this.speed, this.max_speed),
this.min_speed,
);
let c: 'x' | 'y' = 'x';
if (side === 'top' || side === 'bottom') {
this.angle = -this.angle;
c = 'y';
}
else {
this.angle = -this.angle + Math.PI;
c = 'x';
}
this[c] =
walls[side] +
this.size * (side === 'right' || side === 'bottom' ? -1 : 1);
while (this.angle >= Math.PI) {
this.angle -= 2 * Math.PI;
}
while (this.angle < -Math.PI) {
this.angle += 2 * Math.PI;
}
}
public tick() {
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
}
}
function makeAngle(i: number): [number, number, number, number] {
return [
Math.PI / i,
Math.PI / i + Math.PI,
-Math.PI / i,
-Math.PI / i + Math.PI,
];
}
export class Pong {
public gameUpdate: NodeJS.Timeout | null = null;
public static readonly BALL_START_ANGLES: number[] = [
...makeAngle(4),
...makeAngle(6),
];
public ballAngleIdx: number = 0;
public static readonly GAME_WIDTH: number = 800;
public static readonly GAME_HEIGHT: number = 450;
public static readonly PADDLE_OFFSET: number = 40;
public leftPaddle: Paddle = new Paddle(
Pong.PADDLE_OFFSET,
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
);
public rightPaddle: Paddle = new Paddle(
Pong.GAME_WIDTH - Pong.PADDLE_OFFSET - Paddle.DEFAULT_WIDTH,
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
);
public ball: Ball = new Ball(Pong.GAME_WIDTH / 2, Pong.GAME_HEIGHT / 2, Pong.BALL_START_ANGLES[this.ballAngleIdx++]);
public score: [number, number] = [0, 0];
constructor(
public userLeft: UserId,
public userRight: UserId,
) {
}
public tick() {
if (this.paddleCollision(this.leftPaddle, 'left')) {
this.ball.collided('left', {
left: this.leftPaddle.x + this.leftPaddle.width,
right: 0,
top: 0,
bottom: 0,
});
return;
}
if (this.paddleCollision(this.rightPaddle, 'right')) {
this.ball.collided('right', {
right: this.rightPaddle.x,
left: 0,
top: 0,
bottom: 0,
});
return;
}
const wallCollision = this.boxCollision();
if (wallCollision === 'top' || wallCollision === 'bottom') {
this.ball.collided(wallCollision, {
left: 0,
top: 0,
bottom: Pong.GAME_HEIGHT,
right: Pong.GAME_WIDTH,
});
}
else if (wallCollision !== null) {
const idx = wallCollision === 'left' ? 1 : 0;
this.score[idx] += 1;
this.ball = new Ball(
Pong.GAME_WIDTH / 2,
Pong.GAME_HEIGHT / 2,
Pong.BALL_START_ANGLES[this.ballAngleIdx++],
);
this.ballAngleIdx %= Pong.BALL_START_ANGLES.length;
}
this.ball.tick();
}
// This function will return which side the ball collided, if any
private boxCollision(): 'top' | 'bottom' | 'left' | 'right' | null {
if (this.ball.y - this.ball.size <= 0) return 'top';
if (this.ball.y + this.ball.size >= Pong.GAME_HEIGHT) return 'bottom';
if (this.ball.x - this.ball.size <= 0) return 'left';
if (this.ball.x + this.ball.size >= Pong.GAME_WIDTH) return 'right';
return null;
}
private paddleCollision(paddle: Paddle, side: 'left' | 'right'): boolean {
// now we check only if the ball is near enought in the y axis to permform the collision
if (!(
// check if ball is bellow the top of the paddle
paddle.y - this.ball.size < this.ball.y &&
// check if ball is above the bottom of the paddle
this.ball.y < paddle.y + paddle.height + this.ball.size)) return false;
// so we know that the y is close enougth to be a bit, so we check the X. are we closer than the ball size ? if yes -> hit
if (
// check if the paddle.x is at most ball.size away from the center of the ball => we have a hit houston
// call he pentagon, 9 11
Math.abs(
paddle.x + paddle.width * (side === 'left' ? 1 : 0)
- this.ball.x)
< this.ball.size
) return true;
return false;
}
public checkWinner(): 'left' | 'right' | null {
if (this.score[0] >= 5) return 'left';
if (this.score[1] >= 5) return 'right';
return null;
}
public movePaddle(user: UserId, dir: 'up' | 'down') {
const paddle =
user === this.userLeft
? this.leftPaddle
: user == this.userRight
? this.rightPaddle
: null;
if (paddle === null) return;
paddle.move(dir);
paddle.clamp(0, Pong.GAME_HEIGHT);
}
}

View file

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { JwtType } from '@shared/auth';
import { UserId } from '@shared/database/mixin/user';
import { isNullish } from '@shared/utils';
import type {
FastifyInstance,
FastifyPluginAsync,
@ -11,6 +15,21 @@ const F: (
) => Omit<FastifyInstance, 'io'> & { io: Server } = (f) =>
f as Omit<FastifyInstance, 'io'> & { io: Server };
function authenticateToken(
fastify: FastifyInstance,
token: string,
): { id: UserId; name: string; guest: boolean } {
const tok = fastify.jwt.verify<JwtType>(token);
if (tok.kind != 'auth') {
throw new Error('Token isn\'t correct type');
}
const user = fastify.db.getUser(tok.who);
if (isNullish(user)) {
throw new Error('User not found');
}
return { id: user.id, name: user.name, guest: user.guest };
}
const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
function defaultPreClose(done: HookHandlerDoneFunction) {
F(fastify).io.local.disconnectSockets(true);
@ -20,6 +39,36 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
'io',
new Server(fastify.server, { path: '/api/pong/socket.io' }),
);
F(fastify).io.use((socket, next) => {
const cookieHeader = socket.request.headers.cookie;
if (!cookieHeader) {
throw new Error('Missing token cookie');
}
const cookies = Object.fromEntries(
cookieHeader.split(';').map((c) => {
const [k, v] = c.trim().split('=');
return [k, v];
}),
);
if (!cookies.token) {
throw new Error('Missing token cookie');
}
try {
socket.authUser = authenticateToken(fastify, cookies.token);
next();
}
catch (e: any) {
next({
name: 'Unauthorized',
message: e.message,
data: { status: 401 },
});
}
});
fastify.addHook('preClose', defaultPreClose);
fastify.addHook('onClose', (instance: FastifyInstance, done) => {
F(instance).io.close();
@ -28,4 +77,3 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
});
export default fastifySocketIO;

View file

@ -1,6 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from 'typebox';
import { broadcast } from '../broadcast';
export const PongReq = Type.Object({
message: Type.String(),
@ -19,7 +18,6 @@ const route: FastifyPluginAsync = async (fastify): Promise<void> => {
config: { requireAuth: false },
},
async function(req, res) {
broadcast(this, { command: '', destination: 'system-info', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' });
void res;
},
);

View file

@ -1,30 +0,0 @@
import type { ClientProfil } from './chat_types';
import { clientChat, color } from './app';
import { FastifyInstance } from 'fastify';
/**
* function looks for the user online in the chat
* and sends emit to invite - format HTML to make clickable
* message appears in chat window text area
* @param fastify
* @param innerHtml
* @param profil
*/
export function sendInvite(fastify: FastifyInstance, innerHtml: string, profil: ClientProfil) {
fastify.io.fetchSockets().then((sockets) => {
let targetSocket;
for (const socket of sockets) {
const clientInfo: string = clientChat.get(socket.id)?.user || '';
if (clientInfo === profil.user) {
console.log(color.yellow, 'DEBUG LOG: user online found', profil.user);
targetSocket = socket || '';
break;
}
}
profil.innerHtml = innerHtml ?? '';
if (targetSocket) {
targetSocket.emit('inviteGame', profil);
}
});
}

View file

@ -1,6 +0,0 @@
export function setGameLink(link: string): string {
if (!link) {
link = '<a href=\'https://google.com\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>Click me</a>';
}
return link;
};

47
src/pong/src/socket.ts Normal file
View file

@ -0,0 +1,47 @@
import { Socket } from 'socket.io';
export type UpdateInfo = {
inQueue: number,
totalUser: number,
}
export type PaddleData = {
x: number,
y: number,
width: number,
height: number,
};
export type GameUpdate = {
gameId: string;
left: { id: string, paddle: PaddleData, score: number };
right: { id: string, paddle: PaddleData, score: number };
ball: { x: number, y: number, size: number };
}
export type GameMove = {
move: 'up' | 'down' | null,
}
export interface ClientToServer {
enqueue: () => void;
dequeue: () => void;
debugInfo: () => void;
gameMove: (up: GameMove) => void;
connectedToGame: (gameId: string) => void;
};
export interface ServerToClient {
forceDisconnect: (reason: string) => void;
queueEvent: (msg: 'registered' | 'unregistered') => void;
updateInformation: (info: UpdateInfo) => void,
newGame: (initState: GameUpdate) => void,
gameUpdate: (state: GameUpdate) => void,
gameEnd: () => void;
};
export type SSocket = Socket<ClientToServer, ServerToClient>;
export type CSocket = Socket<ServerToClient, ClientToServer>;

182
src/pong/src/state.ts Normal file
View file

@ -0,0 +1,182 @@
import { UserId } from '@shared/database/mixin/user';
import { newUUID } from '@shared/utils/uuid';
import { FastifyInstance } from 'fastify';
import { Pong } from './game';
import { GameMove, GameUpdate, SSocket } from './socket';
import { isNullish } from '@shared/utils';
type PUser = {
id: UserId;
currentGame: null | GameId;
socket: SSocket,
windowId: string,
updateInterval: NodeJS.Timeout,
};
type GameId = string & { readonly __brand: unique symbol };
class StateI {
public static readonly UPDATE_INTERVAL_FRAMES: number = 60;
private users: Map<UserId, PUser> = new Map();
private queue: Set<UserId> = new Set();
private queueInterval: NodeJS.Timeout;
private games: Map<GameId, Pong> = new Map();
public constructor(private fastify: FastifyInstance) {
this.queueInterval = setInterval(() => this.queuerFunction());
void this.queueInterval;
}
private static getGameUpdateData(id: GameId, g: Pong): GameUpdate {
return {
gameId: id,
left: { id: g.userLeft, score: g.score[0], paddle: { x: g.leftPaddle.x, y: g.leftPaddle.y, width: g.leftPaddle.width, height: g.leftPaddle.height } },
right: { id: g.userRight, score: g.score[1], paddle: { x: g.rightPaddle.x, y: g.rightPaddle.y, width: g.rightPaddle.width, height: g.rightPaddle.height } },
ball: { x: g.ball.x, y: g.ball.y, size: g.ball.size },
};
}
private queuerFunction(): void {
const values = Array.from(this.queue.values());
while (values.length >= 2) {
const id1 = values.pop();
const id2 = values.pop();
if (isNullish(id1) || isNullish(id2)) {
continue;
}
const u1 = this.users.get(id1);
const u2 = this.users.get(id2);
if (isNullish(u1) || isNullish(u2)) {
continue;
}
this.queue.delete(id1);
this.queue.delete(id2);
const gameId = newUUID() as unknown as GameId;
const g = new Pong(u1.id, u2.id);
const iState: GameUpdate = StateI.getGameUpdateData(gameId, g);
u1.socket.emit('newGame', iState);
u2.socket.emit('newGame', iState);
this.games.set(gameId, g);
u1.currentGame = gameId;
u2.currentGame = gameId;
g.gameUpdate = setInterval(() => {
g.tick();
this.gameUpdate(gameId, u1.socket);
this.gameUpdate(gameId, u2.socket);
if (g.checkWinner() !== null) { this.cleanupGame(gameId, g); }
}, 1000 / StateI.UPDATE_INTERVAL_FRAMES);
}
}
private gameUpdate(id: GameId, sock: SSocket) {
// does the game we want to update the client exists ?
if (!this.games.has(id)) return;
// is the client someone we know ?
if (!this.users.has(sock.authUser.id)) return;
// is the client associated with that game ?
if (this.users.get(sock.authUser.id)!.currentGame !== id) return;
sock.emit('gameUpdate', StateI.getGameUpdateData(id, this.games.get(id)!));
}
private gameMove(socket: SSocket, u: GameMove) {
// do we know this user ?
if (!this.users.has(socket.authUser.id)) return;
const user = this.users.get(socket.authUser.id)!;
// does the user have a game and do we know such game ?
if (user.currentGame === null || !this.games.has(user.currentGame)) return;
const game = this.games.get(user.currentGame)!;
if (u.move !== null) { game.movePaddle(user.id, u.move); }
}
public registerUser(socket: SSocket): void {
this.fastify.log.info('Registering new user');
if (this.users.has(socket.authUser.id)) {
socket.emit('forceDisconnect', 'Already Connected');
socket.disconnect();
return;
}
this.users.set(socket.authUser.id, {
socket,
id: socket.authUser.id,
windowId: socket.id,
updateInterval: setInterval(() => this.updateClient(socket), 3000),
currentGame: null,
});
this.fastify.log.info('Registered new user');
socket.on('disconnect', () => this.cleanupUser(socket));
socket.on('enqueue', () => this.enqueueUser(socket));
socket.on('dequeue', () => this.dequeueUser(socket));
socket.on('gameMove', (e) => this.gameMove(socket, e));
}
private updateClient(socket: SSocket): void {
socket.emit('updateInformation', {
inQueue: this.queue.size,
totalUser: this.users.size,
});
}
private cleanupUser(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
clearInterval(this.users.get(socket.authUser.id)?.updateInterval);
this.users.delete(socket.authUser.id);
this.queue.delete(socket.authUser.id);
}
private cleanupGame(gameId: GameId, game: Pong): void {
clearInterval(game.gameUpdate ?? undefined);
this.games.delete(gameId);
let player: PUser | undefined = undefined;
if ((player = this.users.get(game.userLeft)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
if ((player = this.users.get(game.userRight)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
// do something here with the game result before deleting the game at the end
}
private enqueueUser(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
if (this.queue.has(socket.authUser.id)) return;
if (this.users.get(socket.authUser.id)?.currentGame !== null) return;
this.queue.add(socket.authUser.id);
socket.emit('queueEvent', 'registered');
}
private dequeueUser(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
if (!this.queue.has(socket.authUser.id)) return;
this.queue.delete(socket.authUser.id);
socket.emit('queueEvent', 'unregistered');
}
}
export let State: StateI = undefined as unknown as StateI;
export function newState(f: FastifyInstance) {
State = new StateI(f);
}