File

tbl/src/game-tbl/game-tbl.gateway.ts

Description

Application websocket gateway.

Index

Properties
Methods

Constructor

constructor(gameService: GameService)

constructor

Parameters :
Name Type Optional
gameService GameService No

Properties

Private awsMetrics
Type : AwsCustomMetrics
Default value : { ioOpenConnectionsCount: 0, ioTotalConnectionsBetweenMeasure: 0, ioTotalDisconnectsBetweenMeasure: 0, ioJwtExpiredConnectionsBetweenMeasure: 0, gamesOpenCount: 0, teachersOnlineCount: 0, studentsOnlineCount: 0, instanceId: '', }
Private Readonly logger
Default value : new Logger(GameGateway.name)

logger

Private runningGamesIndex
Type : literal type
Default value : {}
server
Decorators :
@WebSocketServer()

io server

Methods

collectAwsMetrics
collectAwsMetrics()

Collects metrics for aws

Returns : AwsCustomMetrics
Async currentRoundFinished
currentRoundFinished(client: Socket)
Decorators :
@SubscribeMessage('currentRoundFinished')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
Async fetchPlayers
fetchPlayers(client: Socket)
Decorators :
@SubscribeMessage('fetchPlayers')

callback event to get game players

@param client {Socket} - client socket

@return {RedisPlayerInfo[]} - array of players information

Parameters :
Name Type Optional Description
client Socket No
  • client socket
  • array of players information
gameSavedToDB
gameSavedToDB(redisGameCode: string, mongoGameCode: string)

Called by redis service when recieved message on gameSaved channel

Parameters :
Name Type Optional
redisGameCode string No
mongoGameCode string No
Returns : void
Async getGameInfoIO
getGameInfoIO(client: Socket)
Decorators :
@SubscribeMessage('getGameInfo')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<RedisGame>
  • client game info or null if game not found
Async getLeaderboard
getLeaderboard(client: Socket, withQuestions)
Decorators :
@SubscribeMessage('getLeaderboard')

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 :
Name Type Optional Default value Description
client Socket No
  • client socket
withQuestions No false

false} - add players questions/answers data

Returns : Promise<void>
Async handleConnection
handleConnection(client: Socket)

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
Async handleDisconnect
handleDisconnect(client: Socket)

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
Async hostLeftGame
hostLeftGame(client: Socket)
Decorators :
@SubscribeMessage('hostLeftGame')

Event of host left game. Marks game as finished and sends "hostLeftGame" event to room

@param client {Socket} - client socket

Parameters :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
Async hostPrepareRound
hostPrepareRound(client: Socket, questions: AbstractRoundQuestion[])
Decorators :
@SubscribeMessage('hostPrepareRound')

Host prepare round

@param client {Socket} - client socket @param data {AbstractRoundQuestions[]} - next round questions

Parameters :
Name Type Optional Description
client Socket No
  • client socket
questions AbstractRoundQuestion[] No
Returns : Promise<RedisGameRound>
Async hostStartRound
hostStartRound(client: Socket)
Decorators :
@SubscribeMessage('hostStartRound')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
  • array of players information
Async imUnpacked
imUnpacked(client: Socket)
Decorators :
@SubscribeMessage('imUnpacked')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<boolean>
  • should be true
Async leaveGame
leaveGame(client: Socket)
Decorators :
@SubscribeMessage('leaveGame')

leave game message (triggered by quitGame) - checks if host and sends required update to game room

@param client {Socket} - client socket

Parameters :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<void>
Async playerAnswer
playerAnswer(client: Socket, data: null)
Decorators :
@SubscribeMessage('playerAnswer')

Callback event for player answer.

@param client {Socket} - client socket @param data {Object} - player answer data

@return {RedisPlayerStats} - player stats

Parameters :
Name Type Optional Default value Description
client Socket No
  • client socket
data null No null
  • player answer data
  • player stats
Async playerSelectedAvatar
playerSelectedAvatar(client: Socket, avatar: string)
Decorators :
@SubscribeMessage('selectedAvatar')

Select avatar player message

@param client {Socket} - client socket @param avatar {string} - avatar name

Parameters :
Name Type Optional Description
client Socket No
  • client socket
avatar string No
  • avatar name
Returns : Promise<void>
Async playerSelectedBackground
playerSelectedBackground(client: Socket, background: string)
Decorators :
@SubscribeMessage('selectedBackground')

Select background player message

@param client {Socket} - client socket @param background {string} - background name

Parameters :
Name Type Optional Description
client Socket No
  • client socket
background string No
  • background name
Returns : Promise<void>
Async playerSelectedCustomAvatar
playerSelectedCustomAvatar(client: Socket, customAvatar: literal type)
Decorators :
@SubscribeMessage('selectedCustomAvatar')

Select custom avatar player message

@param client {Socket} - client socket @param customAvatar {object} - custom avatar

Parameters :
Name Type Optional Description
client Socket No
  • client socket
customAvatar literal type No
  • custom avatar
Returns : Promise<void>
playerSelectedEmoji
playerSelectedEmoji(client: Socket, emoji: string)
Decorators :
@SubscribeMessage('selectedEmoji')

Select emoji player message

@param client {Socket} - client socket @param emoji {string} - emoji name

Parameters :
Name Type Optional Description
client Socket No
  • client socket
emoji string No
  • emoji name
Returns : void
Async playerSelectedFrame
playerSelectedFrame(client: Socket, frame: string)
Decorators :
@SubscribeMessage('selectedFrame')

Select frame player message

@param client {Socket} - client socket @param frame {string} - frame name

Parameters :
Name Type Optional Description
client Socket No
  • client socket
frame string No
  • frame name
Returns : Promise<void>
Async playerStats
playerStats(client: Socket)
Decorators :
@SubscribeMessage('playerStats')

Callback event required player game stats

@param client {Socket} - client socket

@return {RedisPlayerStats} - player stats

Parameters :
Name Type Optional Description
client Socket No
  • client socket
  • player stats
Async removePlayer
removePlayer(client: Socket, payload: object)
Decorators :
@SubscribeMessage('removePlayer')

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 :
Name Type Optional Default value Description
client Socket No
  • client socket
payload object No { clientId: '', choice: 'remove', }
Returns : Promise<boolean>
  • success/fail
requestDeliverQuestionsToStudents
requestDeliverQuestionsToStudents(client: Socket, payload: literal type)
Decorators :
@SubscribeMessage('deliverQuestionsOnStudents')

Host send more questions to current round

@param client {Socket} - client socket @param payload {{questions: AbstractRoundQuestion[]}} - object with new questions

Parameters :
Name Type Optional Description
client Socket No
  • client socket
payload literal type No
  • object with new questions
Returns : void
Async saveGameToDB
saveGameToDB(client: Socket)
Decorators :
@SubscribeMessage('saveResultsToDB')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<string>
  • mongodb game code or null if saving is in process and mongo code is not defined yet
syncGameTimer
syncGameTimer()

Emits timerSync event to all connected sockets

Returns : void
Async unassignGameClassCode
unassignGameClassCode(client: Socket)
Decorators :
@SubscribeMessage('unassignClassCode')

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 :
Name Type Optional Description
client Socket No
  • client socket
Returns : Promise<boolean>
  • success/fail
Async updateGameClassCode
updateGameClassCode(client: Socket, payload: literal type)
Decorators :
@SubscribeMessage('updateClassCode')

callback event to update class code and requireLogin fields

@param client {Socket} - client socket @param payload {Object} - update information

@return {boolean} - success/fail

Parameters :
Name Type Optional Description
client Socket No
  • client socket
payload literal type No
  • update information
Returns : Promise<boolean>
  • success/fail
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());
  }
}

results matching ""

    No results matching ""