diff --git a/frontend/src/pages/pong/addPongMessage.ts b/frontend/src/pages/pong/addPongMessage.ts deleted file mode 100644 index 13fae4b..0000000 --- a/frontend/src/pages/pong/addPongMessage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { color } from './pong'; - -/** - * function adds a message to the frontend pongMessage - * ATTENTION send inner HTML ****** - * @param text - * @returns - */ - -export function addPongMessage(text: string) { - const pongMessage = document.getElementById("system-box") as HTMLDivElement; - if (!pongMessage) return; - const messageElement = document.createElement("div-test"); - messageElement.innerHTML = text; - pongMessage.appendChild(messageElement); - pongMessage.scrollTop = pongMessage.scrollHeight; - console.log(`%c DEBUG LOG: Added PONG new message:%c ${text}`, color.red, color.reset); - return ; -}; \ No newline at end of file diff --git a/frontend/src/pages/pong/broadcastMsg.ts b/frontend/src/pages/pong/broadcastMsg.ts deleted file mode 100644 index e17644d..0000000 --- a/frontend/src/pages/pong/broadcastMsg.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { addPongMessage } from "./addPongMessage"; -import { Socket } from 'socket.io-client'; -import { getUser } from "@app/auth"; - -/** - * function sends socket.emit to the backend to active and a broadcast message to all sockets - * echos addPongMessage() the message with addMessage to the sender - * @param socket - * @param msgCommand - */ -export function broadcastMsg (socket: Socket, msgCommand: string[]): void { - let msgText = msgCommand[1] ?? ""; - addPongMessage(msgText); - const user = getUser(); - if (user && socket?.connected) { - const message = { - command: msgCommand, - destination: '', - type: "chat", - user: user.name, - token: document.cookie, - text: msgText, - timestamp: Date.now(), - SenderWindowID: socket.id, - }; - socket.emit('message', JSON.stringify(message)); - } -}; diff --git a/frontend/src/pages/pong/isLoggedIn.ts b/frontend/src/pages/pong/isLoggedIn.ts deleted file mode 100644 index 2f01067..0000000 --- a/frontend/src/pages/pong/isLoggedIn.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { getUser } from "@app/auth"; -import type { User } from '@app/auth' -/** - * function checks if logged in - * @returns either user | null - */ -export function isLoggedIn(): User | null { - return getUser() || null; -}; \ No newline at end of file diff --git a/frontend/src/pages/pong/pong.html b/frontend/src/pages/pong/pong.html index 7684f61..7b896af 100644 --- a/frontend/src/pages/pong/pong.html +++ b/frontend/src/pages/pong/pong.html @@ -30,7 +30,7 @@
-
+
diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts index c22633c..eacd711 100644 --- a/frontend/src/pages/pong/pong.ts +++ b/frontend/src/pages/pong/pong.ts @@ -1,276 +1,36 @@ import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; -import { showError } from "@app/toast"; import authHtml from './pong.html?raw'; -import client from '@app/api' -import { getUser, updateUser } from "@app/auth"; -import io, { Socket } from 'socket.io-client'; -import { addPongMessage } from './addPongMessage'; -import { isLoggedIn } from './isLoggedIn'; -import type { ClientMessage, ClientProfil } from './types_front'; -import { isNullish } from "@app/utils"; - -export const color = { - red: 'color: red;', - green: 'color: green;', - yellow: 'color: orange;', - blue: 'color: blue;', - reset: '', -}; +import io from 'socket.io-client'; +import type { CSocket, GameMove, GameUpdate } from "./socket"; +import { showError, showInfo } from "@app/toast"; // TODO: local game (2player -> server -> 2player : current setup) // TODO: tournament via remote (dedicated queu? idk) // // get the name of the machine used to connect -const machineHostName = window.location.hostname; -console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow); +declare module 'ft_state' { + interface State { + pongSock?: CSocket; + } +} -export let __socket: Socket | undefined = undefined; - -document.addEventListener('ft:pageChange', () => { // dont regen socket on page change from forward/backward navigation arrows - if (__socket !== undefined) - __socket.close(); - __socket = undefined; - console.log("Page changed"); +document.addEventListener("ft:pageChange", () => { + if (window.__state.pongSock !== undefined) window.__state.pongSock.close(); + window.__state.pongSock = undefined; }); -/** - * @returns the initialized socket - */ -export function getSocket(): Socket { - let addressHost = `wss://${machineHostName}:8888`; - - if (__socket === undefined) - __socket = io(addressHost, { - path: "/api/pong/socket.io/", - secure: false, - transports: ["websocket"], - }); - return __socket; -}; - -/** - * - * @param socket The socket to wait for - * @returns voir or a promise - */ -function waitSocketConnected(socket: Socket): Promise { - return new Promise(resolve => { - if (socket.connected) return resolve(); - socket.on("connect", () => resolve()); - }); -}; - -/** - * - * @param socket The socket to communicat - * @returns nothing - */ -async function whoami(socket: Socket) { - try { - const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; - - const res = await client.guestLogin(); - switch (res.kind) { - case 'success': { - let user = await updateUser(); - if (chatWindow) { - socket.emit('updateClientName', { - oldUser: '', - user: user?.name - }); - } - if (user === null) - return showError('Failed to get user: no user ?'); - setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); - break; - } - case 'failed': { - showError(`Failed to login: ${res.msg}`); - } - } - } catch (e) { - console.error("Login error:", e); - showError('Failed to login: Unknown error'); - } -}; +export function getSocket(): CSocket { + if (window.__state.pongSock === undefined) + window.__state.pongSock = io(window.location.host, { path: "/api/pong/socket.io/" }) as any as CSocket; + return window.__state.pongSock; +} function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn { - let socket = getSocket(); - - socket.on("connect", async () => { - const systemWindow = document.getElementById('system-box') as HTMLDivElement; - await waitSocketConnected(socket); - console.log("I AM Connected to the server:", socket.id); - // const message = { - // command: "", - // destination: 'system-info', - // type: "chat", - // user: getUser()?.name, - // token: document.cookie ?? "", - // text: " has Just ARRIVED in the chat", - // timestamp: Date.now(), - // SenderWindowID: socket.id, - // }; - // socket.emit('message', JSON.stringify(message)); - const messageElement = document.createElement("div"); - // messageElement.textContent = `${message.user}: is connected au server`; - messageElement.textContent = `${getUser()?.name ?? "unkown user"}: is connected au server`; - systemWindow.appendChild(messageElement); - systemWindow.scrollTop = systemWindow.scrollHeight; - }); - - // Queu handler - async function joinQueu(socket : Socket) { - try { - const res = await client.guestLogin(); - - switch (res.kind) { - case 'success': { - let user = await updateUser(); - - if (user === null) - return showError('Failed to get user: no user ?'); - socket.emit('queuJoin', user.id); - console.log('queu join sent for : ', user.id); - break; - } - case 'failed': { - showError(`Failed to Join Game Queu: ${res.msg}`); - console.log('Failed to Join Game Queu'); - } - } - } catch (err ) { - showError(`Failed to Join Game Queu`); - console.log('Failed to Join Game Queu'); - } - } - - // keys handler - const keys: Record = {}; - - document.addEventListener("keydown", (e) => { - keys[e.key.toLowerCase()] = true; - }); - document.addEventListener("keyup", (e) => { - keys[e.key.toLowerCase()] = false; - }); - setInterval(() => { // key sender - if ((keys['w'] || keys['s']) && !(keys['w'] && keys['s'])) { // exclusive or to filter requests - if (keys['w']) { - socket.emit("batmove_Left", "up"); - console.log('north key pressed - emit batmove_Left up'); - } - if (keys['s']) { - socket.emit("batmove_Left", "down"); - console.log('south key pressed - emit batmove_Left down'); - } - } - if ((keys['p'] || keys['l']) && !(keys['p'] && keys['l'])) { // exclusive or to filter requests - if (keys['p']) { - socket.emit("batmove_Right", "up"); - console.log('north key pressed - emit batmove_Right up'); - } - if (keys['l']) { - socket.emit("batmove_Right", "down"); - console.log('south key pressed - emit batmove_Right down'); - } - } - }, 16); - - // Pong Objects updators - socket.on("batLeft_update", (y: number) => { - console.log('batLeft_update received y: ', y); - const bat = document.getElementById("batleft") as HTMLDivElement | null; - if (!bat) { - console.error("FATAL ERROR: Bat element with ID 'bat-left' not found. Check HTML."); - return ; - } - if (typeof y === 'number' && !isNaN(y)) { - bat.style.transform = `translateY(${y}px)`; - } else { - console.warn(`Received invalid Y value: ${y}`); - } - }); - socket.on("batRight_update", (y: number) => { - console.log('batRight_update received y: ', y); - const bat = document.getElementById("batright") as HTMLDivElement | null; - if (!bat) { - console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML."); - return ; - } - if (typeof y === 'number' && !isNaN(y)) { - bat.style.transform = `translateY(${y}px)`; - } else { - console.warn(`Received invalid Y value: ${y}`); - } - }); - socket.on("ballPos_update", (x:number, y : number) => { - console.log('ballPos_update recieved'); - const ball = document.getElementById("ball") as HTMLDivElement | null; - - if (!ball) { - console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML."); - return ; - } - if (typeof y !== 'number' || isNaN(y) || typeof x !== 'number' || isNaN(x)) { - console.warn(`Received invalid X/Y value: ${x} / ${y}`); - return ; - } - ball.style.transform = `translateY(${y}px)`; - ball.style.transform += `translateX(${x}px)`; - }); - - // socket.once('welcome', (data) => { - // console.log('%cWelcome PONG PAGE', color.yellow ); - // addPongMessage('socket.once \'Welcome\' called') - // }); - - // Listen for messages from the server "MsgObjectServer" - socket.on("MsgObjectServer", (data: { message: ClientMessage}) => { - // Display the message in the chat window - console.log("message recieved : ", data.message.text); - const systemWindow = document.getElementById('system-box') as HTMLDivElement; - const MAX_SYSTEM_MESSAGES = 10; - - if (systemWindow && data.message.destination === "system-info") { - const messageElement = document.createElement("div"); - messageElement.textContent = `${data.message.user}: ${data.message.text}`; - systemWindow.appendChild(messageElement); - - // keep only last 10 - while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { - systemWindow.removeChild(systemWindow.firstChild!); - } - systemWindow.scrollTop = systemWindow.scrollHeight; - } - if (systemWindow && data.message.destination === "score-info") { - console.log("score update:", data.message.text); - const scoreboard = document.getElementById('score-board') as HTMLHeadingElement ; - - if (!scoreboard) { - console.log("update score failed :("); - return ; - } - scoreboard.textContent = `${data.message.text}`; - } - // console.log("Getuser():", getUser()); - }); - setTitle('Pong Game Page'); return { html: authHtml, postInsert: async (app) => { - const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement; - const bqueu = document.getElementById('b-joinQueu') as HTMLButtonElement; - - bwhoami?.addEventListener('click', async () => { - whoami(socket); - }); - bqueu?.addEventListener('click', async () => { - joinQueu(socket); - }); - const checkbox = document.getElementById("modeToggle") as HTMLInputElement; const label = document.getElementById("toggleLabel") as HTMLSpanElement; const track = document.getElementById("toggleTrack") as HTMLDivElement; @@ -287,7 +47,67 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn knob.classList.remove("translate-x-7"); } }); + + + + const batLeft = document.querySelector("#batleft"); + const batRight = document.querySelector("#batright"); + const ball = document.querySelector("#ball"); + const score = document.querySelector("#score-board"); + if (!batLeft || !batRight || !ball || !score) + return showError('fatal error'); + + let socket = getSocket(); + + // keys handler + const keys: Record = {}; + + document.addEventListener("keydown", (e) => { + keys[e.key.toLowerCase()] = true; + }); + document.addEventListener("keyup", (e) => { + keys[e.key.toLowerCase()] = false; + }); + + setInterval(() => { // key sender + let packet: GameMove = { + move: null, + } + if ((keys['w'] !== keys['s'])) { + packet.move = keys['w'] ? 'up' : 'down'; + } + + socket.emit('gameMove', packet); + }, 1000 / 60); + + const render = (state: GameUpdate) => { + //batLeft.style.transform = `translateY(${state.left.paddle.y}px) translateX(${state.left.paddle.x}px)`; + batLeft.style.top = `${state.left.paddle.y}px`; + batLeft.style.left = `${state.left.paddle.x}px`; + batLeft.style.width = `${state.left.paddle.width}px`; + batLeft.style.height = `${state.left.paddle.height}px`; + + //batRight.style.transform = `translateY(${state.right.paddle.y}px) translateX(-${state.left.paddle.x}px)`; + batRight.style.top = `${state.right.paddle.y}px`; + batRight.style.left = `${state.right.paddle.x}px`; + batRight.style.width = `${state.right.paddle.width}px`; + batRight.style.height = `${state.right.paddle.height}px`; + + ball.style.transform = `translateX(${state.ball.x - state.ball.size}px) translateY(${state.ball.y - state.ball.size}px)`; + ball.style.height = `${state.ball.size * 2}px`; + ball.style.width = `${state.ball.size * 2}px`; + + + score.innerText = `${state.left.score} | ${state.right.score}` + } + + socket.on('gameUpdate', (state: GameUpdate) => render(state)); + socket.on('newGame', (state) => render(state)); + + socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`)); + socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); + socket.emit('enqueue'); } } }; -addRoute('/pong', pongClient, { bypass_auth: true }); \ No newline at end of file +addRoute('/pong', pongClient); diff --git a/frontend/src/pages/pong/socket.ts b/frontend/src/pages/pong/socket.ts new file mode 100644 index 0000000..044fb8b --- /dev/null +++ b/frontend/src/pages/pong/socket.ts @@ -0,0 +1,47 @@ +import { Socket } from 'socket.io-client'; + +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; +export type CSocket = Socket; diff --git a/frontend/src/pages/pong/types_front.ts b/frontend/src/pages/pong/types_front.ts deleted file mode 100644 index 792bdea..0000000 --- a/frontend/src/pages/pong/types_front.ts +++ /dev/null @@ -1,22 +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, -}; \ No newline at end of file diff --git a/src/pong/entrypoint.sh b/src/pong/entrypoint.sh deleted file mode 100644 index 2dcab02..0000000 --- a/src/pong/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -set -e -set -x -# do anything here - -# run the CMD [ ... ] from the dockerfile -exec "$@" diff --git a/src/pong/extra/.gitkeep b/src/pong/extra/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/pong/src/@types/socket.io.d.ts b/src/pong/src/@types/socket.io.d.ts new file mode 100644 index 0000000..38dedaa --- /dev/null +++ b/src/pong/src/@types/socket.io.d.ts @@ -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; + } + } +}; diff --git a/src/pong/src/app.ts b/src/pong/src/app.ts index d4dc42c..8c1ef49 100644 --- a/src/pong/src/app.ts +++ b/src/pong/src/app.ts @@ -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(); - // @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 => { 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; } } -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 = {}; - 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); }); } diff --git a/src/pong/src/broadcast.ts b/src/pong/src/broadcast.ts deleted file mode 100644 index ac93f1f..0000000 --- a/src/pong/src/broadcast.ts +++ /dev/null @@ -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}`); - } - }); -} diff --git a/src/pong/src/chat_types.ts b/src/pong/src/chat_types.ts deleted file mode 100644 index 73ceeeb..0000000 --- a/src/pong/src/chat_types.ts +++ /dev/null @@ -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, - -}; diff --git a/src/pong/src/game.ts b/src/pong/src/game.ts new file mode 100644 index 0000000..b7f9974 --- /dev/null +++ b/src/pong/src/game.ts @@ -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); + } +} diff --git a/src/pong/src/plugins/socket.ts b/src/pong/src/plugins/socket.ts index ed248ff..c9705ba 100644 --- a/src/pong/src/plugins/socket.ts +++ b/src/pong/src/plugins/socket.ts @@ -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 & { io: Server } = (f) => f as Omit & { io: Server }; +function authenticateToken( + fastify: FastifyInstance, + token: string, +): { id: UserId; name: string; guest: boolean } { + const tok = fastify.jwt.verify(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; - diff --git a/src/pong/src/routes/broadcast.ts b/src/pong/src/routes/broadcast.ts index 8f4b533..a37b460 100644 --- a/src/pong/src/routes/broadcast.ts +++ b/src/pong/src/routes/broadcast.ts @@ -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 => { config: { requireAuth: false }, }, async function(req, res) { - broadcast(this, { command: '', destination: 'system-info', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' }); void res; }, ); diff --git a/src/pong/src/sendInvite.ts b/src/pong/src/sendInvite.ts deleted file mode 100644 index 489dc89..0000000 --- a/src/pong/src/sendInvite.ts +++ /dev/null @@ -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); - } - }); -} diff --git a/src/pong/src/setGameLink.ts b/src/pong/src/setGameLink.ts deleted file mode 100644 index e0cb51b..0000000 --- a/src/pong/src/setGameLink.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function setGameLink(link: string): string { - if (!link) { - link = 'Click me'; - } - return link; -}; diff --git a/src/pong/src/socket.ts b/src/pong/src/socket.ts new file mode 100644 index 0000000..05e674c --- /dev/null +++ b/src/pong/src/socket.ts @@ -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; +export type CSocket = Socket; diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts new file mode 100644 index 0000000..7fd7cff --- /dev/null +++ b/src/pong/src/state.ts @@ -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 = new Map(); + private queue: Set = new Set(); + private queueInterval: NodeJS.Timeout; + private games: Map = 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); +}