diff --git a/frontend/src/pages/pong/pong.css b/frontend/src/pages/pong/pong.css index a913582..176eb9a 100644 --- a/frontend/src/pages/pong/pong.css +++ b/frontend/src/pages/pong/pong.css @@ -8,8 +8,8 @@ .btn-style { @apply - w-25 - h-8 + min-w-25 + min-h-8 border border-gray-500 rounded-3xl @@ -188,4 +188,4 @@ inline-flex items-center justify-center rounded-full w-8 h-8 bg-blue-500 border-10 border-blue-500 -} \ No newline at end of file +} diff --git a/frontend/src/pages/pong/pong.html b/frontend/src/pages/pong/pong.html index a2828d8..310c687 100644 --- a/frontend/src/pages/pong/pong.html +++ b/frontend/src/pages/pong/pong.html @@ -2,8 +2,13 @@
- ?👤 ?⏳ ?▮•▮ - + ?👤 + ?⏳ ?▮•▮ + +

Pong Box @@ -25,7 +30,8 @@ down: S
- You are red.
Your goal is to bounce the ball back to the adversary. + You are red.
Your goal is to bounce the ball back to the + adversary.
local games keys for the left paddle:
up: @@ -33,7 +39,7 @@ down: L
-

+
diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts index 150598d..618c1f9 100644 --- a/frontend/src/pages/pong/pong.ts +++ b/frontend/src/pages/pong/pong.ts @@ -1,14 +1,20 @@ -import { addRoute, navigateTo, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; -import authHtml from './pong.html?raw'; -import io from 'socket.io-client'; +import { + addRoute, + navigateTo, + setTitle, + type RouteHandlerParams, + type RouteHandlerReturn, +} from "@app/routing"; +import authHtml from "./pong.html?raw"; +import io from "socket.io-client"; import type { CSocket, GameMove, GameUpdate } from "./socket"; -import { showError, showInfo } from "@app/toast"; +import { showError, showInfo, showSuccess } from "@app/toast"; import { getUser, type User } from "@app/auth"; import { isNullish } from "@app/utils"; import client from "@app/api"; import "./pong.css"; -declare module 'ft_state' { +declare module "ft_state" { interface State { pongSock?: CSocket; } @@ -20,96 +26,176 @@ enum QueueState { Ready = "Ready-ing", Iddle = "Queue Up", In_local = "In Local", -}; +} enum ReadyState { readyUp = "ready ok", readyDown = "not ready", -}; +} + +enum TourBtnState { + Joined = "Joined", + Started = "Started", + AbleToJoin = "Join Tournament", + AbleToCreate = "Create Tournament", + AbleToStart = "Start Tournament", + AbeToProcreate = "He would be proud", +} document.addEventListener("ft:pageChange", (newUrl) => { - if (newUrl.detail.startsWith('/app/pong') || newUrl.detail.startsWith('/pong')) return; + if ( + newUrl.detail.startsWith("/app/pong") || + newUrl.detail.startsWith("/pong") + ) + return; if (window.__state.pongSock !== undefined) window.__state.pongSock.close(); window.__state.pongSock = undefined; }); 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; + 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 { - setTitle('Pong Game Page'); +function pongClient( + _url: string, + _args: RouteHandlerParams, +): RouteHandlerReturn { + setTitle("Pong Game Page"); return { - html: authHtml, postInsert: async (app) => { + html: authHtml, + postInsert: async (app) => { const DEFAULT_COLOR = "white"; const SELF_COLOR = "red"; const user = getUser(); let currentGame: GameUpdate | null = null; let opponent: User | null = null; - const rdy_btn = document.querySelector('#readyup-btn'); + const rdy_btn = + document.querySelector("#readyup-btn"); const batLeft = document.querySelector("#batleft"); - const batRight = document.querySelector("#batright"); + const batRight = + document.querySelector("#batright"); const ball = document.querySelector("#ball"); - const score = document.querySelector("#score-board"); - const playerL = document.querySelector('#player-left'); - const playerR = document.querySelector('#player-right'); - const queueBtn = document.querySelector("#QueueBtn"); - const LocalGameBtn = document.querySelector("#LocalBtn"); - const gameBoard = document.querySelector("#pongbox"); - const queue_infos = document.querySelector("#queue-info"); - const how_to_play_btn = document.querySelector("#play-info"); - const protips = document.querySelector("#protips-box"); + const score = + document.querySelector("#score-board"); + const playerL = + document.querySelector("#player-left"); + const playerR = + document.querySelector("#player-right"); + const queueBtn = + document.querySelector("#QueueBtn"); + const LocalGameBtn = + document.querySelector("#LocalBtn"); + const gameBoard = + document.querySelector("#pongbox"); + const queue_infos = + document.querySelector("#queue-info"); + const how_to_play_btn = + document.querySelector("#play-info"); + const protips = + document.querySelector("#protips-box"); + const tournamentBtn = + document.querySelector("#TourBtn"); let socket = getSocket(); - if (isNullish(user)) { // if no user (no loggin / other) : GTFO + if (isNullish(user)) { + // if no user (no loggin / other) : GTFO navigateTo("/app"); - return ; + return; } - if (!batLeft || !batRight || !ball || !score || !queueBtn || !playerL || !playerR || !gameBoard || !queue_infos || !LocalGameBtn || !rdy_btn) // sanity check - return showError('fatal error'); - if (!how_to_play_btn || !protips) - showError('missing protips'); // not a fatal error + if ( + !batLeft || + !batRight || + !ball || + !score || + !queueBtn || + !playerL || + !playerR || + !gameBoard || + !queue_infos || + !LocalGameBtn || + !rdy_btn || + !tournamentBtn + ) + // sanity check + return showError("fatal error"); + if (!how_to_play_btn || !protips) showError("missing protips"); // not a fatal error + + tournamentBtn.addEventListener("click", () => { + showInfo(`Button State: ${tournamentBtn.innerText}`); + + switch (tournamentBtn.innerText) { + case TourBtnState.AbleToStart: + //socket.emit('') + break; + case TourBtnState.AbleToJoin: + socket.emit("tourRegister"); + break; + case TourBtnState.AbleToCreate: + socket.emit("tourCreate"); + break; + case TourBtnState.AbeToProcreate: + showError("We are developpers, this is impossible !"); + break; + case TourBtnState.Joined: + socket.emit("tourUnregister"); + break; + case TourBtnState.Started: + showInfo("tournament Started"); + //socket.emit("tourStart"); + break; + } + }); // --- // keys handler // --- const keys: Record = {}; if (how_to_play_btn && protips) - how_to_play_btn.addEventListener("click", ()=>{ + how_to_play_btn.addEventListener("click", () => { protips.classList.toggle("hidden"); - how_to_play_btn.innerText = how_to_play_btn.innerText === '?' ? 'x' : '?'; + how_to_play_btn.innerText = + how_to_play_btn.innerText === "?" ? "x" : "?"; }); - document.addEventListener("keydown", (e) => {keys[e.key.toLowerCase()] = true;}); - document.addEventListener("keyup", (e) => {keys[e.key.toLowerCase()] = false;}); + document.addEventListener("keydown", (e) => { + keys[e.key.toLowerCase()] = true; + }); + document.addEventListener("keyup", (e) => { + keys[e.key.toLowerCase()] = false; + }); - setInterval(() => { // key sender - if (keys['escape'] === true && protips && how_to_play_btn) { + setInterval(() => { + // key sender + if (keys["escape"] === true && protips && how_to_play_btn) { protips.classList.add("hidden"); - how_to_play_btn.innerText = '?'; + how_to_play_btn.innerText = "?"; } - if (queueBtn.innerText !== QueueState.InGame)//we're in game ? continue | gtfo - return ; + if (queueBtn.innerText !== QueueState.InGame) + //we're in game ? continue | gtfo + return; if (currentGame === null) return; let packet: GameMove = { move: null, moveRight: null, - } + }; - if (queueBtn.innerText !== QueueState.InGame)//we're in game ? continue | gtfo - return ; + if (queueBtn.innerText !== QueueState.InGame) + //we're in game ? continue | gtfo + return; if (currentGame === null) return; - if ((keys['w'] !== keys['s'])) - packet.move = keys['w'] ? 'up' : 'down'; - if (currentGame.local && (keys['o'] !== keys['l'])) - packet.moveRight = keys['o'] ? 'up' : 'down'; - socket.emit('gameMove', packet); + if (keys["w"] !== keys["s"]) + packet.move = keys["w"] ? "up" : "down"; + if (currentGame.local && keys["o"] !== keys["l"]) + packet.moveRight = keys["o"] ? "up" : "down"; + socket.emit("gameMove", packet); }, 1000 / 60); // --- // keys end @@ -118,15 +204,28 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn // --- // position logic (client) // --- - const DEFAULT_POSITIONS : GameUpdate = { - gameId:"", - ball:{size:16, x:800/2, y:450/2}, - left:{id:"", paddle:{x:40, y:185, width:12, height:80}, score:0}, - right:{id:"", paddle:{x:748, y:185, width:12, height:80}, score:0}, - local:false + const DEFAULT_POSITIONS: GameUpdate = { + gameId: "", + ball: { size: 16, x: 800 / 2, y: 450 / 2 }, + left: { + id: "", + paddle: { x: 40, y: 185, width: 12, height: 80 }, + score: 0, + }, + right: { + id: "", + paddle: { x: 748, y: 185, width: 12, height: 80 }, + score: 0, + }, + local: false, }; - function resetBoard(batLeft : HTMLDivElement, batRight : HTMLDivElement, playerL : HTMLDivElement, playerR : HTMLDivElement) { + function resetBoard( + batLeft: HTMLDivElement, + batRight: HTMLDivElement, + playerL: HTMLDivElement, + playerR: HTMLDivElement, + ) { render(DEFAULT_POSITIONS); batLeft.style.backgroundColor = DEFAULT_COLOR; batRight.style.backgroundColor = DEFAULT_COLOR; @@ -154,115 +253,131 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn 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);}); + score.innerText = `${state.left.score} | ${state.right.score}`; + }; + socket.on("gameUpdate", (state: GameUpdate) => { + render(state); + }); // --- // position logic (client) end // --- // --- - // queue evt + // queue evt // --- // utils - function set_pretty(batU : HTMLDivElement, txtU : HTMLDivElement, txtO : HTMLDivElement, colorYou : string) { + function set_pretty( + batU: HTMLDivElement, + txtU: HTMLDivElement, + txtO: HTMLDivElement, + colorYou: string, + ) { batU.style.backgroundColor = colorYou; txtU.style.color = colorYou; txtU.innerText = isNullish(user) ? "you" : user.name; - txtO.innerText = isNullish(opponent) ? "the mechant" : opponent.name; + txtO.innerText = isNullish(opponent) + ? "the mechant" + : opponent.name; } - async function get_opponent(opponent_id : string) { - let t = await client.getUser({user:opponent_id}); + async function get_opponent(opponent_id: string) { + let t = await client.getUser({ user: opponent_id }); switch (t.kind) { - case "success" : + case "success": opponent = t.payload; - break ; - default : + break; + default: opponent = null; } } // btn setup - queueBtn.addEventListener("click", ()=>{ + queueBtn.addEventListener("click", () => { if (queueBtn.innerText !== QueueState.Iddle) { if (queueBtn.innerText === QueueState.InQueu) { socket.emit("dequeue"); queueBtn.innerText = QueueState.Iddle; } - return ; + return; } queueBtn.innerText = QueueState.InQueu; - socket.emit('enqueue'); + socket.emit("enqueue"); }); LocalGameBtn.addEventListener("click", () => { - if (queueBtn.innerText !== QueueState.Iddle || currentGame !== null) { - showError("cant launch a local game while in queue/in game"); - return ; + if ( + queueBtn.innerText !== QueueState.Iddle || + currentGame !== null + ) { + showError( + "cant launch a local game while in queue/in game", + ); + return; } socket.emit("localGame"); queueBtn.innerText = QueueState.In_local; LocalGameBtn.innerText = "playing"; }); - rdy_btn.addEventListener("click", ()=>{ + rdy_btn.addEventListener("click", () => { showInfo("rdy-evt"); switch (rdy_btn.innerText) { case ReadyState.readyDown: - socket.emit('readyUp'); + socket.emit("readyUp"); rdy_btn.innerText = ReadyState.readyUp; rdy_btn.classList.remove("text-red-600"); rdy_btn.classList.add("text-green-600"); - break ; + break; case ReadyState.readyUp: - socket.emit('readyDown'); + socket.emit("readyDown"); rdy_btn.innerText = ReadyState.readyDown; rdy_btn.classList.remove("text-green-600"); rdy_btn.classList.add("text-red-600"); - break ; + break; default: showError("error on ready btn"); } }); - socket.on('newGame', async (state) => { + socket.on("newGame", async (state) => { render(state); - - await get_opponent(state.left.id == user.id ? state.right.id : state.left.id); + + await get_opponent( + state.left.id == user.id ? state.right.id : state.left.id, + ); queueBtn.innerText = QueueState.InGame; - queueBtn.style.color = 'red'; + queueBtn.style.color = "red"; batLeft.style.backgroundColor = DEFAULT_COLOR; batRight.style.backgroundColor = DEFAULT_COLOR; if (state.left.id === user.id) { set_pretty(batLeft, playerL, playerR, SELF_COLOR); } else if (state.right.id === user.id) { set_pretty(batRight, playerR, playerL, SELF_COLOR); - } else - showError("couldn't find your id in game"); - rdy_btn.classList.remove('hidden'); + } else showError("couldn't find your id in game"); + rdy_btn.classList.remove("hidden"); rdy_btn.classList.add("text-red-600"); rdy_btn.innerText = ReadyState.readyDown; }); - socket.on('rdyEnd', () => { + socket.on("rdyEnd", () => { rdy_btn.classList.remove("text-green-600"); rdy_btn.classList.remove("text-red-600"); - rdy_btn.classList.add('hidden'); + rdy_btn.classList.add("hidden"); }); socket.on("gameEnd", (winner) => { - rdy_btn.classList.add('hidden'); + rdy_btn.classList.add("hidden"); queueBtn.innerHTML = QueueState.Iddle; - queueBtn.style.color = 'white'; + queueBtn.style.color = "white"; if (!isNullish(currentGame)) { - let new_div = document.createElement('div'); + let new_div = document.createElement("div"); let end_txt = ""; - if ((user.id === currentGame.left.id && winner === 'left') || - (user.id === currentGame.right.id && winner === 'right')) - end_txt = 'won! #yippe'; - else - end_txt = 'lost #sadge'; - new_div.innerText = 'you ' + end_txt; + if ( + (user.id === currentGame.left.id && + winner === "left") || + (user.id === currentGame.right.id && winner === "right") + ) + end_txt = "won! #yippe"; + else end_txt = "lost #sadge"; + new_div.innerText = "you " + end_txt; new_div.className = "pong-end-screen"; gameBoard.appendChild(new_div); setTimeout(() => { @@ -270,25 +385,61 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn }, 3 * 1000); if (currentGame.local) { - LocalGameBtn.innerText = "Local Game" + LocalGameBtn.innerText = "Local Game"; } } resetBoard(batLeft, batRight, playerL, playerR); - }) - socket.on('updateInformation', (e) => { + }); + socket.on("updateInformation", (e) => { queue_infos.innerText = `${e.totalUser}👤 ${e.inQueue}⏳ ${e.totalGames}▮•▮`; }); - socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); // MAYBE: play a sound? to notify user that smthing happend + socket.on("queueEvent", (e) => showInfo(`QueueEvent: ${e}`)); // MAYBE: play a sound? to notify user that smthing happend // --- // queue evt end // --- + socket.on("tournamentInfo", (s) => { + // no tournament => we can create it ! + if (s === null) { + tournamentBtn.innerText = TourBtnState.AbleToCreate; + // create tournament + return; + } + + let weIn = s.players.some((p) => p.id === user.id); + let imOwner = s.ownerId === user.id; + switch (s.state) { + case "ended": + tournamentBtn.innerText = TourBtnState.AbleToCreate; + break; + case "playing": + tournamentBtn.innerText = TourBtnState.Started; + break; + case "prestart": + if (imOwner) { + tournamentBtn.innerText = TourBtnState.AbleToStart; + } else { + tournamentBtn.innerText = weIn + ? TourBtnState.Joined + : TourBtnState.AbleToJoin; + } + break; + } + + console.log(s.players); + }); + + socket.on("tournamentRegister", ({ kind, msg }) => { + if (kind === "success") showSuccess(msg ?? "Success"); + if (kind === "failure") showError(msg ?? "An error Occured"); + }); + // init - rdy_btn.classList.add('hidden'); + rdy_btn.classList.add("hidden"); queueBtn.innerText = QueueState.Iddle; rdy_btn.innerText = ReadyState.readyUp; resetBoard(batLeft, batRight, playerL, playerR); - } - } -}; -addRoute('/pong', pongClient); + }, + }; +} +addRoute("/pong", pongClient); diff --git a/frontend/src/pages/pong/socket.ts b/frontend/src/pages/pong/socket.ts index 387f2c7..f0e1638 100644 --- a/frontend/src/pages/pong/socket.ts +++ b/frontend/src/pages/pong/socket.ts @@ -1,56 +1,80 @@ import { Socket } from 'socket.io-client'; export type UpdateInfo = { - inQueue: number, - totalUser: number, - totalGames : number -} + inQueue: number; + totalUser: number; + totalGames: number; +}; export type PaddleData = { - x: number, - y: number, + x: number; + y: number; - width: number, - height: number, + width: number; + height: number; }; export type GameUpdate = { gameId: string; - left: { id: string, paddle: PaddleData, score: number }; - right: { id: string, paddle: PaddleData, score: number }; + left: { id: string; paddle: PaddleData; score: number }; + right: { id: string; paddle: PaddleData; score: number }; - ball: { x: number, y: number, size: number }; - local: boolean, -} + ball: { x: number; y: number; size: number }; + local: boolean; +}; export type GameMove = { - move: 'up' | 'down' | null, + move: 'up' | 'down' | null; // only used in local games - moveRight: 'up' | 'down' | null, -} + moveRight: 'up' | 'down' | null; +}; + +export type TourInfo = { + ownerId: string; + state: 'prestart' | 'playing' | 'ended'; + players: { id: string; name: string; score: number }[]; + currentGameInfo: GameUpdate | null; +}; -// TODO: add new evt such as "local play", "ready-up" see: ./pong.ts export interface ClientToServer { enqueue: () => void; dequeue: () => void; readyUp: () => void; - readyDown:() => void; + readyDown: () => void; debugInfo: () => void; gameMove: (up: GameMove) => void; connectedToGame: (gameId: string) => void; - localGame: () => void, -}; + localGame: () => void; + + // TOURNAMENT + + tourRegister: () => void; + tourUnregister: () => void; + + tourCreate: () => void; +} export interface ServerToClient { forceDisconnect: (reason: string) => void; queueEvent: (msg: 'registered' | 'unregistered') => void; - rdyEnd:() => void, - updateInformation: (info: UpdateInfo) => void, - newGame: (initState: GameUpdate) => void, // <- consider this the gameProc eg not start of game but wait for client to "ready up" - gameUpdate: (state: GameUpdate) => void, + rdyEnd: () => void; + updateInformation: (info: UpdateInfo) => void; + newGame: (initState: GameUpdate) => void; + gameUpdate: (state: GameUpdate) => void; gameEnd: (winner: 'left' | 'right') => void; -}; + + // TOURNAMENT + tournamentRegister: (res: { + kind: 'success' | 'failure'; + msg?: string; + }) => void; + tournamentCreateMsg: (res: { + kind: 'success' | 'failure'; + msg?: string; + }) => void; + tournamentInfo: (info: TourInfo | null) => void; +} export type SSocket = Socket; export type CSocket = Socket; diff --git a/nginx-dev/nginx.conf b/nginx-dev/nginx.conf index abc21a6..2b6663a 100644 --- a/nginx-dev/nginx.conf +++ b/nginx-dev/nginx.conf @@ -31,6 +31,30 @@ http { proxy_ssl_verify off; proxy_pass https://localhost:8888; } + location /api/chat/socket.io/ { + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 3600s; + proxy_pass https://localhost:8888; + } + location /api/pong/socket.io/ { + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 3600s; + proxy_pass https://localhost:8888; + } + location /api/ttt/socket.io/ { + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 3600s; + proxy_pass https://localhost:8888; + } location /assets { proxy_pass http://localhost:5173; proxy_ssl_verify off; diff --git a/src/@shared/src/utils/index.ts b/src/@shared/src/utils/index.ts index 2a5946c..30648ba 100644 --- a/src/@shared/src/utils/index.ts +++ b/src/@shared/src/utils/index.ts @@ -149,3 +149,20 @@ export function escape(s: string): string { c => '&#' + c.charCodeAt(0) + ';', ); } + +export function shuffle(array: T[]) { + let currentIndex = array.length; + + // While there remain elements to shuffle... + while (currentIndex != 0) { + // Pick a remaining element... + const randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], + array[currentIndex], + ]; + } +} diff --git a/src/pong/src/socket.ts b/src/pong/src/socket.ts index 1126c2a..be065b1 100644 --- a/src/pong/src/socket.ts +++ b/src/pong/src/socket.ts @@ -1,55 +1,81 @@ import { Socket } from 'socket.io'; export type UpdateInfo = { - inQueue: number, - totalUser: number, - totalGames : number -} + inQueue: number; + totalUser: number; + totalGames: number; +}; export type PaddleData = { - x: number, - y: number, + x: number; + y: number; - width: number, - height: number, + width: number; + height: number; }; export type GameUpdate = { gameId: string; - left: { id: string, paddle: PaddleData, score: number }; - right: { id: string, paddle: PaddleData, score: number }; + left: { id: string; paddle: PaddleData; score: number }; + right: { id: string; paddle: PaddleData; score: number }; - ball: { x: number, y: number, size: number }; - local: boolean, -} + ball: { x: number; y: number; size: number }; + local: boolean; +}; export type GameMove = { - move: 'up' | 'down' | null, + move: 'up' | 'down' | null; // only used in local games - moveRight: 'up' | 'down' | null, -} + moveRight: 'up' | 'down' | null; +}; + +export type TourInfo = { + ownerId: string; + state: 'prestart' | 'playing' | 'ended'; + players: { id: string; name: string; score: number }[]; + currentGameInfo: GameUpdate | null; +}; export interface ClientToServer { enqueue: () => void; dequeue: () => void; readyUp: () => void; - readyDown:() => void; + readyDown: () => void; debugInfo: () => void; gameMove: (up: GameMove) => void; connectedToGame: (gameId: string) => void; - localGame: () => void, -}; + localGame: () => void; + + // TOURNAMENT + + tourRegister: () => void; + tourUnregister: () => void; + + tourCreate: () => void; + // tourStart: () => void; +} export interface ServerToClient { forceDisconnect: (reason: string) => void; queueEvent: (msg: 'registered' | 'unregistered') => void; - rdyEnd:() => void, - updateInformation: (info: UpdateInfo) => void, - newGame: (initState: GameUpdate) => void, - gameUpdate: (state: GameUpdate) => void, + rdyEnd: () => void; + updateInformation: (info: UpdateInfo) => void; + newGame: (initState: GameUpdate) => void; + gameUpdate: (state: GameUpdate) => void; gameEnd: (winner: 'left' | 'right') => void; -}; + + // TOURNAMENT + tournamentRegister: (res: { + kind: 'success' | 'failure'; + msg?: string; + }) => void; + tournamentCreateMsg: (res: { + kind: 'success' | 'failure'; + msg?: string; + }) => void; + tournamentInfo: (info: TourInfo | null) => void; +} export type SSocket = Socket; export type CSocket = Socket; diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts index 0987e72..9123031 100644 --- a/src/pong/src/state.ts +++ b/src/pong/src/state.ts @@ -2,9 +2,10 @@ 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'; +import { GameMove, GameUpdate, SSocket, TourInfo } from './socket'; +import { isNullish, shuffle } from '@shared/utils'; import { PongGameId, PongGameOutcome } from '@shared/database/mixin/pong'; +import { Tournament } from './tour'; type PUser = { id: UserId; @@ -23,6 +24,7 @@ class StateI { private queue: Set = new Set(); private queueInterval: NodeJS.Timeout; private games: Map = new Map(); + private tournament: Tournament | null = null; public constructor(private fastify: FastifyInstance) { this.queueInterval = setInterval(() => this.queuerFunction()); @@ -39,8 +41,71 @@ class StateI { }; } + private registerForTournament(sock: SSocket, name: string | null) { + const user = this.users.get(sock.authUser.id); + if (isNullish(user)) return; + + if (isNullish(this.tournament)) { + sock.emit('tournamentRegister', { kind: 'failure', msg: 'No tournament exists' }); + return; + } + if (this.tournament.started) { + sock.emit('tournamentRegister', { kind: 'failure', msg: 'No tournament already started' }); + return; + } + const udb = this.fastify.db.getUser(user.id); + if (isNullish(udb)) { + sock.emit('tournamentRegister', { kind: 'failure', msg: 'User not found' }); + return; + } + + this.tournament.addUser(user.id, name ?? udb.name); + sock.emit('tournamentRegister', { kind: 'success', msg: 'Registered to Tournament' }); + return; + } + + private unregisterForTournament(sock: SSocket) { + const user = this.users.get(sock.authUser.id); + if (isNullish(user)) return; + + if (isNullish(this.tournament)) { + sock.emit('tournamentRegister', { kind: 'failure', msg: 'No tournament exists' }); + return; + } + if (this.tournament.started) { + sock.emit('tournamentRegister', { kind: 'failure', msg: 'No tournament already started' }); + return; + } + + this.tournament.removeUser(user.id); + sock.emit('tournamentRegister', { kind: 'success', msg: 'Unregistered to Tournament' }); + return; + } + + private createTournament(sock: SSocket) { + const user = this.users.get(sock.authUser.id); + if (isNullish(user)) return; + + if (this.tournament !== null) { + sock.emit('tournamentCreateMsg', { kind: 'failure', msg: 'A tournament already exists' }); + return ; + } + + this.tournament = new Tournament(user.id); + this.registerForTournament(sock, null); + } + + private tournamentStart(sock: SSocket) { + if (isNullish(this.tournament)) return; + const user = this.users.get(sock.authUser.id); + if (isNullish(user)) return; + + this.tournament.start(); + } + private queuerFunction(): void { const values = Array.from(this.queue.values()); + shuffle(values); while (values.length >= 2) { const id1 = values.pop(); const id2 = values.pop(); @@ -80,7 +145,7 @@ class StateI { this.gameUpdate(gameId, u1.socket); this.gameUpdate(gameId, u2.socket); } - if (g.checkWinner() !== null) {this.cleanupGame(gameId, g); } + if (g.checkWinner() !== null) { this.cleanupGame(gameId, g); } }, 1000 / StateI.UPDATE_INTERVAL_FRAMES); } } @@ -148,7 +213,7 @@ class StateI { socket, id: socket.authUser.id, windowId: socket.id, - updateInterval: setInterval(() => this.updateClient(socket), 3000), + updateInterval: setInterval(() => this.updateClient(socket), 100), currentGame: null, }); this.fastify.log.info('Registered new user'); @@ -162,6 +227,12 @@ class StateI { socket.on('gameMove', (e) => this.gameMove(socket, e)); socket.on('localGame', () => this.newLocalGame(socket)); + + // todo: allow passing nickname + socket.on('tourRegister', () => this.registerForTournament(socket, null)); + socket.on('tourUnregister', () => this.unregisterForTournament(socket)); + + socket.on('tourCreate', () => this.createTournament(socket)); } private updateClient(socket: SSocket): void { @@ -170,6 +241,21 @@ class StateI { totalUser: this.users.size, totalGames: this.games.size, }); + let tourInfo: TourInfo | null = null; + if (this.tournament !== null) { + tourInfo = { + ownerId: this.tournament.owner, + state: this.tournament.started ? 'playing' : 'prestart', + players: this.tournament.users.values().toArray(), + currentGameInfo: (() => { + if (this.tournament.currentGame === null) return null; + const game = this.games.get(this.tournament.currentGame); + if (isNullish(game)) return null; + return StateI.getGameUpdateData(this.tournament.currentGame, game); + })(), + }; + } + socket.emit('tournamentInfo', tourInfo); } private cleanupUser(socket: SSocket): void { @@ -203,20 +289,20 @@ class StateI { this.fastify.db.setPongGameOutcome(gameId, { id: game.userLeft, score: game.score[0] }, { id: game.userRight, score: game.score[1] }, outcome, game.local); this.fastify.log.info('SetGameOutcome !'); if (!game.local) { - const payload = { 'nextGame':chat_text }; + const payload = { 'nextGame': chat_text }; try { const resp = await fetch('http://app-chat/api/chat/broadcast', { - method:'POST', - headers:{ 'Content-type':'application/json' }, + method: 'POST', + headers: { 'Content-type': 'application/json' }, body: JSON.stringify(payload), }); - if (!resp.ok) { throw (resp); } + if (!resp.ok) { throw (resp); } else { this.fastify.log.info('game-end info to chat success'); } } // disable eslint for err catching // eslint-disable-next-line @typescript-eslint/no-explicit-any - catch (e : any) { + catch (e: any) { this.fastify.log.error(`game-end info to chat failed: ${e}`); } } @@ -243,7 +329,7 @@ class StateI { socket.emit('queueEvent', 'unregistered'); } - private readydownUser(socket: SSocket) : void { + private readydownUser(socket: SSocket): void { // do we know this user ? if (!this.users.has(socket.authUser.id)) return; const user = this.users.get(socket.authUser.id)!; @@ -254,7 +340,7 @@ class StateI { if (game.local === true) return; game.readydown(user.id); } - private readyupUser(socket: SSocket) : void { + private readyupUser(socket: SSocket): void { // do we know this user ? if (!this.users.has(socket.authUser.id)) return; const user = this.users.get(socket.authUser.id)!; diff --git a/src/pong/src/tour.ts b/src/pong/src/tour.ts new file mode 100644 index 0000000..d2656f7 --- /dev/null +++ b/src/pong/src/tour.ts @@ -0,0 +1,54 @@ +import { PongGameId } from '@shared/database/mixin/pong'; +import { UserId } from '@shared/database/mixin/user'; +import { shuffle } from '@shared/utils'; +import { newUUID } from '@shared/utils/uuid'; +import { Pong } from './game'; + +type TourUser = { + id: UserId; + score: number; + name: string; +}; + +export class Tournament { + public users: Map = new Map(); + public currentGame: PongGameId | null = null; + public games: Map = new Map(); + public started: boolean = false; + public gameUpdate: NodeJS.Timeout | undefined; + + constructor(public owner: UserId) { } + + public addUser(id: UserId, name: string) { + if (this.started) return; + this.users.set(id, { id, name, score: 0 }); + } + + public removeUser(id: UserId) { + if (this.started) return; + this.users.delete(id); + } + + public start() { + this.started = true; + const users = Array.from(this.users.keys()); + const comb: [UserId, UserId][] = []; + + for (let i = 0; i < users.length; i++) { + for (let j = i + 1; j < users.length; j++) { + comb.push([users[i], users[j]]); + } + } + + shuffle(comb); + comb.forEach(shuffle); + + comb.forEach(([u1, u2]) => { + const gameId = newUUID() as PongGameId; + const g = new Pong(u1, u2); + + this.games.set(gameId, g); + }); + this.currentGame = this.games.keys().next().value ?? null; + } +}