From 91fbed7859326b3c44853a287991cb369e965e21 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Thu, 12 Jan 2023 18:07:33 -0500 Subject: [PATCH] further redis effort --- client/src/config/globals.js | 8 +- client/src/modules/game_state/states/Ended.js | 8 +- .../modules/game_state/states/InProgress.js | 77 +++-- client/src/modules/game_state/states/Lobby.js | 35 +- .../states/shared/SharedStateUtil.js | 13 +- client/src/styles/game.css | 19 ++ server/api/AdminAPI.js | 6 +- server/api/GamesAPI.js | 8 +- server/config/globals.js | 17 +- server/model/Game.js | 6 +- server/model/Person.js | 5 +- server/modules/Events.js | 252 ++++++++++++--- server/modules/GameStateCurator.js | 25 +- server/modules/singletons/ActiveGameRunner.js | 10 +- server/modules/singletons/GameManager.js | 301 ++++-------------- server/modules/singletons/SocketManager.js | 75 +---- 16 files changed, 452 insertions(+), 413 deletions(-) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 5a46ec7..0a487f4 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -1,6 +1,6 @@ export const globals = { CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', - USER_SIGNATURE_LENGTH: 25, + USER_SIGNATURE_LENGTH: 75, CLOCK_TICK_INTERVAL_MILLIS: 100, MAX_CUSTOM_ROLE_NAME_LENGTH: 50, MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, @@ -50,8 +50,10 @@ export const globals = { SYNC_GAME_STATE: 'syncGameState', START_TIMER: 'startTimer', PLAYER_LEFT: 'playerLeft', - UPDATE_SPECTATORS: 'newSpectator', - RESTART_GAME: 'restartGame' + ADD_SPECTATOR: 'addSpectator', + UPDATE_SPECTATORS: 'updateSpectators', + RESTART_GAME: 'restartGame', + ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' }, USER_TYPES: { MODERATOR: 'moderator', diff --git a/client/src/modules/game_state/states/Ended.js b/client/src/modules/game_state/states/Ended.js index 773c184..72b03cb 100644 --- a/client/src/modules/game_state/states/Ended.js +++ b/client/src/modules/game_state/states/Ended.js @@ -27,7 +27,11 @@ export class Ended { // sortPeopleByStatus(this.stateBucket.currentGameState.people); const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; renderGroupOfPlayers( - this.stateBucket.currentGameState.people, + this.stateBucket.currentGameState.people.filter( + p => p.userType === globals.USER_TYPES.PLAYER + || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + || p.killed + ), this.stateBucket.currentGameState.accessCode, null, modType, @@ -35,7 +39,7 @@ export class Ended { ); document.getElementById('players-alive-label').innerText = 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + - this.stateBucket.currentGameState.people.length + ' Alive'; + this.stateBucket.currentGameState.gameSize + ' Alive'; } } diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 8b6d5d4..de8c8cb 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -61,11 +61,11 @@ export class InProgress { if (spectatorCount) { spectatorCount?.addEventListener('click', () => { - Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.spectators), null, true); + Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR)), null, true); }); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.spectators.length, + this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, spectatorCount ); } @@ -90,9 +90,16 @@ export class InProgress { /* TODO: UX issue - it's easier to parse visually when players are sorted this way, but shifting players around when they are killed or revealed is bad UX for the moderator. */ // sortPeopleByStatus(this.stateBucket.currentGameState.people); - const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; + const modType = tempMod + ? this.stateBucket.currentGameState.people.find(person => + person.id === this.stateBucket.currentGameState.currentModeratorId).userType + : null; this.renderGroupOfPlayers( - this.stateBucket.currentGameState.people, + this.stateBucket.currentGameState.people.filter( + p => p.userType === globals.USER_TYPES.PLAYER + || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + || p.killed + ), this.killPlayerHandlers, this.revealRoleHandlers, this.stateBucket.currentGameState.accessCode, @@ -102,7 +109,7 @@ export class InProgress { ); document.getElementById('players-alive-label').innerText = 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + - this.stateBucket.currentGameState.people.length + ' Alive'; + this.stateBucket.currentGameState.gameSize + ' Alive'; } removePlayerListEventListeners (removeEl = true) { @@ -155,6 +162,7 @@ export class InProgress { const killedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === id); if (killedPerson) { killedPerson.out = true; + killedPerson.killed = true; killedPerson.userType = globals.USER_TYPES.KILLED_PLAYER; if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { toast(killedPerson.name + ' killed.', 'success', true, true, 'medium'); @@ -203,14 +211,27 @@ export class InProgress { } }); - if (this.socket.hasListeners(globals.EVENT_IDS.UPDATE_SPECTATORS)) { - this.socket.removeAllListeners(globals.EVENT_IDS.UPDATE_SPECTATORS); + if (this.socket.hasListeners(globals.EVENT_IDS.ADD_SPECTATOR)) { + this.socket.removeAllListeners(globals.EVENT_IDS.ADD_SPECTATOR); } - this.socket.on(globals.EVENT_IDS.UPDATE_SPECTATORS, (updatedSpectatorList) => { - stateBucket.currentGameState.spectators = updatedSpectatorList; + this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => { + stateBucket.currentGameState.people.push(spectator); SharedStateUtil.setNumberOfSpectators( - stateBucket.currentGameState.spectators.length, + stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + document.getElementById('spectator-count') + ); + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + this.displayAvailableModerators(); + } + }); + + this.socket.on(globals.EVENT_IDS.UPDATE_SPECTATORS, (spectators) => { + stateBucket.currentGameState.people = stateBucket.currentGameState.people.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR); + stateBucket.currentGameState.people = stateBucket.currentGameState.people.concat(spectators); + SharedStateUtil.setNumberOfSpectators( + stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, document.getElementById('spectator-count') ); if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR @@ -231,15 +252,26 @@ export class InProgress { this.stateBucket.currentGameState.people.sort((a, b) => { return a.name >= b.name ? 1 : -1; }); - const teamGood = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD); - const teamEvil = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.EVIL); + const teamGood = this.stateBucket.currentGameState.people.filter( + (p) => p.alignment === globals.ALIGNMENT.GOOD + && (p.userType === globals.USER_TYPES.PLAYER + || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + || p.killed) + + ); + const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL + && (p.userType === globals.USER_TYPES.PLAYER + || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + || p.killed) + ); this.renderGroupOfPlayers( teamEvil, this.killPlayerHandlers, this.revealRoleHandlers, this.stateBucket.currentGameState.accessCode, globals.ALIGNMENT.EVIL, - this.stateBucket.currentGameState.moderator.userType, + this.stateBucket.currentGameState.people.find(person => + person.id === this.stateBucket.currentGameState.currentModeratorId).userType, this.socket ); this.renderGroupOfPlayers( @@ -248,12 +280,13 @@ export class InProgress { this.revealRoleHandlers, this.stateBucket.currentGameState.accessCode, globals.ALIGNMENT.GOOD, - this.stateBucket.currentGameState.moderator.userType, + this.stateBucket.currentGameState.people.find(person => + person.id === this.stateBucket.currentGameState.currentModeratorId).userType, this.socket ); document.getElementById('players-alive-label').innerText = 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + - this.stateBucket.currentGameState.people.length + ' Alive'; + this.stateBucket.currentGameState.gameSize + ' Alive'; } renderGroupOfPlayers ( @@ -302,7 +335,11 @@ export class InProgress { } else if (!player.out && moderatorType) { killPlayerHandlers[player.id] = () => { Confirmation('Kill \'' + player.name + '\'?', () => { - socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.KILL_PLAYER, accessCode, { personId: player.id }); + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.ASSIGN_DEDICATED_MOD, accessCode, { personId: player.id }); + } else { + socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.KILL_PLAYER, accessCode, { personId: player.id }); + } }); }; playerEl.querySelector('.kill-player-button').addEventListener('click', killPlayerHandlers[player.id]); @@ -347,12 +384,6 @@ export class InProgress { this.transferModHandlers, this.socket ); - renderPotentialMods( // spectators can also be made mods. - this.stateBucket.currentGameState, - this.stateBucket.currentGameState.spectators, - this.transferModHandlers, - this.socket - ); if (document.querySelectorAll('.potential-moderator').length === 0) { document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.'; @@ -475,7 +506,7 @@ function insertPlaceholderButton (container, append, type) { function renderPotentialMods (gameState, group, transferModHandlers, socket) { const modalContent = document.getElementById('transfer-mod-modal-content'); for (const member of group) { - if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { + if ((member.userType === globals.USER_TYPES.KILLED_PLAYER || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { const container = document.createElement('div'); container.classList.add('potential-moderator'); container.setAttribute('tabindex', '0'); diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index 8f726c1..49e9fa6 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -48,11 +48,11 @@ export class Lobby { playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players'; this.container.querySelector('#spectator-count').addEventListener('click', () => { - Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.spectators), null, true); + Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people), null, true); }); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.spectators.length, + this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, this.container.querySelector('#spectator-count') ); @@ -68,18 +68,20 @@ export class Lobby { populatePlayers () { document.querySelectorAll('.lobby-player').forEach((el) => el.remove()); const lobbyPlayersContainer = this.container.querySelector('#lobby-players'); - if (this.stateBucket.currentGameState.moderator.userType === globals.USER_TYPES.MODERATOR) { - lobbyPlayersContainer.appendChild( - renderLobbyPerson( - this.stateBucket.currentGameState.moderator.name, - this.stateBucket.currentGameState.moderator.userType - ) - ); - } - for (const person of this.stateBucket.currentGameState.people) { + const sorted = this.stateBucket.currentGameState.people.sort( + function (a, b) { + if (a.userType === globals.USER_TYPES.MODERATOR) { + return -1; + } + return 1; + } + ); + for (const person of sorted.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR)) { lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType)); } - const playerCount = this.stateBucket.currentGameState.people.length; + const playerCount = this.stateBucket.currentGameState.people.filter( + p => p.userType === globals.USER_TYPES.PLAYER || p.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ).length; document.querySelector("label[for='lobby-players']").innerText = 'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; } @@ -99,10 +101,10 @@ export class Lobby { } }); - this.socket.on(globals.EVENT_IDS.UPDATE_SPECTATORS, (updatedSpectatorList) => { - this.stateBucket.currentGameState.spectators = updatedSpectatorList; + this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => { + this.stateBucket.currentGameState.people.push(spectator); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.spectators.length, + this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, document.getElementById('spectator-count') ); }); @@ -193,6 +195,9 @@ function renderLobbyPerson (name, userType) { personNameEl.innerText = name; personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; el.classList.add('lobby-player'); + if (userType === globals.USER_TYPES.MODERATOR) { + el.classList.add('moderator'); + } el.appendChild(personNameEl); el.appendChild(personTypeEl); diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index e2d52cb..614392f 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -132,8 +132,9 @@ export const SharedStateUtil = { } }, - buildSpectatorList (spectators) { + buildSpectatorList (people) { const list = document.createElement('div'); + const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR); if (spectators.length === 0) { list.innerHTML = '
Nobody currently spectating.
'; } else { @@ -173,8 +174,18 @@ function processGameState ( easing: 'ease-in-out', fill: 'both' }); + const clientAnimation = document.getElementById('client-container').animate([ + { opacity: '0' }, + { opacity: '1' } + ], { + duration: 500, + easing: 'ease-out', + fill: 'both' + }); + if (animateContainer) { containerAnimation.play(); + clientAnimation.play(); } displayClientInfo(currentGameState.client.name, currentGameState.client.userType); diff --git a/client/src/styles/game.css b/client/src/styles/game.css index ebccfe2..5972d24 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -13,6 +13,10 @@ margin: 0 auto 0.25em auto; } +.moderator { + border: 2px solid #c58f13; +} + .potential-moderator { margin: 0.5em auto; } @@ -952,3 +956,18 @@ canvas { transform: translateY(0px); } } + +@keyframes fade-in-slide-down { + 0% { + opacity: 0; + transform: translateY(-20px); + } + 5% { + opacity: 1; + transform: translateY(0px); + } + 100% { + opacity: 1; + transform: translateY(0px); + } +} diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index ebf5c09..aaf0aaa 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -3,7 +3,7 @@ const router = express.Router(); const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug'); const logger = require('../modules/Logger')(debugMode); const socketManager = (require('../modules/singletons/SocketManager.js')).instance; -const gameManager = (require('../modules/singletons/GameManager.js')).instance; +const activeGameRunner = (require('../modules/singletons/ActiveGameRunner.js')).instance; const globals = require('../config/globals.js'); const cors = require('cors'); @@ -22,8 +22,8 @@ router.post('/sockets/broadcast', function (req, res) { router.get('/games/state', async (req, res) => { const gamesArray = []; - await this.client.hGetAll('activeGames').then(async (r) => { - Object.values(r).forEach((v) => gamesArray.push(v)); + await activeGameRunner.client.keys('*').then(async (r) => { + Object.values(r).forEach((v) => gamesArray.push(JSON.parse(v))); }); res.status(200).send(gamesArray); }); diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 89147e3..f7b6b78 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -91,7 +91,7 @@ router.patch('/:code/players', async function (req, res) { } }); -router.patch('/:code/restart', function (req, res) { +router.patch('/:code/restart', async function (req, res) { if ( req.body === null || !validateAccessCode(req.body.accessCode) @@ -101,7 +101,7 @@ router.patch('/:code/restart', function (req, res) { ) { res.status(400).send(); } else { - const game = gameManager.activeGameRunner.getActiveGame(req.body.accessCode); + const game = await gameManager.activeGameRunner.getActiveGame(req.body.accessCode); if (game) { gameManager.restartGame(game, gameManager.namespace).then((data) => { res.status(200).send(); @@ -123,11 +123,11 @@ function validateName (name) { } function validateCookie (cookie) { - return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === globals.USER_SIGNATURE_LENGTH); + return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === globals.INSTANCE_ID_LENGTH); } function validateAccessCode (accessCode) { - return /^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH; + return /^[a-zA-Z0-9]+$/.test(accessCode) && accessCode?.length === globals.ACCESS_CODE_LENGTH; } function validateSpectatorFlag (spectatorFlag) { diff --git a/server/config/globals.js b/server/config/globals.js index 0a2917c..9e03373 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -11,7 +11,7 @@ const globals = { EVIL: 'evil' }, REDIS_CHANNELS: { - ACTIVE_GAME_STREAM: 'active_game_stream' + ACTIVE_GAME_STREAM: 'active_game_stream' }, CORS: process.env.NODE_ENV?.trim() === 'development' ? { @@ -30,7 +30,7 @@ const globals = { res.status(400).send('Request has invalid content type.'); } }, - STALE_GAME_HOURS: 24, + STALE_GAME_SECONDS: 86400, SOCKET_EVENTS: { IN_GAME_MESSAGE: 'inGameMessage' }, @@ -49,8 +49,10 @@ const globals = { RESTART_GAME: 'restartGame', PLAYER_JOINED: 'playerJoined', UPDATE_SPECTATORS: 'updateSpectators', + ADD_SPECTATOR: 'addSpectator', SYNC_GAME_STATE: 'syncGameState', - UPDATE_SOCKET: 'updateSocket' + UPDATE_SOCKET: 'updateSocket', + ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' }, SYNCABLE_EVENTS: function () { return [ @@ -65,10 +67,13 @@ const globals = { this.EVENT_IDS.END_GAME, this.EVENT_IDS.RESTART_GAME, this.EVENT_IDS.PLAYER_JOINED, - this.EVENT_IDS.UPDATE_SPECTATORS, + this.EVENT_IDS.ADD_SPECTATOR, + this.EVENT_IDS.REMOVE_SPECTATOR, this.EVENT_IDS.SYNC_GAME_STATE, - this.EVENT_IDS.UPDATE_SOCKET - ] + this.EVENT_IDS.UPDATE_SOCKET, + this.EVENT_IDS.FETCH_GAME_STATE, + this.EVENT_IDS.ASSIGN_DEDICATED_MOD + ]; }, MESSAGES: { ENTER_NAME: 'Client must enter name.' diff --git a/server/model/Game.js b/server/model/Game.js index cd1be2f..6ef153f 100644 --- a/server/model/Game.js +++ b/server/model/Game.js @@ -5,7 +5,7 @@ class Game { people, deck, hasTimer, - moderator, + currentModeratorId, hasDedicatedModerator, originalModeratorId, createTime, @@ -13,7 +13,7 @@ class Game { ) { this.accessCode = accessCode; this.status = status; - this.moderator = moderator; + this.currentModeratorId = currentModeratorId; this.people = people; this.deck = deck; this.gameSize = deck.reduce( @@ -23,11 +23,11 @@ class Game { this.hasTimer = hasTimer; this.hasDedicatedModerator = hasDedicatedModerator; this.originalModeratorId = originalModeratorId; + this.previousModeratorId = null; this.createTime = createTime; this.timerParams = timerParams; this.isFull = this.gameSize === 1 && !this.hasDedicatedModerator; this.timeRemaining = null; - this.spectators = []; } } diff --git a/server/model/Person.js b/server/model/Person.js index 3c6afa0..21c859b 100644 --- a/server/model/Person.js +++ b/server/model/Person.js @@ -1,4 +1,6 @@ // noinspection DuplicatedCode +const globals = require('../config/globals'); + class Person { constructor (id, cookie, name, userType, gameRole = null, gameRoleDescription = null, alignment = null, assigned = false) { this.id = id; @@ -10,7 +12,8 @@ class Person { this.gameRoleDescription = gameRoleDescription; this.alignment = alignment; this.assigned = assigned; - this.out = false; + this.out = userType === globals.USER_TYPES.MODERATOR || userType === globals.USER_TYPES.SPECTATOR; + this.killed = false; this.revealed = false; this.hasEnteredName = false; } diff --git a/server/modules/Events.js b/server/modules/Events.js index e3373c3..f497937 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -1,66 +1,242 @@ const globals = require('../config/globals'); -const GameStateCurator = require("./GameStateCurator"); +const GameStateCurator = require('./GameStateCurator'); const EVENT_IDS = globals.EVENT_IDS; const Events = [ { id: EVENT_IDS.PLAYER_JOINED, - stateChange: (game, args, gameManager) => { - let toBeAssignedIndex = game.people.findIndex( - (person) => person.id === args.id && person.assigned === false + stateChange: (game, socketArgs, vars) => { + const toBeAssignedIndex = game.people.findIndex( + (person) => person.id === socketArgs.id && person.assigned === false ); if (toBeAssignedIndex >= 0) { - game.people[toBeAssignedIndex] = args; - game.isFull = gameManager.isGameFull(game); + game.people[toBeAssignedIndex] = socketArgs; + game.isFull = vars.gameManager.isGameFull(game); } }, - communicate: (game, args, gameManager) => { - gameManager.namespace.in(game.accessCode).emit( + communicate: (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, - GameStateCurator.mapPerson(args), + GameStateCurator.mapPerson(socketArgs), game.isFull ); } }, { - id: EVENT_IDS.UPDATE_SPECTATORS, - stateChange: (game, args, gameManager) => { - game.spectators = args; + id: EVENT_IDS.ADD_SPECTATOR, + stateChange: (game, socketArgs, vars) => { + game.people.push(socketArgs); }, - communicate: (game, args, gameManager) => { - gameManager.namespace.in(game.accessCode).emit( - globals.EVENTS.UPDATE_SPECTATORS, - game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) + communicate: (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit( + globals.EVENT_IDS.ADD_SPECTATOR, + GameStateCurator.mapPerson(socketArgs) + ); + } + }, + { + id: EVENT_IDS.REMOVE_SPECTATOR, + stateChange: (game, socketArgs, vars) => { + const spectatorIndex = game.people.findIndex(person => person.userType === globals.USER_TYPES.SPECTATOR && person.id === socketArgs.personId); + if (spectatorIndex >= 0) { + game.people.splice(spectatorIndex, 1); + } + }, + communicate: (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit( + globals.EVENT_IDS.REMOVE_SPECTATOR, + GameStateCurator.mapPerson(socketArgs) ); } }, { id: EVENT_IDS.FETCH_GAME_STATE, - stateChange: (game, args, gameManager) => { - const matchingPerson = gameManager.findPersonByField(game, 'cookie', args.personId); - if (matchingPerson) { - if (matchingPerson.socketId === socketId) { - logger.debug('matching person found with an established connection to the room: ' + matchingPerson.name); - if (ackFn) { - ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); - } - } else { - logger.debug('matching person found with a new connection to the room: ' + matchingPerson.name); - this.namespace.sockets.get(socketId).join(accessCode); - matchingPerson.socketId = socketId; - await this.publisher.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SOCKET + ';' + JSON.stringify({ personId: matchingPerson.id, socketId: socketId }) + ';' + this.instanceId - ); - if (ackFn) { - ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); - } - } + stateChange: (game, socketArgs, vars) => { + const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId); + if (matchingPerson && matchingPerson.socketId !== vars.socketId) { + matchingPerson.socketId = vars.socketId; + vars.gameManager.namespace.sockets.get(vars.socketId)?.join(game.accessCode); + } + }, + communicate: (game, socketArgs, vars) => { + if (!vars.ackFn) return; + const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId); + if (matchingPerson && vars.gameManager.namespace.sockets.get(matchingPerson.socketId)) { + vars.ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); } else { - if (ackFn) { - rejectClientRequestForGameState(ackFn); + vars.ackFn(null); + } + } + }, + // { + // id: EVENT_IDS.UPDATE_SOCKET, + // stateChange: (game, socketArgs, vars) => { + // const matchingPerson = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); + // if (matchingPerson) { + // matchingPerson.socketId = socketArgs.socketId; + // } + // } + // } + { + id: EVENT_IDS.SYNC_GAME_STATE, + stateChange: (game, socketArgs, vars) => {}, + communicate: (game, socketArgs, vars) => { + const matchingPerson = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); + if (matchingPerson && vars.gameManager.namespace.sockets.get(matchingPerson.socketId)) { + vars.gameManager.namespace.to(matchingPerson.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } + } + }, + { + id: EVENT_IDS.START_GAME, + stateChange: (game, socketArgs, vars) => { + if (game.isFull) { + game.status = globals.STATUS.IN_PROGRESS; + if (game.hasTimer) { + game.timerParams.paused = true; + // this.activeGameRunner.runGame(game, namespace); } } + }, + communicate: (game, socketArgs, vars) => { + if (vars.ackFn) { + vars.ackFn(); + } + vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME); + } + }, + { + id: EVENT_IDS.KILL_PLAYER, + stateChange: (game, socketArgs, vars) => { + const person = game.people.find((person) => person.id === socketArgs.personId); + if (person && !person.out) { + person.userType = globals.USER_TYPES.KILLED_PLAYER; + person.out = true; + person.killed = true; + } + }, + communicate: (game, socketArgs, vars) => { + const person = game.people.find((person) => person.id === socketArgs.personId); + if (person) { + vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); + } + } + }, + { + id: EVENT_IDS.REVEAL_PLAYER, + stateChange: (game, socketArgs, vars) => { + const person = game.people.find((person) => person.id === socketArgs.personId); + if (person && !person.revealed) { + person.revealed = true; + } + }, + communicate: (game, socketArgs, vars) => { + const person = game.people.find((person) => person.id === socketArgs.personId); + if (person) { + vars.gameManager.namespace.in(game.accessCode).emit( + globals.EVENT_IDS.REVEAL_PLAYER, + { + id: person.id, + gameRole: person.gameRole, + alignment: person.alignment + } + ); + } + } + }, + { + id: EVENT_IDS.END_GAME, + stateChange: (game, socketArgs, vars) => { + 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(); + // } + for (const person of game.people) { + person.revealed = true; + } + }, + communicate: (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode) + .emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); + if (vars.ackFn) { + vars.ackFn(); + } + } + }, + { + id: EVENT_IDS.TRANSFER_MODERATOR, + stateChange: (game, socketArgs, vars) => { + const currentModerator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); + const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); + if (currentModerator) { + if (currentModerator.gameRole) { + currentModerator.userType = globals.USER_TYPES.KILLED_PLAYER; + } else { + currentModerator.userType = globals.USER_TYPES.SPECTATOR; + } + game.previousModeratorId = currentModerator.id; + } + if (toTransferTo) { + toTransferTo.userType = globals.USER_TYPES.MODERATOR; + game.currentModeratorId = toTransferTo.id; + } + }, + communicate: (game, socketArgs, vars) => { + const moderator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); + const previousModerator = vars.gameManager.findPersonByField(game, 'id', game.previousModeratorId); + if (moderator && vars.gameManager.namespace.sockets.get(moderator.socketId)) { + vars.gameManager.namespace.to(moderator.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } + if (previousModerator && vars.gameManager.namespace.sockets.get(previousModerator.socketId)) { + vars.gameManager.namespace.to(previousModerator.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } + vars.gameManager.namespace.to(game.accessCode).emit(globals.EVENT_IDS.UPDATE_SPECTATORS, game.people + .filter(p => p.userType === globals.USER_TYPES.SPECTATOR) + .map(spectator => GameStateCurator.mapPerson(spectator)) + ); + } + }, + { + id: EVENT_IDS.ASSIGN_DEDICATED_MOD, + stateChange: (game, socketArgs, vars) => { + const currentModerator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); + const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); + if (currentModerator && toTransferTo) { + if (currentModerator.id !== toTransferTo.id) { + currentModerator.userType = globals.USER_TYPES.PLAYER; + } + + toTransferTo.userType = globals.USER_TYPES.MODERATOR; + toTransferTo.out = true; + toTransferTo.killed = true; + game.previousModeratorId = currentModerator.id; + game.currentModeratorId = toTransferTo.id; + } + }, + communicate: (game, socketArgs, vars) => { + const moderator = vars.gameManager.findPersonByField(game, 'id', game.currentModeratorId); + const moderatorSocket = vars.gameManager.namespace.sockets.get(moderator?.socketId); + if (moderator && moderatorSocket) { + vars.gameManager.namespace.to(moderator.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + moderatorSocket.to(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, game.previousModeratorId); + } else { + vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, game.currentModeratorId); + } + const previousModerator = vars.gameManager.findPersonByField(game, 'id', game.previousModeratorId); + if (previousModerator && previousModerator.id !== moderator.id && vars.gameManager.namespace.sockets.get(previousModerator.socketId)) { + vars.gameManager.namespace.to(previousModerator.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); + } + } + }, + { + id: EVENT_IDS.RESTART_GAME, + stateChange: (game, socketArgs, vars) => {}, + communicate: (game, socketArgs, vars) => { + if (vars.ackFn) { + vars.ackFn(); + } + vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME); } } ]; diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index 317bb5c..836b3b5 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -12,7 +12,7 @@ const GameStateCurator = { mapPeopleForModerator: (people) => { return people .filter((person) => { - return person.assigned === true; + return person.assigned === true || (person.userType === globals.USER_TYPES.SPECTATOR || person.userType === globals.USER_TYPES.MODERATOR); }) .map((person) => ({ name: person.name, @@ -22,6 +22,7 @@ const GameStateCurator = { gameRoleDescription: person.gameRoleDescription, alignment: person.alignment, out: person.out, + killed: person.killed, revealed: person.revealed })); }, @@ -32,12 +33,13 @@ const GameStateCurator = { id: person.id, userType: person.userType, out: person.out, + killed: person.killed, revealed: person.revealed, gameRole: person.gameRole, alignment: person.alignment }; } else { - return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed }; + return { name: person.name, id: person.id, userType: person.userType, out: person.out, killed: person.killed, revealed: person.revealed }; } } }; @@ -55,23 +57,21 @@ function getGameStateBasedOnPermissions (game, person) { gameRoleDescription: person.gameRoleDescription, customRole: person.customRole, alignment: person.alignment, - out: person.out + out: person.out, + killed: person.killed }; switch (person.userType) { case globals.USER_TYPES.MODERATOR: return { accessCode: game.accessCode, status: game.status, - moderator: GameStateCurator.mapPerson(game.moderator), + currentModeratorId: game.currentModeratorId, client: client, deck: game.deck, gameSize: game.gameSize, people: GameStateCurator.mapPeopleForModerator(game.people, client), timerParams: game.timerParams, - isFull: game.isFull, - spectators: game.spectators.map((filteredPerson) => - GameStateCurator.mapPerson(filteredPerson) - ) + isFull: game.isFull }; case globals.USER_TYPES.TEMPORARY_MODERATOR: case globals.USER_TYPES.SPECTATOR: @@ -80,20 +80,17 @@ function getGameStateBasedOnPermissions (game, person) { return { accessCode: game.accessCode, status: game.status, - moderator: GameStateCurator.mapPerson(game.moderator), + currentModeratorId: game.currentModeratorId, client: client, deck: game.deck, gameSize: game.gameSize, people: game.people .filter((person) => { - return person.assigned === true; + return person.assigned === true || person.userType === globals.USER_TYPES.SPECTATOR; }) .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), timerParams: game.timerParams, - isFull: game.isFull, - spectators: game.spectators.map((filteredPerson) => - GameStateCurator.mapPerson(filteredPerson) - ) + isFull: game.isFull }; default: break; diff --git a/server/modules/singletons/ActiveGameRunner.js b/server/modules/singletons/ActiveGameRunner.js index 65f535e..bf46d7b 100644 --- a/server/modules/singletons/ActiveGameRunner.js +++ b/server/modules/singletons/ActiveGameRunner.js @@ -19,8 +19,8 @@ class ActiveGameRunner { } getActiveGame = async (accessCode) => { - const r = await this.client.hGet('activeGames', accessCode); - return JSON.parse(r); + const r = await this.client.get(accessCode); + return r === null ? r : JSON.parse(r); } createGameSyncSubscriber = async (gameManager, socketManager) => { @@ -28,7 +28,7 @@ class ActiveGameRunner { await this.subscriber.connect(); await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => { this.logger.info('MESSAGE: ' + message); - let messageComponents = message.split(';'); + const messageComponents = message.split(';'); if (messageComponents[messageComponents.length - 1] === this.instanceId) { this.logger.trace('Disregarding self-authored message'); return; @@ -44,10 +44,10 @@ class ActiveGameRunner { game, null, game?.accessCode || messageComponents[0], - args ? args : null, + args || null, null, true - ) + ); } }); this.logger.info('ACTIVE GAME RUNNER - CREATED GAME SYNC SUBSCRIBER'); diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index cf5f438..3b2045c 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -6,7 +6,6 @@ const UsernameGenerator = require('../UsernameGenerator'); const GameCreationRequest = require('../../model/GameCreationRequest'); const redis = require('redis'); - class GameManager { constructor (logger, environment, instanceId) { if (GameManager.instance) { @@ -34,8 +33,8 @@ class GameManager { }; refreshGame = async (game) => { - this.logger.debug('PUSHING REFRESH OF ' + game.accessCode); - await this.activeGameRunner.client.hSet('activeGames', game.accessCode, JSON.stringify(game)); + this.logger.debug('PUSHING REFRESH OF ' + game.accessCode); + await this.activeGameRunner.client.set(game.accessCode, JSON.stringify(game)); } createGame = async (gameParams) => { @@ -48,12 +47,12 @@ class GameManager { gameParams.moderatorName, gameParams.hasDedicatedModerator ); - //await this.pruneStaleGames(); const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); if (newAccessCode === null) { return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); } const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator); + console.log(moderator); moderator.assigned = true; if (req.timerParams !== null) { req.timerParams.paused = false; @@ -64,13 +63,15 @@ class GameManager { initializePeopleForGame(req.deck, moderator, this.shuffle), req.deck, req.hasTimer, - moderator, + moderator.id, req.hasDedicatedModerator, moderator.id, new Date().toJSON(), req.timerParams ); - await this.activeGameRunner.client.hSet('activeGames', newAccessCode, JSON.stringify(newGame)); + await this.activeGameRunner.client.set(newAccessCode, JSON.stringify(newGame), { + EX: globals.STALE_GAME_SECONDS + }); return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment }); }).catch((message) => { console.log(message); @@ -79,17 +80,6 @@ class GameManager { }); }; - startGame = async (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 = async (game, logger) => { const thread = this.activeGameRunner.timerThreads[game.accessCode]; if (thread && !thread.killed) { @@ -132,34 +122,6 @@ class GameManager { } }; - revealPlayer = async (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 - } - ); - } - }; - - endGame = async (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(); - } - 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 = async (code) => { const game = await this.activeGameRunner.getActiveGame(code.toUpperCase().trim()); if (game) { @@ -173,7 +135,7 @@ class GameManager { const charCount = charPool.length; let codeDigits, accessCode; let attempts = 0; - while (!accessCode || ((await this.activeGameRunner.client.hKeys('activeGames')).includes(accessCode) + while (!accessCode || ((await this.activeGameRunner.client.keys('*')).includes(accessCode) && attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) { codeDigits = []; let iterations = globals.ACCESS_CODE_LENGTH; @@ -184,76 +146,11 @@ class GameManager { accessCode = codeDigits.join(''); attempts ++; } - return (await this.activeGameRunner.client.hKeys('activeGames')).includes(accessCode) + return (await this.activeGameRunner.client.keys('*')).includes(accessCode) ? null : accessCode; }; - transferModeratorPowers = async (socketId, game, person, namespace, logger) => { - if (person && (person.out || person.userType === globals.USER_TYPES.SPECTATOR)) { - let spectatorsUpdated = false; - if (game.spectators.includes(person)) { - game.spectators.splice(game.spectators.indexOf(person), 1); - spectatorsUpdated = true; - } - logger.debug('game ' + game.accessCode + ': transferring mod powers to ' + person.name); - if (game.moderator === person) { - person.userType = globals.USER_TYPES.MODERATOR; - const socket = this.namespace.sockets.get(socketId); - if (socket) { - this.namespace.to(socketId).emit(globals.EVENTS.SYNC_GAME_STATE); // they are guaranteed to be connected to this instance. - } - } else { - const oldModerator = game.moderator; - if (game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - game.moderator.userType = globals.USER_TYPES.PLAYER; - } else if (game.moderator.gameRole) { // the current moderator was at one point a dealt-in player. - game.moderator.userType = globals.USER_TYPES.KILLED_PLAYER; // restore their state from before being made mod. - } else if (game.moderator.userType === globals.USER_TYPES.MODERATOR) { - game.moderator.userType = globals.USER_TYPES.SPECTATOR; - game.spectators.push(game.moderator); - spectatorsUpdated = true; - } - person.userType = globals.USER_TYPES.MODERATOR; - game.moderator = person; - if (spectatorsUpdated === true) { - namespace.in(game.accessCode).emit( - globals.EVENTS.UPDATE_SPECTATORS, - game.spectators.map((spectator) => GameStateCurator.mapPerson(spectator)) - ); - } - await notifyPlayerInvolvedInModTransfer(game, this.namespace, person); - await notifyPlayerInvolvedInModTransfer(game, this.namespace, oldModerator); - } - } - }; - - killPlayer = async (socketId, game, person, namespace, logger) => { - if (person && !person.out) { - logger.debug('game ' + game.accessCode + ': killing player ' + person.name); - if (person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) { - person.userType = globals.USER_TYPES.KILLED_PLAYER; - } - person.out = true; - const socket = namespace.sockets.get(socketId); - if (socket && game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - socket.to(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, person.id); - } else { - 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) { - await this.socketManager.handleAndSyncEvent( - globals.EVENT_IDS.TRANSFER_MODERATOR, - game, - socket, - { personId: person.id }, - null - ); - } - } - }; - joinGame = async (game, name, cookie, joinAsSpectator) => { const matchingPerson = this.findPersonByField(game, 'cookie', cookie); if (matchingPerson) { @@ -262,14 +159,16 @@ class GameManager { if (isNameTaken(game, name)) { return Promise.reject({ status: 400, reason: 'This name is taken.' }); } - if (joinAsSpectator && game.spectators.length === globals.MAX_SPECTATORS) { + if (joinAsSpectator + && game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS + ) { return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); } else if (joinAsSpectator) { return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame); } - const unassignedPerson = game.moderator.assigned === false - ? game.moderator - : game.people.find((person) => person.assigned === false); + const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false + ? this.findPersonByField(game, 'id', game.currentModeratorId) + : game.people.find((person) => person.assigned === false && person.userType === globals.USER_TYPES.PLAYER); if (unassignedPerson) { this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name); unassignedPerson.assigned = true; @@ -287,7 +186,7 @@ class GameManager { ); return Promise.resolve(unassignedPerson.cookie); } else { - if (game.spectators.length === globals.MAX_SPECTATORS) { + if (game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS) { return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' }); } return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame); @@ -296,15 +195,15 @@ class GameManager { restartGame = async (game, namespace) => { // kill any outstanding timer threads - const subProcess = this.activeGameRunner.timerThreads[game.accessCode]; - if (subProcess) { - if (!subProcess.killed) { - this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); - this.activeGameRunner.timerThreads[game.accessCode].kill(); - } - this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); - delete this.activeGameRunner.timerThreads[game.accessCode]; - } + // const subProcess = this.activeGameRunner.timerThreads[game.accessCode]; + // if (subProcess) { + // if (!subProcess.killed) { + // this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); + // this.activeGameRunner.timerThreads[game.accessCode].kill(); + // } + // this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); + // delete this.activeGameRunner.timerThreads[game.accessCode]; + // } // re-shuffle the deck const cards = []; @@ -318,23 +217,21 @@ class GameManager { // make sure no players are marked as out or revealed, and give them new cards. for (let i = 0; i < game.people.length; i ++) { - if (game.people[i].out) { - game.people[i].out = false; - } if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) { game.people[i].userType = globals.USER_TYPES.PLAYER; + game.people[i].out = false; } game.people[i].revealed = false; - game.people[i].gameRole = cards[i].role; - game.people[i].gameRoleDescription = cards[i].description; - game.people[i].alignment = cards[i].team; - } - - /* If there is currently a dedicated mod, and that person was once a player (i.e. they have a game role), make - them a temporary mod for the restarted game. - */ - if (game.moderator.gameRole && game.moderator.userType === globals.USER_TYPES.MODERATOR) { - game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR; + game.people[i].killed = false; + if (game.people[i].gameRole) { + game.people[i].gameRole = cards[i].role; + game.people[i].gameRoleDescription = cards[i].description; + game.people[i].alignment = cards[i].team; + if (game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) { + game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR; + game.people[i].out = false; + } + } } // start the new game @@ -345,36 +242,13 @@ class GameManager { } await this.refreshGame(game); + await this.publisher?.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + game.accessCode + ';' + globals.EVENT_IDS.RESTART_GAME + ';' + JSON.stringify({}) + ';' + this.instanceId + ); namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME); }; - handleRequestForGameState = async (game, namespace, logger, gameRunner, accessCode, personCookie, ackFn, socketId) => { - const matchingPerson = this.findPersonByField(game, 'cookie', personCookie); - if (matchingPerson) { - if (matchingPerson.socketId === socketId) { - logger.debug('matching person found with an established connection to the room: ' + matchingPerson.name); - if (ackFn) { - ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); - } - } else { - logger.debug('matching person found with a new connection to the room: ' + matchingPerson.name); - this.namespace.sockets.get(socketId).join(accessCode); - matchingPerson.socketId = socketId; - await this.publisher.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SOCKET + ';' + JSON.stringify({ personId: matchingPerson.id, socketId: socketId }) + ';' + this.instanceId - ); - if (ackFn) { - ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); - } - } - } else { - if (ackFn) { - rejectClientRequestForGameState(ackFn); - } - } - }; - /* -- To shuffle an array a of n elements (indices 0..n-1): for i from n−1 downto 1 do @@ -392,39 +266,12 @@ class GameManager { return array; }; - // pruneStaleGames = async () => { - // this.activeGameRunner.activeGames.forEach((key, value) => { - // if (value.createTime) { - // const createDate = new Date(value.createTime); - // if (createDate.setHours(createDate.getHours() + globals.STALE_GAME_HOURS) < Date.now()) { - // this.logger.info('PRUNING STALE GAME ' + key); - // this.activeGameRunner.activeGames.delete(key); - // if (this.activeGameRunner.timerThreads[key]) { - // this.logger.info('KILLING STALE TIMER PROCESS FOR ' + key); - // this.activeGameRunner.timerThreads[key].kill(); - // delete this.activeGameRunner.timerThreads[key]; - // } - // } - // } - // }); - // }; - isGameFull = (game) => { - return game.moderator.assigned === true && !game.people.find((person) => person.assigned === false); + return !game.people.find((person) => person.userType === globals.USER_TYPES.PLAYER && person.assigned === false); } findPersonByField = (game, fieldName, value) => { - let person; - if (value === game.moderator[fieldName]) { - person = game.moderator; - } - if (!person) { - person = game.people.find((person) => person[fieldName] === value); - } - if (!person) { - person = game.spectators.find((spectator) => spectator[fieldName] === value); - } - return person; + return game.people.find(person => person[fieldName] === value); } } @@ -439,30 +286,23 @@ function initializeModerator (name, hasDedicatedModerator) { return new Person(createRandomId(), createRandomId(), name, userType); } -function initializePeopleForGame (uniqueCards, moderator, shuffle) { +function initializePeopleForGame (uniqueRoles, moderator, shuffle) { const people = []; + const cards = []; - let numberOfRoles = 0; - for (const card of uniqueCards) { - for (let i = 0; i < card.quantity; i ++) { - cards.push(card); - numberOfRoles ++; + for (const role of uniqueRoles) { + for (let i = 0; i < role.quantity; i ++) { + cards.push(role); } } shuffle(cards); // this shuffles in-place. let j = 0; - if (moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { // temporary moderators should be dealt in. - moderator.gameRole = cards[j].role; - moderator.customRole = cards[j].custom; - moderator.gameRoleDescription = cards[j].description; - moderator.alignment = cards[j].team; - people.push(moderator); - j ++; - } - - while (j < numberOfRoles) { + const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ? cards.length - 1 + : cards.length; + while (j < number) { const person = new Person( createRandomId(), createRandomId(), @@ -478,30 +318,29 @@ function initializePeopleForGame (uniqueCards, moderator, shuffle) { j ++; } + if (moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + moderator.gameRole = cards[cards.length - 1].role; + moderator.customRole = cards[cards.length - 1].custom; + moderator.gameRoleDescription = cards[cards.length - 1].description; + moderator.alignment = cards[cards.length - 1].team; + } + + people.push(moderator); + return people; } function createRandomId () { let id = ''; - for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i ++) { - id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)]; + for (let i = 0; i < globals.INSTANCE_ID_LENGTH; i ++) { + id += globals.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * globals.INSTANCE_ID_CHAR_POOL.length)]; } return id; } -function rejectClientRequestForGameState (acknowledgementFunction) { - return acknowledgementFunction(null); -} - -function findPlayerBySocketId (people, socketId) { - return people.find((person) => person.socketId === socketId && person.userType === globals.USER_TYPES.PLAYER); -} - function isNameTaken (game, name) { const processedName = name.toLowerCase().trim(); - return (game.people.find((person) => person.name.toLowerCase().trim() === processedName)) - || (game.moderator.name.toLowerCase().trim() === processedName) - || (game.spectators.find((spectator) => spectator.name.toLowerCase().trim() === processedName)); + return game.people.find((person) => person.name.toLowerCase().trim() === processedName); } function getGameSize (cards) { @@ -513,12 +352,6 @@ function getGameSize (cards) { return quantity; } -async function notifyPlayerInvolvedInModTransfer(game, namespace, person) { - if (namespace.sockets.get(person.socketId)) { - namespace.to(person.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); - } -} - async function addSpectator (game, name, logger, namespace, publisher, instanceId, refreshGame) { const spectator = new Person( createRandomId(), @@ -527,15 +360,15 @@ async function addSpectator (game, name, logger, namespace, publisher, instanceI globals.USER_TYPES.SPECTATOR ); logger.trace('new spectator: ' + spectator.name); - game.spectators.push(spectator); + game.people.push(spectator); await refreshGame(game); namespace.in(game.accessCode).emit( - globals.EVENTS.UPDATE_SPECTATORS, - game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) + globals.EVENT_IDS.ADD_SPECTATOR, + GameStateCurator.mapPerson(spectator) ); await publisher.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + globals.EVENT_IDS.UPDATE_SPECTATORS + ';' + JSON.stringify(game.spectators) + ';' + instanceId + game.accessCode + ';' + globals.EVENT_IDS.ADD_SPECTATOR + ';' + JSON.stringify(GameStateCurator.mapPerson(spectator)) + ';' + instanceId ); return Promise.resolve(spectator.cookie); } diff --git a/server/modules/singletons/SocketManager.js b/server/modules/singletons/SocketManager.js index 449238c..0584f54 100644 --- a/server/modules/singletons/SocketManager.js +++ b/server/modules/singletons/SocketManager.js @@ -2,8 +2,7 @@ const globals = require('../../config/globals'); const EVENT_IDS = globals.EVENT_IDS; const { RateLimiterMemory } = require('rate-limiter-flexible'); const redis = require('redis'); -const GameStateCurator = require("../GameStateCurator"); -const Events = require("../Events"); +const Events = require('../Events'); class SocketManager { constructor (logger, instanceId) { @@ -73,58 +72,33 @@ class SocketManager { }); }; - handleAndSyncEvent = async (eventId, game, socket, args, ackFn) => { - await this.handleEventById(eventId, game, socket?.id, game.accessCode, args, ackFn, false); + handleAndSyncEvent = async (eventId, game, socket, socketArgs, ackFn) => { + await this.handleEventById(eventId, game, socket?.id, game.accessCode, socketArgs, ackFn, false); /* This server should publish events initiated by a connected socket to Redis for consumption by other instances. */ if (globals.SYNCABLE_EVENTS().includes(eventId)) { await this.gameManager.refreshGame(game); - this.publisher?.publish( + await this.publisher?.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - game.accessCode + ';' + eventId + ';' + JSON.stringify(args) + ';' + this.instanceId + game.accessCode + ';' + eventId + ';' + JSON.stringify(socketArgs) + ';' + this.instanceId ); } } - handleEventById = async (eventId, game, socketId, accessCode, args, ackFn, syncOnly) => { - this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(args)); + handleEventById = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => { + this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(socketArgs)); const event = Events.find((event) => event.id === eventId); + const additionalVars = { + gameManager: this.gameManager, + socketId: socketId, + ackFn: ackFn + }; if (event) { if (!syncOnly) { - event.stateChange(game, args, this.gameManager); + event.stateChange(game, socketArgs, additionalVars); } - event.communicate(game, args, this.gameManager); + event.communicate(game, socketArgs, additionalVars); } switch (eventId) { - case EVENT_IDS.FETCH_GAME_STATE: - await this.gameManager.handleRequestForGameState( - game, - this.namespace, - this.logger, - this.activeGameRunner, - accessCode, - args.personId, - ackFn, - socketId - ); - break; - case EVENT_IDS.UPDATE_SOCKET: - const matchingPerson = this.gameManager.findPersonByField(game, 'id', args.personId); - if (matchingPerson) { - matchingPerson.socketId = args.socketId; - } - break; - case EVENT_IDS.SYNC_GAME_STATE: - const personToSync = this.gameManager.findPersonByField(game, 'id', args.personId); - if (personToSync) { - this.gameManager.namespace.to(personToSync.socketId).emit(globals.EVENTS.SYNC_GAME_STATE); - } - break; - case EVENT_IDS.START_GAME: - await this.gameManager.startGame(game, this.gameManager.namespace); - if (ackFn) { - ackFn(); - } - break; case EVENT_IDS.PAUSE_TIMER: await this.gameManager.pauseTimer(game, this.logger); break; @@ -134,27 +108,6 @@ class SocketManager { case EVENT_IDS.GET_TIME_REMAINING: await this.gameManager.getTimeRemaining(game, socketId); break; - case EVENT_IDS.KILL_PLAYER: - await this.gameManager.killPlayer(socketId, game, game.people.find((person) => person.id === args.personId), this.gameManager.namespace, this.logger); - break; - case EVENT_IDS.REVEAL_PLAYER: - await this.gameManager.revealPlayer(game, args.personId); - break; - case EVENT_IDS.TRANSFER_MODERATOR: - await this.gameManager.transferModeratorPowers( - socketId, - game, - this.gameManager?.findPersonByField(game, 'id', args.personId), - this.gameManager.namespace, - this.logger - ); - break; - case EVENT_IDS.END_GAME: - await this.gameManager.endGame(game); - if (ackFn) { - ackFn(); - } - break; default: break; }