feat(tour): added tournament broadcast

This commit is contained in:
Maieul BOYER 2026-01-14 15:55:39 +01:00 committed by Nigel
parent eccc763644
commit 8e6ff3aedc
10 changed files with 157 additions and 41 deletions

View file

@ -39,7 +39,7 @@
<div id="game-modal" class="gamePopup hidden"> <div id="game-modal" class="gamePopup hidden">
<div class="popUpMessage"> <div class="popUpMessage">
<div id="game-info"> <div id="game-info">
<p class="modal-messages" id="modal-message"></p> <p class="" id="modal-message"></p>
<button id="close-modal-message" class="btn-style relative bottom-8 left-85">Close</button> <button id="close-modal-message" class="btn-style relative bottom-8 left-85">Close</button>
</div> </div>
</div> </div>

View file

@ -296,6 +296,11 @@ function initChatSocket() {
socket.on("nextGame", (message: string) => { socket.on("nextGame", (message: string) => {
openMessagePopup(message); openMessagePopup(message);
}); });
//receives broadcast of the next GAME
socket.on("tourStatus", (message: string) => {
openMessagePopup(message);
});
socket.on("listBud", async (myBuddies: string[]) => { socket.on("listBud", async (myBuddies: string[]) => {
const buddies = document.getElementById( const buddies = document.getElementById(

View file

@ -5,16 +5,14 @@ import { incrementCounter } from "./incrementCounter";
// return count; // return count;
// } // }
export async function openMessagePopup(message: any) { export async function openMessagePopup(message: string) {
const modalmessage = document.getElementById("modal-message") ?? null; const modalmessage = document.getElementById("modal-message") ?? null;
if(!message) return if(!message) return
const obj = message; const obj = message;
if (modalmessage) { if (modalmessage) {
const messageElement = document.createElement("div"); const messageElement = document.createElement("div");
messageElement.innerHTML = ` messageElement.innerHTML = `<div id="profile-about">${message}</div>`;
<div id="profile-about">Game-Info: ${incrementCounter()}: ${obj.nextGame}</div>
`;
modalmessage.appendChild(messageElement); modalmessage.appendChild(messageElement);
modalmessage.scrollTop = modalmessage.scrollHeight; modalmessage.scrollTop = modalmessage.scrollHeight;
} }

View file

@ -82,6 +82,7 @@ declare module 'fastify' {
blockBtn: (data: blockedUnBlocked) => void; blockBtn: (data: blockedUnBlocked) => void;
isBlockdBtn: (data: blockedUnBlocked) => void; isBlockdBtn: (data: blockedUnBlocked) => void;
check_Block_button: (data: blockedUnBlocked) => void; check_Block_button: (data: blockedUnBlocked) => void;
tourStatus: (data: string) => void;
}>; }>;
} }
} }
@ -370,4 +371,4 @@ async function onReady(fastify: FastifyInstance) {
}); });
} }

View file

@ -6,16 +6,13 @@ import { clientChat } from './app';
* @param fastify * @param fastify
* @param gameLink * @param gameLink
*/ */
export async function broadcastNextGame(fastify: FastifyInstance, gameLink?: Promise<string>) { export async function broadcastNextGame(fastify: FastifyInstance, gameLink: string) {
const link = gameLink ? await gameLink : undefined;
const sockets = await fastify.io.fetchSockets(); const sockets = await fastify.io.fetchSockets();
for (const socket of sockets) { for (const socket of sockets) {
const clientInfo = clientChat.get(socket.id); const clientInfo = clientChat.get(socket.id);
if (!clientInfo?.user) { if (!clientInfo?.user) {
continue; continue;
} }
if (link) { socket.emit('nextGame', gameLink);
socket.emit('nextGame', link);
}
} }
}; };

View file

@ -0,0 +1,21 @@
import { FastifyInstance } from 'fastify';
import { clientChat } from './app';
/**
* function broadcast a clickable link
* @param fastify
* @param gameLink
*/
export async function broadcastTourStatus(fastify: FastifyInstance, gameLink?: string) {
const link = gameLink;
const sockets = await fastify.io.fetchSockets();
for (const socket of sockets) {
const clientInfo = clientChat.get(socket.id);
if (!clientInfo?.user) {
continue;
}
if (link) {
socket.emit('tourStatus', link);
}
}
};

View file

@ -3,7 +3,7 @@ import { Static, Type } from 'typebox';
import { broadcastNextGame } from '../broadcastNextGame'; import { broadcastNextGame } from '../broadcastNextGame';
export const ChatReq = Type.Object({ export const ChatReq = Type.Object({
message: Type.String(), nextGame: Type.String(),
}); });
export type ChatReq = Static<typeof ChatReq>; export type ChatReq = Static<typeof ChatReq>;
@ -27,21 +27,14 @@ export type ChatReq = Static<typeof ChatReq>;
// export default route; // export default route;
const route: FastifyPluginAsync = async (fastify): Promise<void> => { const route: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/api/chat/broadcast', { fastify.post<{ Body: ChatReq }>('/broadcastNextGame', {
schema: { schema: {
body: { body: ChatReq,
type: 'object', hide: true,
required: ['nextGame'],
properties: {
nextGame: { type: 'string' },
},
},
}, },
}, async (req, reply) => { }, async function(req, reply) {
const gameLink: Promise<string> = Promise.resolve(req.body as string); this.log.info({ msg: 'Broadcasting nextGame status', ...req.body });
if (gameLink) { broadcastNextGame(fastify, req.body.nextGame);
broadcastNextGame(fastify, gameLink);
}
return reply.send({ status: 'ok' }); return reply.send({ status: 'ok' });
}); });
}; };

View file

@ -0,0 +1,44 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from 'typebox';
import { broadcastTourStatus } from '../broadcastTourStatus';
export const ChatReq = Type.Object({
message: Type.String(),
});
export type ChatReq = Static<typeof ChatReq>;
// const route: FastifyPluginAsync = async (fastify): Promise<void> => {
// fastify.post<{ Body: ChatReq }>(
// '/api/chat/broadcast',
// {
// schema: {
// body: ChatReq,
// hide: true,
// },
// config: { requireAuth: false },
// },
// async function(req, res) {
// // broadcast(this, { command: '', destination: '', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' });
// void res;
// },
// );
// };
// export default route;
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<{ Body: ChatReq }>('/broadcastTourStatus', {
schema: {
body: ChatReq,
hide: true,
},
}, async function(req, reply) {
const gameLink: string = req.body.message;
if (gameLink) {
broadcastTourStatus(fastify, gameLink);
}
return reply.send({ status: 'ok' });
});
};
export default route;

View file

@ -90,8 +90,12 @@ class StateI {
g.rdy_timer = Date.now(); g.rdy_timer = Date.now();
this.games.set(gameId, g); this.games.set(gameId, g);
if (u1) { u1.currentGame = gameId; } if (u1) {
if (u2) { u2.currentGame = gameId; } u1.currentGame = gameId;
}
if (u2) {
u2.currentGame = gameId;
}
g.gameUpdate = setInterval(() => { g.gameUpdate = setInterval(() => {
g.tick(); g.tick();
@ -193,6 +197,26 @@ class StateI {
return; return;
} }
public async broadcastTourStatus(message: string) {
try {
const resp = await fetch('http://app-chat/broadcastTourStatus', {
method: 'POST',
headers: { 'Content-type': 'application/json' },
body: JSON.stringify({ message }),
});
if (!resp.ok) {
throw resp;
}
else {
this.fastify.log.info('tour-status info to chat success');
}
}
catch (e: unknown) {
this.fastify.log.error(`tour-status info to chat failed: ${e}`);
}
}
private createTournament(sock: SSocket) { private createTournament(sock: SSocket) {
const user = this.users.get(sock.authUser.id); const user = this.users.get(sock.authUser.id);
if (isNullish(user)) return; if (isNullish(user)) return;
@ -215,7 +239,14 @@ class StateI {
private cleanupTournament() { private cleanupTournament() {
if (this.tournament === null) return; if (this.tournament === null) return;
if (this.tournament.state === 'ended') { this.fastify.db.createNewTournamentById(this.tournament.owner, this.tournament.users.values().toArray(), this.tournament.games); } if (this.tournament.state === 'ended') {
this.fastify.db.createNewTournamentById(
this.tournament.owner,
this.tournament.users.values().toArray(),
this.tournament.games,
);
this.broadcastTourStatus('The tournament has ended');
}
this.tournament = null; this.tournament = null;
this.fastify.log.info('Tournament has been ended'); this.fastify.log.info('Tournament has been ended');
} }
@ -231,8 +262,15 @@ class StateI {
} }
public newPausedGame(suid1: string, suid2: string): GameId | undefined { public newPausedGame(suid1: string, suid2: string): GameId | undefined {
this.fastify.log.info('new game request: suid1: ' + suid1 + '\tsuid2: ' + suid2); this.fastify.log.info(
if (!this.fastify.db.getUser(suid1) || !this.fastify.db.getUser(suid2)) { return undefined; } 'new game request: suid1: ' + suid1 + '\tsuid2: ' + suid2,
);
if (
!this.fastify.db.getUser(suid1) ||
!this.fastify.db.getUser(suid2)
) {
return undefined;
}
this.fastify.log.info('new paused start'); this.fastify.log.info('new paused start');
const uid1: UserId = suid1 as UserId; const uid1: UserId = suid1 as UserId;
const uid2: UserId = suid2 as UserId; const uid2: UserId = suid2 as UserId;
@ -313,7 +351,9 @@ class StateI {
const user = this.users.get(sock.authUser.id); const user = this.users.get(sock.authUser.id);
if (!user) return; if (!user) return;
if (this.tournament && this.tournament.users.has(sock.authUser.id)) return; if (this.tournament && this.tournament.users.has(sock.authUser.id)) {
return;
}
const gameId = newUUID() as unknown as GameId; const gameId = newUUID() as unknown as GameId;
const g = Pong.makeLocal(user.id); const g = Pong.makeLocal(user.id);
@ -388,19 +428,28 @@ class StateI {
if (this.games.has(game_id) === false) { if (this.games.has(game_id) === false) {
this.fastify.log.warn('gameId:' + g_id + ' is unknown!'); this.fastify.log.warn('gameId:' + g_id + ' is unknown!');
return (JoinRes.no); return JoinRes.no;
} }
const game: Pong = this.games.get(game_id)!; const game: Pong = this.games.get(game_id)!;
if (game.local === true || (game.userLeft !== sock.authUser.id && game.userRight !== sock.authUser.id)) { if (
this.fastify.log.warn('user trying to connect to a game he\'s not part of: gameId:' + g_id + ' userId:' + sock.authUser.id); game.local === true ||
return (JoinRes.no); (game.userLeft !== sock.authUser.id &&
game.userRight !== sock.authUser.id)
) {
this.fastify.log.warn(
'user trying to connect to a game he\'s not part of: gameId:' +
g_id +
' userId:' +
sock.authUser.id,
);
return JoinRes.no;
} }
game.userOnPage[game.userLeft === sock.authUser.id ? 0 : 1] = true; game.userOnPage[game.userLeft === sock.authUser.id ? 0 : 1] = true;
if (game.userOnPage[0] === game.userOnPage[1]) { if (game.userOnPage[0] === game.userOnPage[1]) {
this.fastify.log.info('Paused game start: gameId:' + g_id); this.fastify.log.info('Paused game start: gameId:' + g_id);
this.initGame(game, game_id, game.userLeft, game.userRight); this.initGame(game, game_id, game.userLeft, game.userRight);
} }
return (JoinRes.yes); return JoinRes.yes;
} }
public registerUser(socket: SSocket): void { public registerUser(socket: SSocket): void {
@ -433,7 +482,9 @@ class StateI {
socket.on('gameMove', (e) => this.gameMove(socket, e)); socket.on('gameMove', (e) => this.gameMove(socket, e));
socket.on('localGame', () => this.newLocalGame(socket)); socket.on('localGame', () => this.newLocalGame(socket));
socket.on('joinGame', (g_id, ack) => { return (ack(this.tryJoinGame(g_id, socket))); }); socket.on('joinGame', (g_id, ack) => {
return ack(this.tryJoinGame(g_id, socket));
});
// todo: allow passing nickname // todo: allow passing nickname
socket.on('tourRegister', () => socket.on('tourRegister', () =>
this.registerForTournament(socket, null), this.registerForTournament(socket, null),
@ -522,7 +573,7 @@ class StateI {
if (!game.local) { if (!game.local) {
const payload = { nextGame: chat_text }; const payload = { nextGame: chat_text };
try { try {
const resp = await fetch('http://app-chat/api/chat/broadcast', { const resp = await fetch('http://app-chat/broadcastNextGame', {
method: 'POST', method: 'POST',
headers: { 'Content-type': 'application/json' }, headers: { 'Content-type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
@ -548,7 +599,9 @@ class StateI {
if (this.users.get(socket.authUser.id)?.currentGame !== null) return; if (this.users.get(socket.authUser.id)?.currentGame !== null) return;
if (this.tournament && this.tournament.users.has(socket.authUser.id)) return; if (this.tournament && this.tournament.users.has(socket.authUser.id)) {
return;
}
this.queue.add(socket.authUser.id); this.queue.add(socket.authUser.id);
socket.emit('queueEvent', 'registered'); socket.emit('queueEvent', 'registered');

View file

@ -19,7 +19,9 @@ export class Tournament {
public startTimeout: NodeJS.Timeout | undefined; public startTimeout: NodeJS.Timeout | undefined;
public games: PongGameId[] = []; public games: PongGameId[] = [];
constructor(public owner: UserId) { } constructor(public owner: UserId) {
State.broadcastTourStatus('A new Tournament has been created');
}
public addUser(id: UserId, name: string) { public addUser(id: UserId, name: string) {
if (this.state !== 'prestart') return; if (this.state !== 'prestart') return;
@ -35,6 +37,7 @@ export class Tournament {
if (this.state !== 'prestart') return; if (this.state !== 'prestart') return;
if (this.users.size < 2) { if (this.users.size < 2) {
this.state = 'canceled'; this.state = 'canceled';
State.broadcastTourStatus('The tournament has been canceled !');
return; return;
} }
this.state = 'playing'; this.state = 'playing';
@ -54,6 +57,7 @@ export class Tournament {
comb.forEach(([u1, u2]) => { result.push([u1, u2]); }); comb.forEach(([u1, u2]) => { result.push([u1, u2]); });
this.matchup = result; this.matchup = result;
this.setupNextGame(); this.setupNextGame();
State.broadcastTourStatus('The tournament Started !');
} }
private setupNextGame() { private setupNextGame() {