feat(pong): reworked pong to use sane system as ttt
This commit is contained in:
parent
afd79e334c
commit
0b15fd897b
20 changed files with 637 additions and 707 deletions
|
|
@ -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
12
src/pong/src/@types/socket.io.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
216
src/pong/src/game.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
47
src/pong/src/socket.ts
Normal 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
182
src/pong/src/state.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue