diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index ceed17c165e468ddab1ffdaa5d6737dbc7a80426..601f051a9250e9e154834950cb6f053047c9e05b 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -20,8 +20,8 @@ export const routes: Routes = [ loadComponent: async () => (await import('@app/pages/game/create-game-page/create-game.component')).CreateGamePageComponent, }, { - path: 'wait', - loadComponent: async () => (await import('@app/pages/game/wait-page/wait-page.component')).WaitPageComponent, + path: 'lobby', + loadComponent: async () => (await import('@app/pages/game/lobby-page/lobby-page.component')).LobbyPageComponent, }, { path: 'play', diff --git a/client/src/app/components/topbar/playing-actions/playing-actions.component.html b/client/src/app/components/topbar/playing-actions/playing-actions.component.html index 0259f0c9c7fe1fe0ff3ee4b304e9ff91e44c4f0d..f34a1384928bdb609a7f2f5068c6cefa10339d49 100644 --- a/client/src/app/components/topbar/playing-actions/playing-actions.component.html +++ b/client/src/app/components/topbar/playing-actions/playing-actions.component.html @@ -1,3 +1,3 @@ -<button mat-icon-button routerLink="/home" matTooltip="Abandonner la partie"> +<button mat-icon-button (click)="leave()" matTooltip="Abandonner la partie"> <mat-icon>logout</mat-icon> </button> diff --git a/client/src/app/components/topbar/playing-actions/playing-actions.component.spec.ts b/client/src/app/components/topbar/playing-actions/playing-actions.component.spec.ts index fc6e456366f6243befa0368da08c1c837cb8c0ae..ef1bca4c576e8b0c921ffffe58c389259d535dbb 100644 --- a/client/src/app/components/topbar/playing-actions/playing-actions.component.spec.ts +++ b/client/src/app/components/topbar/playing-actions/playing-actions.component.spec.ts @@ -1,19 +1,29 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { PlayingActionsComponent } from './playing-actions.component'; -import { ActivatedRoute } from '@angular/router'; +import { Router } from '@angular/router'; +import { GameService } from '@app/services/game.service'; describe('PlayingActionsComponent', () => { let component: PlayingActionsComponent; let fixture: ComponentFixture<PlayingActionsComponent>; + let gameService: jasmine.SpyObj<GameService>; + let router: jasmine.SpyObj<Router>; + beforeEach(() => { + gameService = jasmine.createSpyObj('GameService', ['leave']); + router = jasmine.createSpyObj('Router', ['navigate']); + TestBed.configureTestingModule({ imports: [PlayingActionsComponent], providers: [ { - provide: ActivatedRoute, - useValue: {}, + provide: GameService, + useValue: gameService, + }, + { + provide: Router, + useValue: router, }, ], }); @@ -25,4 +35,16 @@ describe('PlayingActionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('leave', () => { + it('should call gameService.leave', () => { + component.leave(); + expect(gameService.leave).toHaveBeenCalled(); + }); + + it('should navigate to /game/create', () => { + component.leave(); + expect(router.navigate).toHaveBeenCalledWith(['/', 'game', 'create']); + }); + }); }); diff --git a/client/src/app/components/topbar/playing-actions/playing-actions.component.ts b/client/src/app/components/topbar/playing-actions/playing-actions.component.ts index eb866167d32b4c94fb2e2649769e2ba3e909d543..816b77ec5d44347c387daca3026f3b9508676fe4 100644 --- a/client/src/app/components/topbar/playing-actions/playing-actions.component.ts +++ b/client/src/app/components/topbar/playing-actions/playing-actions.component.ts @@ -1,14 +1,25 @@ import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { Router } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { GameService } from '@app/services/game.service'; @Component({ selector: 'app-playing-actions', standalone: true, - imports: [RouterLink, MatButtonModule, MatIconModule, MatTooltipModule], + imports: [MatButtonModule, MatIconModule, MatTooltipModule], templateUrl: './playing-actions.component.html', styleUrls: ['./playing-actions.component.scss'], }) -export class PlayingActionsComponent {} +export class PlayingActionsComponent { + constructor( + private gameService: GameService, + private router: Router, + ) {} + + leave(): void { + this.gameService.leave(); + this.router.navigate(['/', 'game', 'create']); + } +} diff --git a/client/src/app/components/topbar/topbar.component.ts b/client/src/app/components/topbar/topbar.component.ts index aefeff197ab63a2202af07a65baca2d912b0a485..af5f328aa7b45a09321f54af15d04aed81197b0d 100644 --- a/client/src/app/components/topbar/topbar.component.ts +++ b/client/src/app/components/topbar/topbar.component.ts @@ -23,7 +23,7 @@ export class TopbarComponent { } isPlayingPage(): boolean { - return this.router.url === '/game/play'; + return this.router.url === '/game/play' || this.router.url === '/game/lobby'; } showHomeButton(): boolean { diff --git a/client/src/app/pages/game/wait-page/wait-page.component.html b/client/src/app/pages/game/lobby-page/lobby-page.component.html similarity index 100% rename from client/src/app/pages/game/wait-page/wait-page.component.html rename to client/src/app/pages/game/lobby-page/lobby-page.component.html diff --git a/client/src/app/pages/game/wait-page/wait-page.component.scss b/client/src/app/pages/game/lobby-page/lobby-page.component.scss similarity index 100% rename from client/src/app/pages/game/wait-page/wait-page.component.scss rename to client/src/app/pages/game/lobby-page/lobby-page.component.scss diff --git a/client/src/app/pages/game/wait-page/wait-page.component.spec.ts b/client/src/app/pages/game/lobby-page/lobby-page.component.spec.ts similarity index 84% rename from client/src/app/pages/game/wait-page/wait-page.component.spec.ts rename to client/src/app/pages/game/lobby-page/lobby-page.component.spec.ts index 7a7bf6253a36e878c5d0596a16e4f68b732b8efc..6b219052d5cbba8416c76ffad746b4466b734f08 100644 --- a/client/src/app/pages/game/wait-page/wait-page.component.spec.ts +++ b/client/src/app/pages/game/lobby-page/lobby-page.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { WaitPageComponent } from './wait-page.component'; +import { LobbyPageComponent } from './lobby-page.component'; import { Game } from '@common/game'; import { GameService } from '@app/services/game.service'; import { Router } from '@angular/router'; -describe('WaitPageComponent', () => { - let component: WaitPageComponent; - let fixture: ComponentFixture<WaitPageComponent>; +describe('LobbyPageComponent', () => { + let component: LobbyPageComponent; + let fixture: ComponentFixture<LobbyPageComponent>; let gameService: jasmine.SpyObj<GameService>; let router: jasmine.SpyObj<Router>; @@ -16,7 +16,7 @@ describe('WaitPageComponent', () => { router = jasmine.createSpyObj('Router', ['navigate']); TestBed.configureTestingModule({ - imports: [WaitPageComponent], + imports: [LobbyPageComponent], providers: [ { provide: GameService, @@ -28,7 +28,7 @@ describe('WaitPageComponent', () => { }, ], }); - fixture = TestBed.createComponent(WaitPageComponent); + fixture = TestBed.createComponent(LobbyPageComponent); component = fixture.componentInstance; component.game = {} as Game; diff --git a/client/src/app/pages/game/wait-page/wait-page.component.ts b/client/src/app/pages/game/lobby-page/lobby-page.component.ts similarity index 80% rename from client/src/app/pages/game/wait-page/wait-page.component.ts rename to client/src/app/pages/game/lobby-page/lobby-page.component.ts index a0ec0af8d6456c21ebe9ddcb47e239f4a7261922..0ee218ebccbe5c7b41d07f9c096d166542719e79 100644 --- a/client/src/app/pages/game/wait-page/wait-page.component.ts +++ b/client/src/app/pages/game/lobby-page/lobby-page.component.ts @@ -7,13 +7,13 @@ import { GameService } from '@app/services/game.service'; import { Game } from '@common/game'; @Component({ - selector: 'app-wait-page', + selector: 'app-lobby-page', standalone: true, imports: [AsyncPipe, MatCardModule, MatDividerModule], - templateUrl: './wait-page.component.html', - styleUrls: ['./wait-page.component.scss'], + templateUrl: './lobby-page.component.html', + styleUrls: ['./lobby-page.component.scss'], }) -export class WaitPageComponent implements OnInit { +export class LobbyPageComponent implements OnInit { game: Game = this.gameService.game; constructor( diff --git a/client/src/app/services/game.service.spec.ts b/client/src/app/services/game.service.spec.ts index a48cffa8dbb8947974baf5318e287c77536bed36..d497ea79906196685ae0a4f0f7c7a2d41dcf0227 100644 --- a/client/src/app/services/game.service.spec.ts +++ b/client/src/app/services/game.service.spec.ts @@ -61,7 +61,7 @@ describe('GameService', () => { await callback(game); - expect(router.navigate).toHaveBeenCalledWith(['/', 'game', 'wait']); + expect(router.navigate).toHaveBeenCalledWith(['/', 'game', 'lobby']); }); }); @@ -124,4 +124,12 @@ describe('GameService', () => { expect(gameSocketService.send).toHaveBeenCalledWith(GameClientEvent.Demo, quizId); }); }); + + describe('leave', () => { + it('should send a leave event', () => { + service.leave(); + + expect(gameSocketService.send).toHaveBeenCalledWith(GameClientEvent.Leave); + }); + }); }); diff --git a/client/src/app/services/game.service.ts b/client/src/app/services/game.service.ts index d6b85312f1b81bd7022bb57b80679cf2e28afd84..f179833b97b81c9db665fef3a73f2b95f869b1bb 100644 --- a/client/src/app/services/game.service.ts +++ b/client/src/app/services/game.service.ts @@ -19,7 +19,7 @@ export class GameService { setupWaitingRoomListener(): void { this.gameSocketService.on(GameServerEvent.RedirectToWaitingRoom, async (game: Game) => { this.game = game; - await this.router.navigate(['/', 'game', 'wait']); + await this.router.navigate(['/', 'game', 'lobby']); }); } @@ -43,4 +43,8 @@ export class GameService { // eslint-disable-next-line no-underscore-dangle -- mongodb this.gameSocketService.send(GameClientEvent.Demo, quiz._id); } + + leave(): void { + this.gameSocketService.send(GameClientEvent.Leave); + } } diff --git a/common/events/game.events.ts b/common/events/game.events.ts index 93af3d60611001e3075e7dd4418f30aa9190fb53..e15435d06e1fae6e8f040d4e899d7225dba59b21 100644 --- a/common/events/game.events.ts +++ b/common/events/game.events.ts @@ -12,6 +12,7 @@ export enum GameClientEvent { Create = 'create', Demo = 'demo', Answer = 'answer', + Leave = 'leave', } export type GameEvent = GameServerEvent | GameClientEvent; diff --git a/server/app/game/classes/room.class.spec.ts b/server/app/game/classes/room.class.spec.ts index 4e1dfdf922e47b3f51b7b480075eacc498591ef1..b2d4a29a988538119219a26cf3eeef77c679a66d 100644 --- a/server/app/game/classes/room.class.spec.ts +++ b/server/app/game/classes/room.class.spec.ts @@ -6,10 +6,13 @@ import type { DefaultEventsMap } from 'socket.io/dist/typed-events'; import * as sinon from 'sinon'; import { QuestionType } from '@common/question'; import { WsException } from '@nestjs/websockets'; +import { Writeable } from '@app/core/types/testing'; jest.useFakeTimers(); describe('Room', () => { + let room: Room; + let server: sinon.SinonStubbedInstance<Server>; let serverRoom: sinon.SinonStubbedInstance<Socket>; @@ -17,22 +20,35 @@ describe('Room', () => { server = sinon.createStubInstance<Server>(Server); serverRoom = sinon.createStubInstance<Socket>(Socket); server.to.callsFake(() => serverRoom as unknown as BroadcastOperator<DefaultEventsMap, unknown>); + + room = new Room( + { id: 'gameid' } as Game, + { + questions: [ + { + type: QuestionType.Qcm, + choices: [ + { text: 'choice1', isCorrect: true }, + { text: 'choice2', isCorrect: false }, + ], + }, + ], + } as unknown as Quiz, + server, + ); }); it('should be defined', () => { - const room = new Room({} as Game, {} as Quiz, server); expect(room).toBeDefined(); }); describe('addPlayer', () => { it('should add a player', () => { - const room = new Room({} as Game, {} as Quiz, server); - const client = sinon.createStubInstance(Socket); room.addPlayer(client, 'player1'); - expect(client.join.calledOnceWithExactly(room.id)).toBe(true); + expect(client.join.calledOnceWithExactly(room.game.id)).toBe(true); const players = room['players']; expect(players.size).toBe(1); @@ -40,9 +56,24 @@ describe('Room', () => { }); }); + describe('removePlayer', () => { + it('should remove a player', () => { + const client = sinon.createStubInstance(Socket); + (client as Writeable<Socket>).id = 'playerid'; + room['players'].set(client.id, { username: 'player1', score: 0 }); + + room.removePlayer(client); + + expect(client.leave.calledOnceWithExactly(room.game.id)).toBe(true); + + const players = room['players']; + expect(players.size).toBe(0); + expect(players.get(client.id)).toBeUndefined(); + }); + }); + describe('start', () => { it('should start the game after 3 seconds', () => { - const room = new Room({} as Game, {} as Quiz, server); const nextSpy = sinon.stub(room, 'next'); room.start(); @@ -56,25 +87,9 @@ describe('Room', () => { }); describe('next', () => { - let room: Room; let timesUpStub: sinon.SinonStub; beforeEach(() => { - room = new Room( - { id: 'gameid' } as Game, - { - questions: [ - { - type: QuestionType.Qcm, - choices: [ - { text: 'choice1', isCorrect: true }, - { text: 'choice2', isCorrect: false }, - ], - }, - ], - } as unknown as Quiz, - server, - ); room['currentQuestionIndex'] = -1; timesUpStub = sinon.stub(room, 'timesUp'); @@ -130,7 +145,6 @@ describe('Room', () => { }); describe('answer', () => { - let room: Room; let sendResultsStub: sinon.SinonStub; beforeEach(() => { @@ -181,28 +195,29 @@ describe('Room', () => { describe('timesUp', () => { it('should emit TimesUp', () => { - const room = new Room({} as Game, {} as Quiz, server); - room['currentQuestionIndex'] = 0; room.timesUp(); - expect(server.to.calledOnceWith(room.id)).toBe(true); + expect(server.to.calledOnceWith(room.game.id)).toBe(true); expect(serverRoom.emit.calledOnceWith('times-up')).toBe(true); }); }); describe('sendResults', () => { - let room: Room; let nextStub: sinon.SinonStub; beforeEach(() => { - room = new Room( - {} as Game, - { - questions: [{ type: QuestionType.Qcm, points: 100, choices: [{ isCorrect: true }, { isCorrect: false }, { isCorrect: true }] }], - } as unknown as Quiz, - server, - ); + room['quiz'].questions[0] = { + _id: 'questionid', + type: QuestionType.Qcm, + text: 'question', + points: 100, + choices: [ + { isCorrect: true, text: '1' }, + { isCorrect: false, text: '2' }, + { isCorrect: true, text: '3' }, + ], + }; room['currentQuestionIndex'] = 0; room['players'] = new Map([ ['playerid1', { username: 'player1', score: 10 }], @@ -215,7 +230,7 @@ describe('Room', () => { it('should emit the solution with a QCM', () => { room.sendResults(); - expect(server.to.calledOnceWith(room.id)).toBe(true); + expect(server.to.calledOnceWith(room.game.id)).toBe(true); expect(serverRoom.emit.calledOnce).toBe(true); expect(serverRoom.emit.calledOnceWith('results')).toBe(true); @@ -235,7 +250,7 @@ describe('Room', () => { room.sendResults(); - expect(server.to.calledOnceWith(room.id)).toBe(true); + expect(server.to.calledOnceWith(room.game.id)).toBe(true); expect(serverRoom.emit.calledOnce).toBe(true); expect(serverRoom.emit.calledOnceWith('results')).toBe(true); @@ -265,10 +280,8 @@ describe('Room', () => { describe('end', () => { it('should emit EndGame', () => { - const room = new Room({} as Game, {} as Quiz, server); - room.end(); - expect(server.to.calledOnceWith(room.id)).toBe(true); + expect(server.to.calledOnceWith(room.game.id)).toBe(true); expect(serverRoom.emit.calledOnceWith('end-game')).toBe(true); }); }); diff --git a/server/app/game/classes/room.class.ts b/server/app/game/classes/room.class.ts index 13ffe34c2a440c3cca7bc9cd4608d42ec9074c81..86c8581899ab5616df59abf4d010f1307ea1c348 100644 --- a/server/app/game/classes/room.class.ts +++ b/server/app/game/classes/room.class.ts @@ -9,8 +9,6 @@ import { DELAY_BETWEEN_QUESTIONS_MS, S_TO_MS, BONUS_FACTOR } from './room.class. import { WsException } from '@nestjs/websockets'; export class Room { - readonly id: string; - private players: Map<string, Player> = new Map(); private currentQuestionIndex: number; @@ -20,9 +18,7 @@ export class Room { readonly game: Game, private readonly quiz: Quiz, private readonly server: Server, - ) { - this.id = game.id; - } + ) {} addPlayer(client: Socket, username: string) { client.join(this.game.id); @@ -30,6 +26,12 @@ export class Room { this.players.set(client.id, { username, score: 0 }); } + removePlayer(client: Socket) { + client.leave(this.game.id); + + this.players.delete(client.id); + } + start() { this.currentQuestionIndex = -1; diff --git a/server/app/game/game.gateway.spec.ts b/server/app/game/game.gateway.spec.ts index 26e75469dbf59000825c02714f9e014f5aa64b21..8052519f929e56fb6e98fad81e0341de4d12ec0b 100644 --- a/server/app/game/game.gateway.spec.ts +++ b/server/app/game/game.gateway.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GameGateway } from './game.gateway'; import * as sinon from 'sinon'; import { Socket, Server, BroadcastOperator } from 'socket.io'; -import { GameType } from '@common/game'; +import { Game, GameType } from '@common/game'; import { QuizDocument } from '@app/quiz/schemas/quiz.schema'; import { Logger } from '@nestjs/common'; import { GameRoomService } from './game-room.service'; @@ -50,37 +50,55 @@ describe('GameGateway', () => { }); describe('create', () => { + let quiz: QuizDocument; let room: sinon.SinonStubbedInstance<Room>; + let clientRooms: sinon.SinonStub; beforeEach(() => { - const quiz = { id: 'foo' } as QuizDocument; + quiz = { id: 'foo' } as QuizDocument; room = sinon.createStubInstance(Room); - (room as Writeable<Room>).id = 'bar'; + (room as Writeable<Room>).game = { id: 'bar' } as Game; gameRoomService.create.withArgs(server, quiz, GameType.Normal).returns(room); - gateway.create(quiz, socket); + clientRooms = sinon.stub(socket, 'rooms').value(new Set()); }); - it('should create a room and join the client', () => { + it('should create a room and join the client', async () => { + await gateway.create(quiz, socket); expect(gameRoomService.create.calledOnce).toBe(true); expect(room.addPlayer.calledWith(socket, 'Organisateur')).toBe(true); }); - it('should redirect the client to the waiting room', () => { + it('should redirect the client to the waiting room', async () => { + await gateway.create(quiz, socket); expect(socket.emit.calledWith('redirect-to-waiting-room', room.game)).toBe(true); }); + + it('should leave all previous game rooms', async () => { + clientRooms.value(new Set(['foo', 'bar'])); + gameRoomService.get.withArgs(new Set(['foo'])).returns(room); + + server.in.withArgs('foo').returns({ + fetchSockets: async () => [], + } as unknown as BroadcastOperator<DefaultEventsMap, unknown>); + + await gateway.create(quiz, socket); + expect(room.removePlayer.calledOnce).toBe(true); + }); }); describe('demo', () => { let room: sinon.SinonStubbedInstance<Room>; - beforeEach(() => { + beforeEach(async () => { const quiz = { id: 'foo' } as QuizDocument; room = sinon.createStubInstance(Room); - (room as Writeable<Room>).id = 'bar'; + (room as Writeable<Room>).game = { id: 'bar' } as Game; gameRoomService.create.withArgs(server, quiz, GameType.Demo).returns(room); - gateway.demo(quiz, socket); + sinon.stub(socket, 'rooms').value(new Set()); + + await gateway.demo(quiz, socket); }); it('should create a room and join the client', () => { @@ -116,6 +134,30 @@ describe('GameGateway', () => { }); }); + describe('leave', () => { + it('should get the room and remove the player', async () => { + const room = sinon.createStubInstance(Room); + (room as Writeable<Room>).game = { id: 'foo' } as Game; + sinon.stub(socket, 'rooms').value(new Set()); + gameRoomService.get.withArgs(socket.rooms).returns(room); + + server.in.withArgs('foo').returns({ + fetchSockets: async () => [], + } as unknown as BroadcastOperator<DefaultEventsMap, unknown>); + + await gateway.leave(socket); + + expect(room.removePlayer.calledWith(socket)).toBe(true); + }); + + it('should throw if the room is not found', async () => { + sinon.stub(socket, 'rooms').value(new Set(['foo'])); + gameRoomService.get.withArgs(socket.rooms).returns(undefined); + + await expect(gateway.leave(socket)).rejects.toThrowError(WsException); + }); + }); + describe('handleConnection', () => { it('should log the connection', () => { (socket as Writeable<Socket>).id = 'foo'; diff --git a/server/app/game/game.gateway.ts b/server/app/game/game.gateway.ts index 158e40b395cde9839e65d09c671d213fc403fef6..b500a528952c45d03edb47c70159d28c28715e7f 100644 --- a/server/app/game/game.gateway.ts +++ b/server/app/game/game.gateway.ts @@ -26,21 +26,15 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor(private gameRoomService: GameRoomService) {} @SubscribeMessage(GameClientEvent.Create) - create(@MessageBody(AvailableQuizPipe) quiz: QuizDocument, @ConnectedSocket() client: Socket) { - const room = this.gameRoomService.create(this.server, quiz, GameType.Normal); - room.addPlayer(client, GAME_CREATOR_USERNAME); - - this.logger.log(`Game with quiz ${quiz.id} created: ${room.id}`); + async create(@MessageBody(AvailableQuizPipe) quiz: QuizDocument, @ConnectedSocket() client: Socket) { + const room = await this.createGameRoom(GameType.Normal, quiz, client); client.emit(GameServerEvent.RedirectToWaitingRoom, room.game, GAME_CREATOR_USERNAME); } @SubscribeMessage(GameClientEvent.Demo) - demo(@MessageBody(AvailableQuizPipe) quiz: QuizDocument, @ConnectedSocket() client: Socket) { - const room = this.gameRoomService.create(this.server, quiz, GameType.Demo); - room.addPlayer(client, GAME_CREATOR_USERNAME); - - this.logger.log(`Demo game with quiz ${quiz.id} created: ${room.id}`); + async demo(@MessageBody(AvailableQuizPipe) quiz: QuizDocument, @ConnectedSocket() client: Socket) { + const room = await this.createGameRoom(GameType.Demo, quiz, client); client.emit(GameServerEvent.StartDemo, room.game, GAME_CREATOR_USERNAME); room.start(); @@ -56,6 +50,17 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { room.answer(client.id, answers); } + @SubscribeMessage(GameClientEvent.Leave) + async leave(@ConnectedSocket() client: Socket) { + const room = this.gameRoomService.get(client.rooms); + if (!room) { + throw new WsException('Room not found'); + } + + room.removePlayer(client); + await this.clearEmptyRooms(new Set([...client.rooms, room.game.id])); + } + handleConnection(client: Socket) { this.logger.log(`Client connected: ${client.id}`); @@ -71,6 +76,27 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { this.logger.log(`Client disconnected: ${client.id}`); } + private async createGameRoom(type: GameType, quiz: QuizDocument, client: Socket) { + await this.leaveAllGameRooms(client); + + const room = this.gameRoomService.create(this.server, quiz, type); + room.addPlayer(client, GAME_CREATOR_USERNAME); + + this.logger.log(`Game with quiz ${quiz.id} created: ${room.game.id}`); + + return room; + } + + private async leaveAllGameRooms(client: Socket) { + for (const roomId of client.rooms) { + const room = this.gameRoomService.get(new Set([roomId])); + if (room) { + room.removePlayer(client); + await this.clearEmptyRooms(new Set([roomId])); + } + } + } + private async clearEmptyRooms(rooms: Set<string>) { for (const roomId of rooms) { const clients = await this.server.in(roomId).fetchSockets();