tbl/src/game-tbl/game-tbl.gateway.ts
Application websocket gateway.
Properties |
|
Methods |
|
constructor(gameService: GameService)
|
||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:58
|
||||||
constructor
Parameters :
|
Private awsMetrics |
Type : AwsCustomMetrics
|
Default value : {
ioOpenConnectionsCount: 0,
ioTotalConnectionsBetweenMeasure: 0,
ioTotalDisconnectsBetweenMeasure: 0,
ioJwtExpiredConnectionsBetweenMeasure: 0,
gamesOpenCount: 0,
teachersOnlineCount: 0,
studentsOnlineCount: 0,
instanceId: '',
}
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:47
|
Private Readonly logger |
Default value : new Logger(GameGateway.name)
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:45
|
logger |
Private runningGamesIndex |
Type : literal type
|
Default value : {}
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:58
|
server |
Decorators :
@WebSocketServer()
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:43
|
io server |
collectAwsMetrics |
collectAwsMetrics()
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:69
|
Collects metrics for aws
Returns :
AwsCustomMetrics
|
Async currentRoundFinished | ||||||||
currentRoundFinished(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('currentRoundFinished')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:438
|
||||||||
Event to finish current round. Call gameService to update game info, calculate if it was last round and game is finished and calls "finilization" steps (save game to db, emit required messages to room, etc) @param client {Socket} - client socket
Parameters :
Returns :
Promise<void>
|
Async fetchPlayers | ||||||||
fetchPlayers(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('fetchPlayers')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:304
|
||||||||
callback event to get game players @param client {Socket} - client socket @return {RedisPlayerInfo[]} - array of players information
Parameters :
Returns :
Promise<RedisPlayerInfo[]>
|
gameSavedToDB |
gameSavedToDB(redisGameCode: string, mongoGameCode: string)
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:536
|
Called by redis service when recieved message on gameSaved channel
Returns :
void
|
Async getGameInfoIO | ||||||||
getGameInfoIO(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('getGameInfo')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:239
|
||||||||
callback event requesting game info. search game by socket clientId @param client {Socket} - client socket @return {RedisGame | null} - client game info or null if game not found
Parameters :
Returns :
Promise<RedisGame>
|
Async getLeaderboard | |||||||||||||||
getLeaderboard(client: Socket, withQuestions)
|
|||||||||||||||
Decorators :
@SubscribeMessage('getLeaderboard')
|
|||||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:495
|
|||||||||||||||
Event to get current leaderboard. results emits only to client @param client {Socket} - client socket @param withQuestions {boolean = false} - add players questions/answers data
Parameters :
Returns :
Promise<void>
|
Async handleConnection | ||||||||
handleConnection(client: Socket)
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:97
|
||||||||
handles new websocket connection - searching user's game, call gameService to update game/players information in redis, join client to required rooms, sends sincronization game info and emitting to others about new player @param client {Socket} - client socket @todo: move to a common abstract class that gateways can inherit from. We have one websocket server, so this connect/disconnect should also be handled in one place. @todo check if player was kicked (should be WsGuard problem)
Parameters :
Returns :
Promise<void>
|
Async handleDisconnect | ||||||||
handleDisconnect(client: Socket)
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:159
|
||||||||
handles disconnections - call gameService to update required game/player info (push down ttl/set game to autoremove), emits player disconnect event to game room @param client {Socket} - client socket
Parameters :
Returns :
Promise<void>
|
Async hostLeftGame | ||||||||
hostLeftGame(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('hostLeftGame')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:478
|
||||||||
Event of host left game. Marks game as finished and sends "hostLeftGame" event to room @param client {Socket} - client socket
Parameters :
Returns :
Promise<void>
|
Async hostPrepareRound | ||||||||||||
hostPrepareRound(client: Socket, questions: AbstractRoundQuestion[])
|
||||||||||||
Decorators :
@SubscribeMessage('hostPrepareRound')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:318
|
||||||||||||
Host prepare round @param client {Socket} - client socket @param data {AbstractRoundQuestions[]} - next round questions
Parameters :
Returns :
Promise<RedisGameRound>
|
Async hostStartRound | ||||||||
hostStartRound(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('hostStartRound')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:346
|
||||||||
Event from game host to start next round. Update gameService and emits startRound events to game room @param client {Socket} - client socket @return {RedisPlayerInfo[]} - array of players information
Parameters :
Returns :
Promise<void>
|
Async imUnpacked | ||||||||
imUnpacked(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('imUnpacked')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:190
|
||||||||
callback event requesting connected user to be ready means game is unpacked and player data is unpacked @param client {Socket} - client socket @return {boolean} - should be true
Parameters :
Returns :
Promise<boolean>
|
Async leaveGame | ||||||||
leaveGame(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('leaveGame')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:206
|
||||||||
leave game message (triggered by quitGame) - checks if host and sends required update to game room @param client {Socket} - client socket
Parameters :
Returns :
Promise<void>
|
Async playerAnswer | |||||||||||||||
playerAnswer(client: Socket, data: null)
|
|||||||||||||||
Decorators :
@SubscribeMessage('playerAnswer')
|
|||||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:411
|
|||||||||||||||
Callback event for player answer. @param client {Socket} - client socket @param data {Object} - player answer data @return {RedisPlayerStats} - player stats
Parameters :
Returns :
Promise<RedisPlayerStats>
|
Async playerSelectedAvatar | ||||||||||||
playerSelectedAvatar(client: Socket, avatar: string)
|
||||||||||||
Decorators :
@SubscribeMessage('selectedAvatar')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:584
|
||||||||||||
Select avatar player message @param client {Socket} - client socket @param avatar {string} - avatar name
Parameters :
Returns :
Promise<void>
|
Async playerSelectedBackground | ||||||||||||
playerSelectedBackground(client: Socket, background: string)
|
||||||||||||
Decorators :
@SubscribeMessage('selectedBackground')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:621
|
||||||||||||
Select background player message @param client {Socket} - client socket @param background {string} - background name
Parameters :
Returns :
Promise<void>
|
Async playerSelectedCustomAvatar | ||||||||||||
playerSelectedCustomAvatar(client: Socket, customAvatar: literal type)
|
||||||||||||
Decorators :
@SubscribeMessage('selectedCustomAvatar')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:598
|
||||||||||||
Select custom avatar player message @param client {Socket} - client socket @param customAvatar {object} - custom avatar
Parameters :
Returns :
Promise<void>
|
playerSelectedEmoji | ||||||||||||
playerSelectedEmoji(client: Socket, emoji: string)
|
||||||||||||
Decorators :
@SubscribeMessage('selectedEmoji')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:658
|
||||||||||||
Select emoji player message @param client {Socket} - client socket @param emoji {string} - emoji name
Parameters :
Returns :
void
|
Async playerSelectedFrame | ||||||||||||
playerSelectedFrame(client: Socket, frame: string)
|
||||||||||||
Decorators :
@SubscribeMessage('selectedFrame')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:644
|
||||||||||||
Select frame player message @param client {Socket} - client socket @param frame {string} - frame name
Parameters :
Returns :
Promise<void>
|
Async playerStats | ||||||||
playerStats(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('playerStats')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:386
|
||||||||
Callback event required player game stats @param client {Socket} - client socket @return {RedisPlayerStats} - player stats
Parameters :
Returns :
Promise<RedisPlayerStats>
|
Async removePlayer | |||||||||||||||
removePlayer(client: Socket, payload: object)
|
|||||||||||||||
Decorators :
@SubscribeMessage('removePlayer')
|
|||||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:553
|
|||||||||||||||
Callback event to kick player from game @todo WsGuard for host @param client {Socket} - client socket @param clientId {stinrg} - clientId to kick @return {boolean} - success/fail
Parameters :
Returns :
Promise<boolean>
|
requestDeliverQuestionsToStudents | ||||||||||||
requestDeliverQuestionsToStudents(client: Socket, payload: literal type)
|
||||||||||||
Decorators :
@SubscribeMessage('deliverQuestionsOnStudents')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:671
|
||||||||||||
Host send more questions to current round @param client {Socket} - client socket @param payload {{questions: AbstractRoundQuestion[]}} - object with new questions
Parameters :
Returns :
void
|
Async saveGameToDB | ||||||||
saveGameToDB(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('saveResultsToDB')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:520
|
||||||||
Event to save redis game to mongo. emits result permanent mongo code to room and return it for callback @param client {Socket} - client socket @return {string|null} - mongodb game code or null if saving is in process and mongo code is not defined yet
Parameters :
Returns :
Promise<string>
|
syncGameTimer |
syncGameTimer()
|
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:698
|
Emits timerSync event to all connected sockets
Returns :
void
|
Async unassignGameClassCode | ||||||||
unassignGameClassCode(client: Socket)
|
||||||||
Decorators :
@SubscribeMessage('unassignClassCode')
|
||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:286
|
||||||||
callback event to clear class code and requireLogin fields @todo refactor FE to use update only and remove this method @param client {Socket} - client socket @return {boolean} - success/fail
Parameters :
Returns :
Promise<boolean>
|
Async updateGameClassCode | ||||||||||||
updateGameClassCode(client: Socket, payload: literal type)
|
||||||||||||
Decorators :
@SubscribeMessage('updateClassCode')
|
||||||||||||
Defined in tbl/src/game-tbl/game-tbl.gateway.ts:255
|
||||||||||||
callback event to update class code and requireLogin fields @param client {Socket} - client socket @param payload {Object} - update information @return {boolean} - success/fail
Parameters :
Returns :
Promise<boolean>
|
import {
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { forwardRef, Inject, Logger } from '@nestjs/common';
import { GameService } from './game-tbl.service';
import * as moment from 'moment';
import { Socket } from 'socket.io';
import { LIVE_GAME_EMPTY_PLAYER_RESULT } from '../../../shared/constants/game';
import {
RedisGame,
RedisPlayerStats,
RedisPlayerInfo,
RedisGameRound,
AbstractRoundQuestion,
} from '../../../shared/interfaces/game';
import { SocketEventMessageBody } from '../../../shared/common/types';
import { AwsCustomMetrics } from '../../../shared/interfaces/aws';
import {
ApplyDecoratorToAll,
CatchAndLogError,
} from '../../../shared/common/catch-and-log-error.decorator';
/**
* Application websocket gateway.
*/
@WebSocketGateway({ namespace: 'live' })
@ApplyDecoratorToAll(CatchAndLogError(GameGateway.name), {
syncFunc: [
'collectAwsMetrics',
'gameSavedToDB',
'requestDeliverQuestionsToStudents',
'syncGameTimer',
'playerSelectedEmoji',
],
})
export class GameGateway {
/** io server */
@WebSocketServer() server;
/** logger */
private readonly logger = new Logger(GameGateway.name);
private awsMetrics: AwsCustomMetrics = {
ioOpenConnectionsCount: 0,
ioTotalConnectionsBetweenMeasure: 0,
ioTotalDisconnectsBetweenMeasure: 0,
ioJwtExpiredConnectionsBetweenMeasure: 0,
gamesOpenCount: 0,
teachersOnlineCount: 0,
studentsOnlineCount: 0,
instanceId: '',
};
private runningGamesIndex: { [gameCode: string]: boolean } = {};
/** constructor */
constructor(
@Inject(forwardRef(() => GameService))
private readonly gameService: GameService,
) {}
/**
* Collects metrics for aws
*/
collectAwsMetrics(): AwsCustomMetrics {
const ret: AwsCustomMetrics = JSON.parse(
JSON.stringify(this.awsMetrics),
) as AwsCustomMetrics;
this.awsMetrics.ioTotalConnectionsBetweenMeasure = 0;
this.awsMetrics.ioTotalDisconnectsBetweenMeasure = 0;
ret.gamesOpenCount = Object.keys(this.runningGamesIndex).length;
// TODO find way how to check
// ret.ioJwtExpiredConnectionsBetweenMeasure = this.server.jwtVerifyCounter;
// this.server.jwtVerifyCounter = 0;
return ret;
}
/**
* handles new websocket connection - searching user's game,
* call gameService to update game/players information in redis,
* join client to required rooms, sends sincronization game info
* and emitting to others about new player
*
* @param client {Socket} - client socket
*
* @todo: move to a common abstract class that gateways can inherit from.
* We have one websocket server, so this connect/disconnect should also be
* handled in one place.
*
* @todo check if player was kicked (should be WsGuard problem)
*
*/
async handleConnection(client: Socket): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${gameCode} ${clientId} gateway connection`);
this.awsMetrics.ioTotalConnectionsBetweenMeasure++;
this.awsMetrics.ioOpenConnectionsCount++;
// health check only joins to timer sync channel
if (clientId === 'health-check' && gameCode === 'health-check') {
await client.join('timerSync');
return;
}
const connect = await this.gameService.playerConnected(clientId, gameCode);
if (!connect) {
this.logger.error('websocket connection game not found');
// let's inform browser about disconnection reason
client.emit('game-not-found');
this.awsMetrics.ioOpenConnectionsCount--;
client.disconnect(true);
return;
}
const info = await this.gameService.collectGameInfo(connect.gameCode);
// emit game info to client so client doesn't need to request it
client.emit('game-info', info);
if (info.finished) {
this.logger.error('websocket connection game finished already');
// client should manage disconnection on this event
client.emit('game-finished');
delete this.runningGamesIndex[gameCode];
return;
}
await client.join(connect.gameCode);
const hostInfo = await this.gameService.gameHostInfo(connect.gameCode);
if (!hostInfo) {
this.logger.error(`${connect.gameCode} host info not found!`);
} else {
if (hostInfo.clientId === clientId) {
await client.join(`${connect.gameCode}-teacher`);
this.awsMetrics.teachersOnlineCount++;
}
}
await client.join('timerSync');
this.logger.log(`${connect.gameCode} ${clientId} joined live game`);
// teachers has no playerInfo
if (connect.playerInfo) {
this.awsMetrics.studentsOnlineCount++;
this.server
.to(connect.gameCode)
.emit('playerJoinGame', connect.playerInfo);
}
this.runningGamesIndex[gameCode] = true;
}
/**
* handles disconnections - call gameService to update required
* game/player info (push down ttl/set game to autoremove),
* emits player disconnect event to game room
*
* @param client {Socket} - client socket
*
*/
async handleDisconnect(client: Socket): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.awsMetrics.ioTotalDisconnectsBetweenMeasure++;
this.awsMetrics.ioOpenConnectionsCount--;
const info = await this.gameService.playerDisconnected(clientId, gameCode);
// if kicked - game for player will not be found already
if (!info) {
this.logger.log(`${gameCode} ${clientId} disconnected game not found!`);
return;
}
this.logger.log(`${gameCode} ${clientId} gateway disconnection`);
if (info && info.playerInfo) {
// this is player, not teacher
this.server.to(info.gameCode).emit('playerDisconnected', info.playerInfo);
this.awsMetrics.studentsOnlineCount--;
} else {
this.awsMetrics.teachersOnlineCount--;
}
}
/**
* callback event requesting connected user to be ready
* means game is unpacked and player data is unpacked
*
* @param client {Socket} - client socket
*
* @return {boolean} - should be true
*/
@SubscribeMessage('imUnpacked')
async imUnpacked(client: Socket): Promise<boolean> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
const unpacked = await this.gameService.clientIsReady(gameCode, clientId);
this.logger.log(`${gameCode} ${clientId} im unpacked ${unpacked}`);
return unpacked;
}
/**
* leave game message (triggered by quitGame) - checks if host
* and sends required update to game room
*
* @param client {Socket} - client socket
*
*/
@SubscribeMessage('leaveGame')
async leaveGame(client: Socket): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
const isHost = client.data.user.isHost;
const allOk = await this.gameService.playerLeave(
clientId,
gameCode,
isHost,
);
if (!allOk) {
this.logger.log(
`${gameCode} ${clientId} isHost ${isHost} left unsuccessfully`,
);
return;
}
if (isHost) {
await this.gameService.hostLeft(gameCode);
this.server.to(gameCode).emit('hostLeftGame');
} else {
this.server.to(gameCode).emit('playerLeft', clientId);
}
this.logger.log(`${gameCode} ${clientId} isHost ${isHost} left game`);
}
/**
* callback event requesting game info.
* search game by socket clientId
*
* @param client {Socket} - client socket
*
* @return {RedisGame | null} - client game info or null if game not found
*/
@SubscribeMessage('getGameInfo')
async getGameInfoIO(client: Socket): Promise<RedisGame> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${gameCode} ${clientId} get game info`);
return await this.gameService.collectGameInfo(gameCode);
}
/**
* callback event to update class code and requireLogin fields
*
* @param client {Socket} - client socket
* @param payload {Object} - update information
*
* @return {boolean} - success/fail
*/
@SubscribeMessage('updateClassCode')
async updateGameClassCode(
client: Socket,
payload: {
forGameCode: string;
classCode: string | null;
requireLogin?: boolean;
},
): Promise<boolean> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(
`${gameCode} ${clientId} update class code ${payload.classCode}`,
);
const ok = await this.gameService.updateClassCodeForTheGame(
`${payload.forGameCode}`,
payload.classCode,
payload.requireLogin,
);
return ok;
}
/**
* callback event to clear class code and requireLogin fields
*
* @todo refactor FE to use update only and remove this method
*
* @param client {Socket} - client socket
*
* @return {boolean} - success/fail
*/
@SubscribeMessage('unassignClassCode')
async unassignGameClassCode(client: Socket): Promise<boolean> {
const gameCode = client.data.user.gameCode;
const ok = await this.gameService.updateClassCodeForTheGame(
`${gameCode}`,
null,
false,
);
return ok;
}
/**
* callback event to get game players
*
* @param client {Socket} - client socket
*
* @return {RedisPlayerInfo[]} - array of players information
*/
@SubscribeMessage('fetchPlayers')
async fetchPlayers(client: Socket): Promise<RedisPlayerInfo[]> {
const gameCode = client.data.user.gameCode;
this.logger.log(`${client.id} ${gameCode}: fetching players`);
const players = await this.gameService.getAllPlayers(gameCode);
return players.filter((p) => !p.kicked);
}
/**
* Host prepare round
*
* @param client {Socket} - client socket
* @param data {AbstractRoundQuestions[]} - next round questions
*/
@SubscribeMessage('hostPrepareRound')
async hostPrepareRound(
client: Socket,
questions: AbstractRoundQuestion[],
): Promise<RedisGameRound> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${clientId} ${gameCode}: hostPrepareRound`);
const round = await this.gameService.hostPrepareRound(gameCode, questions);
if (round) {
this.server.to(gameCode).emit('roundPrepared', round);
this.logger.log(
`${clientId} ${gameCode}: preparing next round ${round.round}`,
);
} else {
this.logger.error(`${clientId} ${gameCode}: preparing next round fail`);
}
return round;
}
/**
* Event from game host to start next round. Update gameService and
* emits startRound events to game room
*
* @param client {Socket} - client socket
*
* @return {RedisPlayerInfo[]} - array of players information
*/
@SubscribeMessage('hostStartRound')
async hostStartRound(client: Socket): Promise<void> {
// @todo check perimssions
// const clientId = client.data.user.clientId;
// "to be sure" converting game code to string
const gameCode = client.data.user.gameCode;
const stats = await this.gameService.hostStartRound(gameCode);
if (!stats) {
// error logs should go from gameService
return;
}
// clear stats, cuz we want "startRound" packet to be small,
// so all students recieve it fast
const roundData = stats.rounds.pop();
const { started, endTime, round, rounds } = roundData;
this.logger.log(
`${gameCode}: starting round ${round}/${rounds} end time ${endTime}`,
);
this.server.to(gameCode).emit('startRound', {
stats: {
started: stats.started,
finished: stats.finished,
},
roundData: {
started,
gameCode,
round,
rounds,
endTime,
},
});
}
/**
* Callback event required player game stats
*
* @param client {Socket} - client socket
*
* @return {RedisPlayerStats} - player stats
*/
@SubscribeMessage('playerStats')
async playerStats(client: Socket): Promise<RedisPlayerStats> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${clientId} requests stats in game ${gameCode}`);
const stats = await this.gameService.playerStats(clientId, gameCode);
if (!stats) {
this.logger.log(`${clientId} stats in game ${gameCode} not found!`);
return { ...LIVE_GAME_EMPTY_PLAYER_RESULT, answers: [], rounds: [] };
}
stats.results = [];
(stats.answers || []).forEach((round) => {
stats.results = [...stats.results, ...(round || [])];
});
return stats;
}
/**
* Callback event for player answer.
*
* @param client {Socket} - client socket
* @param data {Object} - player answer data
*
* @return {RedisPlayerStats} - player stats
*/
@SubscribeMessage('playerAnswer')
async playerAnswer(client: Socket, data = null): Promise<RedisPlayerStats> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
const prefix = `${gameCode} ${clientId} ${data.playerName}`;
this.logger.log(`${prefix} answer ${data.questionBody.correct}`);
const stats = await this.gameService.savePlayerAnswer(
gameCode,
clientId,
data,
);
// remove null fields to reduce data
data.questionBody = Object.fromEntries(
Object.entries(data.questionBody).filter(([, val]) => val !== null),
);
// sending player's answer only to teacher
this.server.to(`${gameCode}-teacher`).emit('playerAnswer', data);
return stats;
}
/**
* Event to finish current round. Call gameService to update game info,
* calculate if it was last round and game is finished and calls
* "finilization" steps (save game to db, emit required messages to room, etc)
*
* @param client {Socket} - client socket
*/
@SubscribeMessage('currentRoundFinished')
async currentRoundFinished(client: Socket): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
try {
this.logger.log(`${clientId} ${gameCode}: current round finished`);
// sending leaderboard event to room so noone needs request it
const gameFinished = await this.gameService.setCurrentRoundFinished(
gameCode,
);
const leaderboard = await this.gameService.getGameLeaderboard(
gameCode,
// seems we don't need anymore questions here
// gameFinished,
);
this.server.to(gameCode).emit('leaderboard', leaderboard);
this.logger.log(`${gameCode} leaderboard sent`);
if (gameFinished) {
// let's automatically save it to db with 120 sec removal delay
// 2 min should be enough for all players to get any
// required data from game from redis
const mongoCode = await this.gameService.saveGameToDB(gameCode);
if (mongoCode) {
this.logger.log(
`sending mongo code to game ${gameCode} - ${mongoCode}`,
);
this.server.to(gameCode).emit('mongoCode', mongoCode);
delete this.runningGamesIndex[gameCode];
}
}
} catch (e) {
this.logger.error(e);
}
}
/**
* Event of host left game. Marks game as finished and sends "hostLeftGame" event to room
*
* @param client {Socket} - client socket
*/
@SubscribeMessage('hostLeftGame')
async hostLeftGame(client: Socket): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
await this.gameService.playerDisconnected(clientId, gameCode);
await this.gameService.hostLeft(gameCode);
this.logger.log(`${client.id} ${gameCode}: emit to everyone: host left`);
this.server.to(gameCode).emit('hostLeftGame');
delete this.runningGamesIndex[gameCode];
}
/**
* Event to get current leaderboard. results emits only to client
*
* @param client {Socket} - client socket
* @param withQuestions {boolean = false} - add players questions/answers data
*/
@SubscribeMessage('getLeaderboard')
async getLeaderboard(client: Socket, withQuestions = false): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${clientId} ${gameCode}: get leaderboard`);
const leaderboard = await this.gameService.getGameLeaderboard(
gameCode,
withQuestions,
);
// sending leaderboard only to client to reduce room outgoing network packets
client.emit('leaderboard', leaderboard);
// sending leaderboard to room
this.server.to(gameCode).emit('leaderboard', leaderboard);
this.logger.log(`${gameCode}: emitted to room - leaderboard`);
}
/**
* Event to save redis game to mongo. emits result permanent mongo code to room
* and return it for callback
*
* @param client {Socket} - client socket
*
* @return {string|null} - mongodb game code or null if saving is in process
* and mongo code is not defined yet
*/
@SubscribeMessage('saveResultsToDB')
async saveGameToDB(client: Socket): Promise<string> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.logger.log(`${clientId} saving game ${gameCode} to DB`);
const mongoCode = await this.gameService.saveGameToDB(gameCode);
if (mongoCode) {
// send to everybody
this.server.to(gameCode).emit('mongoCode', mongoCode);
delete this.runningGamesIndex[gameCode];
}
return mongoCode;
}
/**
* Called by redis service when recieved message on gameSaved channel
*/
gameSavedToDB(redisGameCode: string, mongoGameCode: string): void {
this.server.to(redisGameCode).emit('mongoCode', mongoGameCode);
this.logger.log(`${redisGameCode} sended mongo code ${mongoGameCode}`);
delete this.runningGamesIndex[redisGameCode];
}
/**
* Callback event to kick player from game
*
* @todo WsGuard for host
*
* @param client {Socket} - client socket
* @param clientId {stinrg} - clientId to kick
*
* @return {boolean} - success/fail
*/
@SubscribeMessage('removePlayer')
async removePlayer(
client: Socket,
payload = {
clientId: '',
choice: 'remove',
},
): Promise<boolean> {
const gameCode = client.data.user.gameCode;
this.logger.log(
`${gameCode} removePlayer ${payload.clientId} - ${payload.choice}`,
);
const ok = await this.gameService.removePlayer(
payload.clientId,
payload.choice === 'ban',
);
if (!ok) return false;
this.server.to(gameCode).emit('removePlayer', payload.clientId);
this.logger.log(
`${gameCode} player ${payload.clientId} was <${payload.choice}>`,
);
return true;
}
/**
* Select avatar player message
*
* @param client {Socket} - client socket
* @param avatar {string} - avatar name
*/
@SubscribeMessage('selectedAvatar')
async playerSelectedAvatar(client: Socket, avatar: string): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.server.to(gameCode).emit('playerSelectedAvatar', { clientId, avatar });
await this.gameService.playerSelectedAvatar(gameCode, clientId, avatar);
}
/**
* Select custom avatar player message
*
* @param client {Socket} - client socket
* @param customAvatar {object} - custom avatar
*/
@SubscribeMessage('selectedCustomAvatar')
async playerSelectedCustomAvatar(
client: Socket,
customAvatar: { [key: string]: string },
): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.server
.to(gameCode)
.emit('playerSelectedCustomAvatar', { clientId, customAvatar });
await this.gameService.playerSelectedCustomAvatar(
gameCode,
clientId,
customAvatar,
);
}
/**
* Select background player message
*
* @param client {Socket} - client socket
* @param background {string} - background name
*/
@SubscribeMessage('selectedBackground')
async playerSelectedBackground(
client: Socket,
background: string,
): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.server
.to(gameCode)
.emit('playerSelectedBackground', { clientId, background });
await this.gameService.playerSelectedBackground(
gameCode,
clientId,
background,
);
}
/**
* Select frame player message
*
* @param client {Socket} - client socket
* @param frame {string} - frame name
*/
@SubscribeMessage('selectedFrame')
async playerSelectedFrame(client: Socket, frame: string): Promise<void> {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.server.to(gameCode).emit('playerSelectedFrame', { clientId, frame });
await this.gameService.playerSelectedFrame(gameCode, clientId, frame);
}
/**
* Select emoji player message
*
* @param client {Socket} - client socket
* @param emoji {string} - emoji name
*/
@SubscribeMessage('selectedEmoji')
playerSelectedEmoji(client: Socket, emoji: string): void {
const clientId = client.data.user.clientId;
const gameCode = client.data.user.gameCode;
this.server.to(gameCode).emit('playerSelectedEmoji', { clientId, emoji });
}
/**
* Host send more questions to current round
*
* @param client {Socket} - client socket
* @param payload {{questions: AbstractRoundQuestion[]}} - object with new questions
*/
@SubscribeMessage('deliverQuestionsOnStudents')
requestDeliverQuestionsToStudents(
client: Socket,
payload: {
questions: AbstractRoundQuestion[];
},
): void {
// may be unjsonified as number cuz redis game code is number based
const gameCode = client.data.user.gameCode;
this.logger.log(
// eslint-disable-next-line max-len
`${client.id} ${gameCode}: emitted to everyone: new questions delivered to students.`,
);
void this.gameService.addNewQuestionsToRound(gameCode, payload.questions);
const eventName = 'waitForNewQuestions';
const eventData: SocketEventMessageBody = {
data: [...payload.questions],
timestamp: moment().unix(),
room: gameCode,
};
this.server.to(gameCode).emit(eventName, eventData);
}
/**
* Emits timerSync event to all connected sockets
*/
syncGameTimer(): void {
this.server.to('timerSync').emit('timerSync', moment().format());
}
}