diff --git a/.eslintrc.json b/.eslintrc.json
index 1de264a..20490af 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -21,6 +21,7 @@
"no-void": ["error", { "allowAsStatement": true }],
"no-prototype-builtins": "off",
"no-undef": "off",
+ "no-case-declarations": "off",
"no-return-assign": "warn",
"prefer-promise-reject-errors": "warn",
"no-trailing-spaces": "off",
diff --git a/client/src/config/globals.js b/client/src/config/globals.js
index 72f5e8b..fba64bc 100644
--- a/client/src/config/globals.js
+++ b/client/src/config/globals.js
@@ -42,6 +42,21 @@ export const globals = {
PLAYER_LEFT: 'playerLeft',
NEW_SPECTATOR: 'newSpectator'
},
+ SOCKET_EVENTS: {
+ IN_GAME_MESSAGE: 'inGameMessage'
+ },
+ EVENT_IDS: {
+ FETCH_GAME_STATE: 'fetchGameState',
+ START_GAME: 'startGame',
+ PAUSE_TIMER: 'pauseTimer',
+ RESUME_TIMER: 'resumeTimer',
+ GET_TIME_REMAINING: 'getTimeRemaining',
+ KILL_PLAYER: 'killPlayer',
+ REVEAL_PLAYER: 'revealPlayer',
+ TRANSFER_MODERATOR: 'transferModerator',
+ CHANGE_NAME: 'changeName',
+ END_GAME: 'endGame'
+ },
USER_TYPES: {
MODERATOR: 'moderator',
PLAYER: 'player',
diff --git a/client/src/images/tutorial/dedicated-mod.PNG b/client/src/images/tutorial/dedicated-mod.PNG
new file mode 100644
index 0000000..7d966c1
Binary files /dev/null and b/client/src/images/tutorial/dedicated-mod.PNG differ
diff --git a/client/src/images/tutorial/moderation-option.png b/client/src/images/tutorial/moderation-option.png
index 0bf06d0..2763a47 100644
Binary files a/client/src/images/tutorial/moderation-option.png and b/client/src/images/tutorial/moderation-option.png differ
diff --git a/client/src/images/tutorial/moderator-view.PNG b/client/src/images/tutorial/moderator-view.PNG
deleted file mode 100644
index e0b99a1..0000000
Binary files a/client/src/images/tutorial/moderator-view.PNG and /dev/null differ
diff --git a/client/src/images/tutorial/temp-mod-view.PNG b/client/src/images/tutorial/temp-mod-view.PNG
deleted file mode 100644
index 8b1bcb9..0000000
Binary files a/client/src/images/tutorial/temp-mod-view.PNG and /dev/null differ
diff --git a/client/src/images/tutorial/temporary-mod.PNG b/client/src/images/tutorial/temporary-mod.PNG
new file mode 100644
index 0000000..a433c00
Binary files /dev/null and b/client/src/images/tutorial/temporary-mod.PNG differ
diff --git a/client/src/images/tutorial/transfer-mod.gif b/client/src/images/tutorial/transfer-mod.gif
index d6c4609..d43ffe6 100644
Binary files a/client/src/images/tutorial/transfer-mod.gif and b/client/src/images/tutorial/transfer-mod.gif differ
diff --git a/client/src/modules/GameCreationStepManager.js b/client/src/modules/GameCreationStepManager.js
index f674f45..f40e39d 100644
--- a/client/src/modules/GameCreationStepManager.js
+++ b/client/src/modules/GameCreationStepManager.js
@@ -305,7 +305,7 @@ function renderModerationTypeStep (game, containerId, stepNumber) {
stepContainer.innerHTML =
"
This is an example of what a
dedicated moderator sees during the game:
-

+
They can see who is on which team and who has each role. The moderator kills and reveals players. They are
separate actions - note the two buttons for each player. So if you want to play a game where people's roles are
@@ -96,7 +96,7 @@
much the same abilities as a dedicated moderator, except they don't know role or alignment information and cannot
transfer their powers. Their powers will be transferred automatically to the first person they remove from the game:
-

+
Transferring your moderator powers
diff --git a/index.js b/index.js
index a473d39..6eff3be 100644
--- a/index.js
+++ b/index.js
@@ -1,11 +1,7 @@
'use strict';
const express = require('express');
-const path = require('path');
const app = express();
-const GameManager = require('./server/modules/GameManager.js');
-const SocketManager = require('./server/modules/SocketManager.js');
-const globals = require('./server/config/globals');
const ServerBootstrapper = require('./server/modules/ServerBootstrapper');
app.use(express.json());
@@ -15,64 +11,17 @@ const args = ServerBootstrapper.processCLIArgs();
const logger = require('./server/modules/Logger')(args.logLevel);
logger.info('LOG LEVEL IS: ' + args.logLevel);
-const index = ServerBootstrapper.createServerWithCorrectHTTPProtocol(app, args.useHttps, args.port, logger);
+const port = parseInt(process.env.PORT) || args.port || 8080;
-app.set('port', parseInt(process.env.PORT) || args.port || 8080);
+const webServer = ServerBootstrapper.createServerWithCorrectHTTPProtocol(app, args.useHttps, args.port, logger);
+const singletons = ServerBootstrapper.singletons(logger);
-const inGameSocketServer = ServerBootstrapper.createSocketServer(index, app, args.port, logger);
-const gameNamespace = ServerBootstrapper.createGameSocketNamespace(inGameSocketServer, logger);
+const socketServer = singletons.socketManager.createSocketServer(webServer, app, port);
+singletons.gameManager.setGameSocketNamespace(singletons.socketManager.createGameSocketNamespace(socketServer, logger, singletons.gameManager));
+ServerBootstrapper.establishRouting(app, express);
-let gameManager;
+app.set('port', port);
-/* Instantiate the singleton game manager */
-if (process.env.NODE_ENV.trim() === 'development') {
- gameManager = new GameManager(logger, globals.ENVIRONMENT.LOCAL, gameNamespace).getInstance();
-} else {
- gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, gameNamespace).getInstance();
-}
-
-/* Instantiate the singleton socket manager */
-const socketManager = new SocketManager(logger, inGameSocketServer).getInstance();
-
-gameNamespace.on('connection', function (socket) {
- socket.on('disconnecting', (reason) => {
- logger.trace('client socket disconnecting because: ' + reason);
- });
- gameManager.addGameSocketHandlers(gameNamespace, socket);
-});
-
-/* api endpoints */
-const games = require('./server/api/GamesAPI');
-const admin = require('./server/api/AdminAPI');
-app.use('/api/games', games);
-app.use('/api/admin', admin);
-
-/* serve all the app's pages */
-app.use('/manifest.json', (req, res) => {
- res.sendFile(path.join(__dirname, './manifest.json'));
-});
-
-app.use('/favicon.ico', (req, res) => {
- res.sendFile(path.join(__dirname, './client/favicon_package/favicon.ico'));
-});
-
-const router = require('./server/routes/router');
-app.use('', router);
-
-app.use('/dist', express.static(path.join(__dirname, './client/dist')));
-
-// set up routing for static content that isn't being bundled.
-app.use('/images', express.static(path.join(__dirname, './client/src/images')));
-app.use('/styles', express.static(path.join(__dirname, './client/src/styles')));
-app.use('/webfonts', express.static(path.join(__dirname, './client/src/webfonts')));
-app.use('/robots.txt', (req, res) => {
- res.sendFile(path.join(__dirname, './client/robots.txt'));
-});
-
-app.use(function (req, res) {
- res.sendFile(path.join(__dirname, './client/src/views/404.html'));
-});
-
-index.listen(app.get('port'), function () {
+webServer.listen(app.get('port'), function () {
logger.info(`Starting server on port ${app.get('port')}`);
});
diff --git a/server/config/globals.js b/server/config/globals.js
index 75a3e84..77d0dec 100644
--- a/server/config/globals.js
+++ b/server/config/globals.js
@@ -21,7 +21,10 @@ const globals = {
}
},
STALE_GAME_HOURS: 24,
- CLIENT_COMMANDS: {
+ SOCKET_EVENTS: {
+ IN_GAME_MESSAGE: 'inGameMessage'
+ },
+ EVENT_IDS: {
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js
index d292f24..b17a6c9 100644
--- a/server/modules/GameManager.js
+++ b/server/modules/GameManager.js
@@ -6,159 +6,15 @@ const GameStateCurator = require('./GameStateCurator');
const UsernameGenerator = require('./UsernameGenerator');
class GameManager {
- constructor (logger, environment, namespace) {
+ constructor (logger, environment) {
this.logger = logger;
this.environment = environment;
this.activeGameRunner = new ActiveGameRunner(logger).getInstance();
- this.namespace = namespace;
+ this.namespace = null;
}
- addGameSocketHandlers = (namespace, socket) => {
- socket.on(globals.CLIENT_COMMANDS.FETCH_GAME_STATE, async (accessCode, personId, ackFn) => {
- this.logger.trace('request for game state for accessCode: ' + accessCode + ' from socket: ' + socket.id + ' with cookie: ' + personId);
- await this.handleRequestForGameState(
- this.namespace,
- this.logger,
- this.activeGameRunner,
- accessCode,
- personId,
- ackFn,
- socket
- );
- });
-
- socket.on(globals.CLIENT_COMMANDS.START_GAME, (accessCode) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game && game.isFull) {
- game.status = globals.STATUS.IN_PROGRESS;
- if (game.hasTimer) {
- game.timerParams.paused = true;
- this.activeGameRunner.runGame(game, namespace);
- }
- namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.START_GAME);
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.PAUSE_TIMER, (accessCode) => {
- this.logger.trace(accessCode);
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const thread = this.activeGameRunner.timerThreads[accessCode];
- if (thread) {
- this.logger.debug('Timer thread found for game ' + accessCode);
- thread.send({
- command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER,
- accessCode: game.accessCode,
- logLevel: this.logger.logLevel
- });
- }
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.RESUME_TIMER, (accessCode) => {
- this.logger.trace(accessCode);
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const thread = this.activeGameRunner.timerThreads[accessCode];
- if (thread) {
- this.logger.debug('Timer thread found for game ' + accessCode);
- thread.send({
- command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER,
- accessCode: game.accessCode,
- logLevel: this.logger.logLevel
- });
- }
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.GET_TIME_REMAINING, (accessCode) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const thread = this.activeGameRunner.timerThreads[accessCode];
- if (thread) {
- thread.send({
- command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
- accessCode: accessCode,
- socketId: socket.id,
- logLevel: this.logger.logLevel
- });
- } else {
- if (game.timerParams && game.timerParams.timeRemaining === 0) {
- this.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
- }
- }
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.KILL_PLAYER, (accessCode, personId) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const person = game.people.find((person) => person.id === personId);
- this.killPlayer(game, person, namespace, this.logger);
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.REVEAL_PLAYER, (accessCode, personId) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const person = game.people.find((person) => person.id === personId);
- if (person && !person.revealed) {
- this.logger.debug('game ' + accessCode + ': revealing player ' + person.name);
- person.revealed = true;
- namespace.in(accessCode).emit(
- globals.CLIENT_COMMANDS.REVEAL_PLAYER,
- {
- id: person.id,
- gameRole: person.gameRole,
- alignment: person.alignment
- });
- }
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.TRANSFER_MODERATOR, (accessCode, personId) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- let person = game.people.find((person) => person.id === personId);
- if (!person) {
- person = game.spectators.find((spectator) => spectator.id === personId);
- }
- this.transferModeratorPowers(game, person, namespace, this.logger);
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.CHANGE_NAME, (accessCode, data, ackFn) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- const person = findPersonByField(game, 'id', data.personId);
- if (person) {
- if (!isNameTaken(game, data.name)) {
- ackFn('changed');
- person.name = data.name.trim();
- person.hasEnteredName = true;
- namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.CHANGE_NAME, person.id, person.name);
- } else {
- ackFn('taken');
- }
- }
- }
- });
-
- socket.on(globals.CLIENT_COMMANDS.END_GAME, (accessCode) => {
- const game = this.activeGameRunner.activeGames[accessCode];
- if (game) {
- game.status = globals.STATUS.ENDED;
- if (this.activeGameRunner.timerThreads[accessCode]) {
- this.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + accessCode);
- this.activeGameRunner.timerThreads[accessCode].kill();
- delete this.activeGameRunner.timerThreads[accessCode];
- }
- for (const person of game.people) {
- person.revealed = true;
- }
- namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people));
- }
- });
+ setGameSocketNamespace = (namespace) => {
+ this.namespace = namespace;
};
createGame = (gameParams) => {
@@ -196,6 +52,99 @@ class GameManager {
}
};
+ startGame = (game, namespace) => {
+ if (game.isFull) {
+ game.status = globals.STATUS.IN_PROGRESS;
+ if (game.hasTimer) {
+ game.timerParams.paused = true;
+ this.activeGameRunner.runGame(game, namespace);
+ }
+ namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME);
+ }
+ };
+
+ pauseTimer = (game, logger) => {
+ const thread = this.activeGameRunner.timerThreads[game.accessCode];
+ if (thread) {
+ this.logger.debug('Timer thread found for game ' + game.accessCode);
+ thread.send({
+ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER,
+ accessCode: game.accessCode,
+ logLevel: this.logger.logLevel
+ });
+ }
+ };
+
+ resumeTimer = (game, logger) => {
+ const thread = this.activeGameRunner.timerThreads[game.accessCode];
+ if (thread) {
+ this.logger.debug('Timer thread found for game ' + game.accessCode);
+ thread.send({
+ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER,
+ accessCode: game.accessCode,
+ logLevel: this.logger.logLevel
+ });
+ }
+ };
+
+ getTimeRemaining = (game, socket) => {
+ const thread = this.activeGameRunner.timerThreads[game.accessCode];
+ if (thread) {
+ thread.send({
+ command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
+ accessCode: game.accessCode,
+ socketId: socket.id,
+ logLevel: this.logger.logLevel
+ });
+ } else {
+ if (game.timerParams && game.timerParams.timeRemaining === 0) {
+ this.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
+ }
+ }
+ };
+
+ revealPlayer = (game, personId) => {
+ const person = game.people.find((person) => person.id === personId);
+ if (person && !person.revealed) {
+ this.logger.debug('game ' + game.accessCode + ': revealing player ' + person.name);
+ person.revealed = true;
+ this.namespace.in(game.accessCode).emit(
+ globals.EVENT_IDS.REVEAL_PLAYER,
+ {
+ id: person.id,
+ gameRole: person.gameRole,
+ alignment: person.alignment
+ });
+ }
+ };
+
+ changeName = (game, data, ackFn) => {
+ const person = findPersonByField(game, 'id', data.personId);
+ if (person) {
+ if (!isNameTaken(game, data.name)) {
+ ackFn('changed');
+ person.name = data.name.trim();
+ person.hasEnteredName = true;
+ this.namespace.in(game.accessCode).emit(globals.EVENT_IDS.CHANGE_NAME, person.id, person.name);
+ } else {
+ ackFn('taken');
+ }
+ }
+ };
+
+ endGame = (game) => {
+ game.status = globals.STATUS.ENDED;
+ if (this.activeGameRunner.timerThreads[game.accessCode]) {
+ this.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode);
+ this.activeGameRunner.timerThreads[game.accessCode].kill();
+ delete this.activeGameRunner.timerThreads[game.accessCode];
+ }
+ for (const person of game.people) {
+ person.revealed = true;
+ }
+ this.namespace.in(game.accessCode).emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people));
+ };
+
checkAvailability = (code) => {
const game = this.activeGameRunner.activeGames[code.toUpperCase()];
if (game) {
@@ -224,7 +173,7 @@ class GameManager {
: accessCode;
};
- transferModeratorPowers = (game, person, namespace, logger) => {
+ transferModeratorPowers = (game, person, logger) => {
if (person && (person.out || person.userType === globals.USER_TYPES.SPECTATOR)) {
logger.debug('game ' + game.accessCode + ': transferring mod powers to ' + person.name);
if (game.moderator === person) {
@@ -247,7 +196,7 @@ class GameManager {
game.moderator = person;
}
- namespace.in(game.accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
+ this.namespace.in(game.accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
}
};
@@ -258,10 +207,10 @@ class GameManager {
person.userType = globals.USER_TYPES.KILLED_PLAYER;
}
person.out = true;
- namespace.in(game.accessCode).emit(globals.CLIENT_COMMANDS.KILL_PLAYER, person.id);
+ namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id);
// temporary moderators will transfer their powers automatically to the first person they kill.
if (game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
- this.transferModeratorPowers(game, person, namespace, logger);
+ this.transferModeratorPowers(game, person, logger);
}
}
};
@@ -363,7 +312,7 @@ class GameManager {
this.activeGameRunner.runGame(game, namespace);
}
- namespace.in(game.accessCode).emit(globals.CLIENT_COMMANDS.START_GAME);
+ namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME);
};
handleRequestForGameState = async (namespace, logger, gameRunner, accessCode, personCookie, ackFn, clientSocket) => {
@@ -553,10 +502,10 @@ function getGameSize (cards) {
}
class Singleton {
- constructor (logger, environment, namespace) {
+ constructor (logger, environment) {
if (!Singleton.instance) {
logger.info('CREATING SINGLETON GAME MANAGER');
- Singleton.instance = new GameManager(logger, environment, namespace);
+ Singleton.instance = new GameManager(logger, environment);
}
}
diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js
index ce52597..17b65d2 100644
--- a/server/modules/ServerBootstrapper.js
+++ b/server/modules/ServerBootstrapper.js
@@ -4,9 +4,21 @@ const https = require('https');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
-const { RateLimiterMemory } = require('rate-limiter-flexible');
+const SocketManager = require('./SocketManager.js');
+const GameManager = require('./GameManager.js');
+const { ENVIRONMENT } = require('../config/globals.js');
const ServerBootstrapper = {
+
+ singletons: (logger) => {
+ return {
+ socketManager: new SocketManager(logger).getInstance(),
+ gameManager: process.env.NODE_ENV.trim() === 'development'
+ ? new GameManager(logger, ENVIRONMENT.LOCAL).getInstance()
+ : new GameManager(logger, ENVIRONMENT.PRODUCTION).getInstance()
+ };
+ },
+
processCLIArgs: () => {
try {
const args = Array.from(process.argv.map((arg) => arg.trim().toLowerCase()));
@@ -80,46 +92,43 @@ const ServerBootstrapper = {
return main;
},
- createSocketServer: (main, app, port, logger) => {
- let io;
- if (process.env.NODE_ENV.trim() === 'development') {
- io = require('socket.io')(main, {
- cors: { origin: 'http://localhost:' + port }
- });
- } else {
- io = require('socket.io')(main, {
- cors: { origin: 'https://playwerewolf.uk.r.appspot.com' }
- });
- }
+ establishRouting: (app, express) => {
+ /* api endpoints */
+ const games = require('../api/GamesAPI');
+ const admin = require('../api/AdminAPI');
+ app.use('/api/games', games);
+ app.use('/api/admin', admin);
- registerRateLimiter(io, logger);
+ /* serve all the app's pages */
+ app.use('/manifest.json', (req, res) => {
+ res.sendFile(path.join(__dirname, '../../manifest.json'));
+ });
- return io;
- },
+ app.use('/favicon.ico', (req, res) => {
+ res.sendFile(path.join(__dirname, '../../client/favicon_package/favicon.ico'));
+ });
- createGameSocketNamespace (server, logger) {
- const namespace = server.of('/in-game');
- registerRateLimiter(namespace, logger);
- return server.of('/in-game');
+ app.use('/apple-touch-icon.png', (req, res) => {
+ res.sendFile(path.join(__dirname, '../../client/favicon_package/apple-touch-icon.png'));
+ });
+
+ const router = require('../routes/router');
+ app.use('', router);
+
+ app.use('/dist', express.static(path.join(__dirname, '../../client/dist')));
+
+ // set up routing for static content that isn't being bundled.
+ app.use('/images', express.static(path.join(__dirname, '../../client/src/images')));
+ app.use('/styles', express.static(path.join(__dirname, '../../client/src/styles')));
+ app.use('/webfonts', express.static(path.join(__dirname, '../../client/src/webfonts')));
+ app.use('/robots.txt', (req, res) => {
+ res.sendFile(path.join(__dirname, '../../client/robots.txt'));
+ });
+
+ app.use(function (req, res) {
+ res.sendFile(path.join(__dirname, '../../client/src/views/404.html'));
+ });
}
};
-function registerRateLimiter (server, logger) {
- const rateLimiter = new RateLimiterMemory(
- {
- points: 10,
- duration: 1
- });
-
- server.use(async (socket, next) => {
- try {
- await rateLimiter.consume(socket.handshake.address);
- logger.trace('consumed point from ' + socket.handshake.address);
- next();
- } catch (rejection) {
- next(new Error('Your connection has been blocked.'));
- }
- });
-}
-
module.exports = ServerBootstrapper;
diff --git a/server/modules/SocketManager.js b/server/modules/SocketManager.js
index 35bf0e0..0b25bb5 100644
--- a/server/modules/SocketManager.js
+++ b/server/modules/SocketManager.js
@@ -1,21 +1,127 @@
-const globals = require('../config/globals.js');
+const globals = require('../config/globals');
+const EVENT_IDS = globals.EVENT_IDS;
+const { RateLimiterMemory } = require('rate-limiter-flexible');
class SocketManager {
- constructor (logger, io) {
+ constructor (logger) {
this.logger = logger;
- this.io = io;
+ this.io = null;
}
broadcast = (message) => {
- this.io.emit(globals.EVENTS.BROADCAST, message);
+ this.io?.emit(globals.EVENTS.BROADCAST, message);
+ };
+
+ createSocketServer = (main, app, port, logger) => {
+ let io;
+ if (process.env.NODE_ENV.trim() === 'development') {
+ io = require('socket.io')(main, {
+ cors: { origin: 'http://localhost:' + port }
+ });
+ } else {
+ io = require('socket.io')(main, {
+ cors: { origin: 'https://play-werewolf.app' }
+ });
+ }
+
+ registerRateLimiter(io, logger);
+ this.io = io;
+
+ return io;
+ };
+
+ createGameSocketNamespace = (server, logger, gameManager) => {
+ const namespace = server.of('/in-game');
+ const registerHandlers = this.registerHandlers;
+ registerRateLimiter(namespace, logger);
+ namespace.on('connection', function (socket) {
+ socket.on('disconnecting', (reason) => {
+ logger.trace('client socket disconnecting because: ' + reason);
+ });
+
+ registerHandlers(namespace, socket, gameManager);
+ });
+ return server.of('/in-game');
+ };
+
+ registerHandlers = (namespace, socket, gameManager) => {
+ socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args, ackFn) => {
+ const game = gameManager.activeGameRunner.activeGames[accessCode];
+ if (game) {
+ switch (eventId) {
+ case EVENT_IDS.FETCH_GAME_STATE:
+ await gameManager.handleRequestForGameState(
+ this.namespace,
+ this.logger,
+ gameManager.activeGameRunner,
+ accessCode,
+ args.personId,
+ ackFn,
+ socket
+ );
+ break;
+ case EVENT_IDS.START_GAME:
+ gameManager.startGame(game, namespace);
+ break;
+ case EVENT_IDS.PAUSE_TIMER:
+ gameManager.pauseTimer(game, this.logger);
+ break;
+ case EVENT_IDS.RESUME_TIMER:
+ gameManager.resumeTimer(game, this.logger);
+ break;
+ case EVENT_IDS.GET_TIME_REMAINING:
+ gameManager.getTimeRemaining(game, socket);
+ break;
+ case EVENT_IDS.KILL_PLAYER:
+ gameManager.killPlayer(game, game.people.find((person) => person.id === args.personId), namespace, this.logger);
+ break;
+ case EVENT_IDS.REVEAL_PLAYER:
+ gameManager.revealPlayer(game, args.personId);
+ break;
+ case EVENT_IDS.TRANSFER_MODERATOR:
+ let person = game.people.find((person) => person.id === args.personId);
+ if (!person) {
+ person = game.spectators.find((spectator) => spectator.id === args.personId);
+ }
+ gameManager.transferModeratorPowers(game, person, this.logger);
+ break;
+ case EVENT_IDS.CHANGE_NAME:
+ gameManager.changeName(game, args.data, ackFn);
+ break;
+ case EVENT_IDS.END_GAME:
+ gameManager.endGame(game);
+ break;
+ default:
+ break;
+ }
+ }
+ });
};
}
+function registerRateLimiter (server, logger) {
+ const rateLimiter = new RateLimiterMemory(
+ {
+ points: 10,
+ duration: 1
+ });
+
+ server.use(async (socket, next) => {
+ try {
+ await rateLimiter.consume(socket.handshake.address);
+ logger.trace('consumed point from ' + socket.handshake.address);
+ next();
+ } catch (rejection) {
+ next(new Error('Your connection has been blocked.'));
+ }
+ });
+}
+
class Singleton {
- constructor (logger, io) {
+ constructor (logger) {
if (!Singleton.instance) {
logger.info('CREATING SINGLETON SOCKET MANAGER');
- Singleton.instance = new SocketManager(logger, io);
+ Singleton.instance = new SocketManager(logger);
}
}
diff --git a/spec/unit/server/modules/GameManager_Spec.js b/spec/unit/server/modules/GameManager_Spec.js
index 2d172e8..11ae7fc 100644
--- a/spec/unit/server/modules/GameManager_Spec.js
+++ b/spec/unit/server/modules/GameManager_Spec.js
@@ -17,7 +17,8 @@ describe('GameManager', () => {
const inObj = { emit: () => {} };
namespace = { in: () => { return inObj; } };
- gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, namespace).getInstance();
+ gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION).getInstance();
+ gameManager.setGameSocketNamespace(namespace);
});
beforeEach(() => {
@@ -36,7 +37,7 @@ describe('GameManager', () => {
false,
moderator
);
- gameManager.transferModeratorPowers(game, personToTransferTo, namespace, logger);
+ gameManager.transferModeratorPowers(game, personToTransferTo, logger);
expect(game.moderator).toEqual(personToTransferTo);
expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
@@ -55,7 +56,7 @@ describe('GameManager', () => {
moderator
);
game.spectators.push(personToTransferTo);
- gameManager.transferModeratorPowers(game, personToTransferTo, namespace, logger);
+ gameManager.transferModeratorPowers(game, personToTransferTo, logger);
expect(game.moderator).toEqual(personToTransferTo);
expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
@@ -74,7 +75,7 @@ describe('GameManager', () => {
false,
tempMod
);
- gameManager.transferModeratorPowers(game, personToTransferTo, namespace, logger);
+ gameManager.transferModeratorPowers(game, personToTransferTo, logger);
expect(game.moderator).toEqual(personToTransferTo);
expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
@@ -93,7 +94,7 @@ describe('GameManager', () => {
false,
tempMod
);
- gameManager.transferModeratorPowers(game, personToTransferTo, namespace, logger);
+ gameManager.transferModeratorPowers(game, personToTransferTo, logger);
expect(game.moderator).toEqual(personToTransferTo);
expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR);
@@ -330,7 +331,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
});
it('should reset all relevant game parameters, including when the game has a timer', async () => {
@@ -357,7 +358,7 @@ describe('GameManager', () => {
expect(runGameSpy).toHaveBeenCalled();
expect(Object.keys(gameManager.activeGameRunner.timerThreads).length).toEqual(0);
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
});
it('should reset all relevant game parameters and preserve temporary moderator', async () => {
@@ -378,7 +379,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
});
it('should reset all relevant game parameters and restore a temporary moderator from a dedicated moderator', async () => {
@@ -399,7 +400,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
});
it('should reset all relevant game parameters and create a temporary mod if a dedicated mod transferred to a killed player', async () => {
@@ -420,7 +421,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
});
});
});