feat(tournament): allow the creation of a tournament
A tournament can be created (by the "owner") Any other players can join said tournament. The information is currently not displayed in the frontend, but does exists and is passed to the frontend using a socket.io event
This commit is contained in:
parent
272c6f319c
commit
2195207297
9 changed files with 558 additions and 170 deletions
|
|
@ -149,3 +149,20 @@ export function escape(s: string): string {
|
|||
c => '&#' + c.charCodeAt(0) + ';',
|
||||
);
|
||||
}
|
||||
|
||||
export function shuffle<T>(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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ClientToServer, ServerToClient>;
|
||||
export type CSocket = Socket<ServerToClient, ClientToServer>;
|
||||
|
|
|
|||
|
|
@ -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<UserId> = new Set();
|
||||
private queueInterval: NodeJS.Timeout;
|
||||
private games: Map<GameId, Pong> = 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)!;
|
||||
|
|
|
|||
54
src/pong/src/tour.ts
Normal file
54
src/pong/src/tour.ts
Normal file
|
|
@ -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<UserId, TourUser> = new Map();
|
||||
public currentGame: PongGameId | null = null;
|
||||
public games: Map<PongGameId, Pong> = 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue