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