mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 15:57:50 +01:00
more refactoring
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user