From 0b7dd9f4d7c4b08012a38156c19672acd15cf9f8 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Sun, 13 Aug 2023 17:51:41 -0400 Subject: [PATCH] various refactors --- client/src/config/globals.js | 193 +++++---- client/src/model/Game.js | 9 +- .../modules/game_creation/DeckStateManager.js | 18 +- .../game_creation/GameCreationStepManager.js | 83 ++-- client/src/modules/game_creation/RoleBox.js | 31 +- client/src/modules/game_state/StateBucket.js | 11 +- client/src/modules/game_state/states/Ended.js | 12 +- .../modules/game_state/states/InProgress.js | 113 +++--- client/src/modules/game_state/states/Lobby.js | 68 ++-- .../states/shared/SharedStateUtil.js | 372 +++++------------- .../src/modules/page_handlers/gameHandler.js | 253 ++++++++++-- client/src/modules/timer/GameTimerManager.js | 86 ++-- client/src/modules/timer/Timer.js | 29 +- client/src/modules/utility/UserUtility.js | 16 +- client/src/modules/utility/XHRUtility.js | 39 -- client/src/scripts/game.js | 12 +- client/src/scripts/home.js | 44 +-- client/src/scripts/join.js | 39 +- client/src/view_templates/GameTemplate.js | 2 +- server/api/AdminAPI.js | 6 +- server/api/{GamesAPI.js => RoomsAPI.js} | 28 +- server/config/globals.js | 298 +++++++------- server/model/GameCreationRequest.js | 10 +- server/model/Person.js | 4 +- server/modules/Events.js | 65 ++- server/modules/GameProcess.js | 22 +- server/modules/GameStateCurator.js | 18 +- server/modules/Logger.js | 20 +- server/modules/ServerBootstrapper.js | 9 +- server/modules/singletons/EventManager.js | 20 +- server/modules/singletons/GameManager.js | 94 +++-- server/modules/singletons/TimerManager.js | 6 +- spec/e2e/game_spec.js | 222 ++++------- spec/unit/server/modules/Events_Spec.js | 9 +- 34 files changed, 1097 insertions(+), 1164 deletions(-) delete mode 100644 client/src/modules/utility/XHRUtility.js rename server/api/{GamesAPI.js => RoomsAPI.js} (81%) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 81066ba..f428f49 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -1,4 +1,4 @@ -export const globals = { +export const PRIMITIVES = { CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', USER_SIGNATURE_LENGTH: 75, CLOCK_TICK_INTERVAL_MILLIS: 50, @@ -6,100 +6,99 @@ export const globals = { MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, TOAST_DURATION_DEFAULT: 6, ACCESS_CODE_LENGTH: 4, - PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id', - COMMANDS: { - 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', - END_TIMER: 'endTimer' - }, - STATUS: { - LOBBY: 'lobby', - IN_PROGRESS: 'in progress', - ENDED: 'ended' - }, - ALIGNMENT: { - GOOD: 'good', - EVIL: 'evil' - }, - MESSAGES: { - ENTER_NAME: 'Client must enter name.' - }, - 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', - PLAYER_JOINED: 'playerJoined', - SYNC_GAME_STATE: 'syncGameState', - START_TIMER: 'startTimer', - PLAYER_LEFT: 'playerLeft', - ADD_SPECTATOR: 'addSpectator', - UPDATE_SPECTATORS: 'updateSpectators', - RESTART_GAME: 'restartGame', - ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', - KICK_PERSON: 'kickPerson', - UPDATE_GAME_ROLES: 'updateGameRoles', - LEAVE_ROOM: 'leaveRoom' - }, - TIMER_EVENTS: function () { - return [ - this.EVENT_IDS.PAUSE_TIMER, - this.EVENT_IDS.RESUME_TIMER, - this.EVENT_IDS.GET_TIME_REMAINING, - this.EVENT_IDS.END_TIMER - ]; - }, - LOBBY_EVENTS: function () { - return [ - this.EVENT_IDS.PLAYER_JOINED, - this.EVENT_IDS.ADD_SPECTATOR, - this.EVENT_IDS.KICK_PERSON, - this.EVENT_IDS.UPDATE_GAME_ROLES, - this.EVENT_IDS.LEAVE_ROOM - ]; - }, - IN_PROGRESS_EVENTS: function () { - return [ - this.EVENT_IDS.KILL_PLAYER, - this.EVENT_IDS.REVEAL_PLAYER, - this.EVENT_IDS.ADD_SPECTATOR - ]; - }, - USER_TYPES: { - MODERATOR: 'moderator', - PLAYER: 'player', - TEMPORARY_MODERATOR: 'temp mod', - KILLED_PLAYER: 'killed', - SPECTATOR: 'spectator', - BOT: 'bot' - }, - ENVIRONMENT: { - LOCAL: 'local', - PRODUCTION: 'production' - }, - USER_TYPE_ICONS: { - player: ' \uD83C\uDFAE', - moderator: ' \uD83D\uDC51', - 'temp mod': ' \uD83C\uDFAE\uD83D\uDC51', - spectator: ' \uD83D\uDC7B', - killed: ' \uD83D\uDC80', - bot: ' \uD83E\uDD16' - } + PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id' +}; + +export const STATUS = { + LOBBY: 'lobby', + IN_PROGRESS: 'in progress', + ENDED: 'ended' +}; + +export const ALIGNMENT = { + GOOD: 'good', + EVIL: 'evil' +}; + +export const MESSAGES = { + ENTER_NAME: 'Client must enter name.' +}; + +export const SOCKET_EVENTS = { + IN_GAME_MESSAGE: 'inGameMessage' +}; + +export const USER_TYPES = { + MODERATOR: 'moderator', + PLAYER: 'player', + TEMPORARY_MODERATOR: 'temp mod', + KILLED_PLAYER: 'killed', + SPECTATOR: 'spectator', + BOT: 'bot' +}; + +export const ENVIRONMENTS = { + LOCAL: 'local', + PRODUCTION: 'production' +}; + +export const USER_TYPE_ICONS = { + player: ' \uD83C\uDFAE', + moderator: ' \uD83D\uDC51', + 'temp mod': ' \uD83C\uDFAE\uD83D\uDC51', + spectator: ' \uD83D\uDC7B', + killed: ' \uD83D\uDC80', + bot: ' \uD83E\uDD16' +}; + +export const EVENT_IDS = { + FETCH_GAME_STATE: 'fetchGameState', + START_GAME: 'startGame', + PAUSE_TIMER: 'pauseTimer', + RESUME_TIMER: 'resumeTimer', + END_TIMER: 'endTimer', + GET_TIME_REMAINING: 'getTimeRemaining', + KILL_PLAYER: 'killPlayer', + REVEAL_PLAYER: 'revealPlayer', + TRANSFER_MODERATOR: 'transferModerator', + CHANGE_NAME: 'changeName', + END_GAME: 'endGame', + PLAYER_JOINED: 'playerJoined', + SYNC_GAME_STATE: 'syncGameState', + START_TIMER: 'startTimer', + PLAYER_LEFT: 'playerLeft', + ADD_SPECTATOR: 'addSpectator', + UPDATE_SPECTATORS: 'updateSpectators', + RESTART_GAME: 'restartGame', + ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', + KICK_PERSON: 'kickPerson', + UPDATE_GAME_ROLES: 'updateGameRoles', + LEAVE_ROOM: 'leaveRoom' +}; + +export const TIMER_EVENTS = function () { + return [ + EVENT_IDS.PAUSE_TIMER, + EVENT_IDS.RESUME_TIMER, + EVENT_IDS.GET_TIME_REMAINING, + EVENT_IDS.END_TIMER + ]; +}; + +export const LOBBY_EVENTS = function () { + return [ + EVENT_IDS.PLAYER_JOINED, + EVENT_IDS.ADD_SPECTATOR, + EVENT_IDS.KICK_PERSON, + EVENT_IDS.UPDATE_GAME_ROLES, + EVENT_IDS.LEAVE_ROOM + ]; +}; + +export const IN_PROGRESS_EVENTS = function () { + return [ + EVENT_IDS.KILL_PLAYER, + EVENT_IDS.REVEAL_PLAYER, + EVENT_IDS.ADD_SPECTATOR + ]; }; diff --git a/client/src/model/Game.js b/client/src/model/Game.js index 3ca5a3d..b639949 100644 --- a/client/src/model/Game.js +++ b/client/src/model/Game.js @@ -1,5 +1,12 @@ export class Game { - constructor (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams = null, isTestGame = false) { + constructor ( + deck, + hasTimer, + hasDedicatedModerator, + moderatorName, + timerParams = null, + isTestGame = false + ) { this.deck = deck; this.hasTimer = hasTimer; this.timerParams = timerParams; diff --git a/client/src/modules/game_creation/DeckStateManager.js b/client/src/modules/game_creation/DeckStateManager.js index 0cdc68e..f85442b 100644 --- a/client/src/modules/game_creation/DeckStateManager.js +++ b/client/src/modules/game_creation/DeckStateManager.js @@ -1,4 +1,4 @@ -import { globals } from '../../config/globals.js'; +import { ALIGNMENT } from '../../config/globals.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; import { toast } from '../front_end_components/Toast.js'; import { ModalManager } from '../front_end_components/ModalManager.js'; @@ -147,7 +147,7 @@ export class DeckStateManager { } const sortedDeck = this.deck.sort((a, b) => { if (a.team !== b.team) { - return a.team === globals.ALIGNMENT.GOOD ? -1 : 1; + return a.team === ALIGNMENT.GOOD ? -1 : 1; } return a.role.localeCompare(b.role); }); @@ -187,10 +187,10 @@ export class DeckStateManager { roleEl.dataset.roleId = sortedDeck[i].id; roleEl.classList.add('added-role'); roleEl.innerHTML = HTMLFragments.DECK_SELECT_ROLE_ADDED_TO_DECK; - if (sortedDeck[i].team === globals.ALIGNMENT.GOOD) { - roleEl.classList.add(globals.ALIGNMENT.GOOD); + if (sortedDeck[i].team === ALIGNMENT.GOOD) { + roleEl.classList.add(ALIGNMENT.GOOD); } else { - roleEl.classList.add(globals.ALIGNMENT.EVIL); + roleEl.classList.add(ALIGNMENT.EVIL); } populateRoleElementInfo(roleEl, sortedDeck, i); document.getElementById('deck-list').appendChild(roleEl); @@ -219,10 +219,10 @@ export class DeckStateManager { if (e.type === 'click' || e.code === 'Enter') { const alignmentEl = document.getElementById('custom-role-info-modal-alignment'); const nameEl = document.getElementById('custom-role-info-modal-name'); - alignmentEl.classList.remove(globals.ALIGNMENT.GOOD); - alignmentEl.classList.remove(globals.ALIGNMENT.EVIL); - nameEl.classList.remove(globals.ALIGNMENT.GOOD); - nameEl.classList.remove(globals.ALIGNMENT.EVIL); + alignmentEl.classList.remove(ALIGNMENT.GOOD); + alignmentEl.classList.remove(ALIGNMENT.EVIL); + nameEl.classList.remove(ALIGNMENT.GOOD); + nameEl.classList.remove(ALIGNMENT.EVIL); e.preventDefault(); nameEl.innerText = sortedDeck[i].role; nameEl.classList.add(sortedDeck[i].team); diff --git a/client/src/modules/game_creation/GameCreationStepManager.js b/client/src/modules/game_creation/GameCreationStepManager.js index fc7d976..c82f023 100644 --- a/client/src/modules/game_creation/GameCreationStepManager.js +++ b/client/src/modules/game_creation/GameCreationStepManager.js @@ -1,8 +1,7 @@ import { Game } from '../../model/Game.js'; import { cancelCurrentToast, toast } from '../front_end_components/Toast.js'; import { ModalManager } from '../front_end_components/ModalManager.js'; -import { XHRUtility } from '../utility/XHRUtility.js'; -import { globals } from '../../config/globals.js'; +import { ALIGNMENT } from '../../config/globals.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; import { UserUtility } from '../utility/UserUtility.js'; import { RoleBox } from './RoleBox.js'; @@ -118,50 +117,42 @@ export class GameCreationStepManager { button.classList.remove('submitted'); button.addEventListener('click', this.steps['5'].forwardHandler); }; - XHRUtility.xhr( + fetch( '/api/games/create', - 'POST', - null, - JSON.stringify( - new Game( - this.currentGame.deck.filter((card) => card.quantity > 0), - this.currentGame.hasTimer, - this.currentGame.hasDedicatedModerator, - this.currentGame.moderatorName, - this.currentGame.timerParams, - this.currentGame.isTestGame + { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify( + new Game( + this.currentGame.deck.filter((card) => card.quantity > 0), + this.currentGame.hasTimer, + this.currentGame.hasDedicatedModerator, + this.currentGame.moderatorName, + this.currentGame.timerParams, + this.currentGame.isTestGame + ) ) - ) - ) - .then((res) => { - if (res.status === 201) { - try { - const json = JSON.parse(res.content); - UserUtility.setAnonymousUserId(json.cookie, json.environment); - window.location.replace( - window.location.protocol + '//' + window.location.host + - '/game/' + json.accessCode - ); - } catch (e) { - restoreButton(); - toast( - 'There was a problem creating the game. Please contact the developers.', - 'error', - true, - true - ); - } - } }).catch((e) => { - restoreButton(); - if (e.status === 429) { - toast('You\'ve sent this request too many times.', 'error', true, true, 'medium'); - } else if (e.status === 413) { - toast('Your request is too large.', 'error', true, true); - } else { - toast(e.content, 'error', true, true, 'medium'); - } + restoreButton(); + if (e.status === 429) { + toast('You\'ve sent this request too many times.', 'error', true, true, 'medium'); + } else if (e.status === 413) { + toast('Your request is too large.', 'error', true, true); + } else { + toast(e.content, 'error', true, true, 'medium'); + } + }).then(res => { + res.json().then(json => { + UserUtility.setAnonymousUserId(json.cookie, json.environment); + window.location.replace( + window.location.protocol + '//' + window.location.host + + '/game/' + json.accessCode + ); }); + }); } } }; @@ -442,10 +433,10 @@ function renderReviewAndCreateStep (containerId, stepNumber, game, deckManager) for (const card of game.deck) { const roleEl = document.createElement('div'); roleEl.innerText = card.quantity + 'x ' + card.role; - if (card.team === globals.ALIGNMENT.GOOD) { - roleEl.classList.add(globals.ALIGNMENT.GOOD); + if (card.team === ALIGNMENT.GOOD) { + roleEl.classList.add(ALIGNMENT.GOOD); } else { - roleEl.classList.add(globals.ALIGNMENT.EVIL); + roleEl.classList.add(ALIGNMENT.EVIL); } div.querySelector('#roles-option').appendChild(roleEl); } @@ -552,7 +543,7 @@ function initializeRemainingEventListeners (deckManager, roleBox) { roleBox.createMode = true; roleBox.currentlyEditingRoleName = null; document.getElementById('role-name').value = ''; - document.getElementById('role-alignment').value = globals.ALIGNMENT.GOOD; + document.getElementById('role-alignment').value = ALIGNMENT.GOOD; document.getElementById('role-description').value = ''; ModalManager.displayModal( 'role-modal', diff --git a/client/src/modules/game_creation/RoleBox.js b/client/src/modules/game_creation/RoleBox.js index 9cd1d58..1bb801c 100644 --- a/client/src/modules/game_creation/RoleBox.js +++ b/client/src/modules/game_creation/RoleBox.js @@ -1,5 +1,5 @@ import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; -import { globals } from '../../config/globals.js'; +import { ALIGNMENT, PRIMITIVES } from '../../config/globals.js'; import { defaultRoles } from '../../config/defaultRoles.js'; import { toast } from '../front_end_components/Toast.js'; import { ModalManager } from '../front_end_components/ModalManager.js'; @@ -8,7 +8,6 @@ import { Confirmation } from '../front_end_components/Confirmation.js'; export class RoleBox { constructor (container, deckManager) { this.createMode = false; - this.currentlyEditingRoleName = null; this.category = 'default'; this.deckManager = deckManager; this.defaultRoles = []; @@ -32,7 +31,7 @@ export class RoleBox { loadDefaultRoles = () => { this.defaultRoles = defaultRoles.sort((a, b) => { if (a.team !== b.team) { - return a.team === globals.ALIGNMENT.GOOD ? -1 : 1; + return a.team === ALIGNMENT.GOOD ? -1 : 1; } return a.role.localeCompare(b.role); }).map((role) => { @@ -174,9 +173,9 @@ export class RoleBox { defaultRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE_DEFAULT; defaultRole.classList.add('default-role'); defaultRole.dataset.roleId = this.defaultRoles[i].id; - const alignmentClass = this.defaultRoles[i].team === globals.ALIGNMENT.GOOD - ? globals.ALIGNMENT.GOOD - : globals.ALIGNMENT.EVIL; + const alignmentClass = this.defaultRoles[i].team === ALIGNMENT.GOOD + ? ALIGNMENT.GOOD + : ALIGNMENT.EVIL; defaultRole.classList.add(alignmentClass); defaultRole.querySelector('.role-name').innerText = this.defaultRoles[i].role; selectEl.appendChild(defaultRole); @@ -190,7 +189,7 @@ export class RoleBox { this.categoryTransition.play(); this.customRoles.sort((a, b) => { if (a.team !== b.team) { - return a.team === globals.ALIGNMENT.GOOD ? -1 : 1; + return a.team === ALIGNMENT.GOOD ? -1 : 1; } return a.role.localeCompare(b.role); }); @@ -200,7 +199,7 @@ export class RoleBox { customRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE; customRole.classList.add('custom-role'); customRole.dataset.roleId = this.customRoles[i].id; - const alignmentClass = this.customRoles[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL; + const alignmentClass = this.customRoles[i].team === ALIGNMENT.GOOD ? ALIGNMENT.GOOD : ALIGNMENT.EVIL; customRole.classList.add(alignmentClass); customRole.querySelector('.role-name').innerText = this.customRoles[i].role; selectEl.appendChild(customRole); @@ -267,10 +266,10 @@ export class RoleBox { if (e.type === 'click' || e.code === 'Enter') { const alignmentEl = document.getElementById('custom-role-info-modal-alignment'); const nameEl = document.getElementById('custom-role-info-modal-name'); - alignmentEl.classList.remove(globals.ALIGNMENT.GOOD); - alignmentEl.classList.remove(globals.ALIGNMENT.EVIL); - nameEl.classList.remove(globals.ALIGNMENT.GOOD); - nameEl.classList.remove(globals.ALIGNMENT.EVIL); + alignmentEl.classList.remove(ALIGNMENT.GOOD); + alignmentEl.classList.remove(ALIGNMENT.EVIL); + nameEl.classList.remove(ALIGNMENT.GOOD); + nameEl.classList.remove(ALIGNMENT.EVIL); e.preventDefault(); let role; if (isCustom) { @@ -351,7 +350,7 @@ export class RoleBox { function createRandomId () { let id = ''; for (let i = 0; i < 50; i ++) { - id += globals.CHAR_POOL[Math.floor(Math.random() * globals.CHAR_POOL.length)]; + id += PRIMITIVES.CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.CHAR_POOL.length)]; } return id; } @@ -365,9 +364,9 @@ function validateCustomRoleCookie (cookie) { if (Array.isArray(cookieJSON)) { for (const entry of cookieJSON) { if (entry !== null && typeof entry === 'object') { - if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH - || typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL) - || typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH + if (typeof entry.role !== 'string' || entry.role.length > PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH + || typeof entry.team !== 'string' || (entry.team !== ALIGNMENT.GOOD && entry.team !== ALIGNMENT.EVIL) + || typeof entry.description !== 'string' || entry.description.length > PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH ) { return false; } diff --git a/client/src/modules/game_state/StateBucket.js b/client/src/modules/game_state/StateBucket.js index 528ecdd..3f7adb8 100644 --- a/client/src/modules/game_state/StateBucket.js +++ b/client/src/modules/game_state/StateBucket.js @@ -1,10 +1 @@ -/* It started getting confusing where I am reading/writing to the game state, and thus the state started to get inconsistent. - Creating a bucket to hold it so I can overwrite the gameState object whilst still preserving a reference to the containing bucket. - Now several components can read a shared game state. - */ -export const stateBucket = { - joinRequestInFlight: true, - accessCode: null, - currentGameState: null, - environment: null -}; +export const stateBucket = {}; diff --git a/client/src/modules/game_state/states/Ended.js b/client/src/modules/game_state/states/Ended.js index 9c5cf8d..2a94d8d 100644 --- a/client/src/modules/game_state/states/Ended.js +++ b/client/src/modules/game_state/states/Ended.js @@ -1,4 +1,4 @@ -import { globals } from '../../../config/globals.js'; +import { USER_TYPES } from '../../../config/globals.js'; import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; import { SharedStateUtil } from './shared/SharedStateUtil.js'; @@ -12,14 +12,14 @@ export class Ended { renderEndOfGame (gameState) { if ( - gameState.client.userType === globals.USER_TYPES.MODERATOR - || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + gameState.client.userType === USER_TYPES.MODERATOR + || gameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) { document.getElementById('end-of-game-buttons').prepend(SharedStateUtil.createReturnToLobbyButton(this.stateBucket)); } SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people - .find((person) => person.userType === globals.USER_TYPES.MODERATOR - || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR)); + .find((person) => person.userType === USER_TYPES.MODERATOR + || person.userType === USER_TYPES.TEMPORARY_MODERATOR)); this.renderPlayersWithRoleInformation(); } @@ -31,7 +31,7 @@ export class Ended { const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; renderGroupOfPlayers( this.stateBucket.currentGameState.people.filter( - p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR) + p => (p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR) || p.killed ), this.stateBucket.currentGameState.accessCode, diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 5172e90..1601f3d 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -1,5 +1,12 @@ import { toast } from '../../front_end_components/Toast.js'; -import { globals } from '../../../config/globals.js'; +import { + STATUS, + EVENT_IDS, + SOCKET_EVENTS, + USER_TYPE_ICONS, + USER_TYPES, + ALIGNMENT +} from '../../../config/globals.js'; import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; import { Confirmation } from '../../front_end_components/Confirmation.js'; import { ModalManager } from '../../front_end_components/ModalManager.js'; @@ -18,25 +25,25 @@ export class InProgress { setUserView (userType) { switch (userType) { - case globals.USER_TYPES.PLAYER: + case USER_TYPES.PLAYER: this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW; this.renderPlayerView(); break; - case globals.USER_TYPES.KILLED_PLAYER: + case USER_TYPES.KILLED_PLAYER: this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW; this.renderPlayerView(true); break; - case globals.USER_TYPES.MODERATOR: + case USER_TYPES.MODERATOR: document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; this.container.innerHTML = HTMLFragments.MODERATOR_GAME_VIEW; this.renderModeratorView(); break; - case globals.USER_TYPES.TEMPORARY_MODERATOR: + case USER_TYPES.TEMPORARY_MODERATOR: document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; this.container.innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW; this.renderTempModView(); break; - case globals.USER_TYPES.SPECTATOR: + case USER_TYPES.SPECTATOR: this.container.innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW; this.renderSpectatorView(); break; @@ -46,12 +53,12 @@ export class InProgress { if (this.stateBucket.currentGameState.timerParams) { this.socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.GET_TIME_REMAINING, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.GET_TIME_REMAINING, this.stateBucket.currentGameState.accessCode ); setTimeout(() => { - if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) { + if (this.socket.hasListeners(EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) { document.getElementById('game-timer').innerText = 'Timer not found.'; document.getElementById('game-timer').classList.add('timer-error'); } @@ -68,7 +75,7 @@ export class InProgress { Confirmation( SharedStateUtil.buildSpectatorList( this.stateBucket.currentGameState.people - .filter(p => p.userType === globals.USER_TYPES.SPECTATOR), + .filter(p => p.userType === USER_TYPES.SPECTATOR), this.stateBucket.currentGameState.client, this.socket, this.stateBucket.currentGameState), @@ -80,7 +87,7 @@ export class InProgress { if (spectatorCount) { SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length, spectatorCount ); spectatorCount?.addEventListener('click', spectatorHandler); @@ -92,13 +99,13 @@ export class InProgress { if (isKilled) { const clientUserType = document.getElementById('client-user-type'); if (clientUserType) { - clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; + clientUserType.innerText = USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; } } renderPlayerRole(this.stateBucket.currentGameState); SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people - .find((person) => person.userType === globals.USER_TYPES.MODERATOR - || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR)); + .find((person) => person.userType === USER_TYPES.MODERATOR + || person.userType === USER_TYPES.TEMPORARY_MODERATOR)); this.renderPlayersWithNoRoleInformationUnlessRevealed(false); } @@ -116,7 +123,7 @@ export class InProgress { : null; this.renderGroupOfPlayers( this.stateBucket.currentGameState.people.filter( - p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR) + p => (p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR) || p.killed ), this.killPlayerHandlers, @@ -174,34 +181,34 @@ export class InProgress { renderSpectatorView () { SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people - .find((person) => person.userType === globals.USER_TYPES.MODERATOR - || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR)); + .find((person) => person.userType === USER_TYPES.MODERATOR + || person.userType === USER_TYPES.TEMPORARY_MODERATOR)); this.renderPlayersWithNoRoleInformationUnlessRevealed(); } setSocketHandlers () { - this.socket.on(globals.EVENT_IDS.KILL_PLAYER, (killedPlayer) => { + this.socket.on(EVENT_IDS.KILL_PLAYER, (killedPlayer) => { this.stateBucket.currentGameState.people = this.stateBucket.currentGameState.people .map(person => person.id === killedPlayer.id ? killedPlayer : person); - if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) { toast(killedPlayer.name + ' killed.', 'success', true, true, 'medium'); - this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); + this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === STATUS.ENDED); } else { if (killedPlayer.id === this.stateBucket.currentGameState.client.id) { const clientUserType = document.getElementById('client-user-type'); if (clientUserType) { - clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; + clientUserType.innerText = USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; } this.updatePlayerCardToKilledState(); toast('You have been killed!', 'warning', true, true, 'medium'); } else { toast(killedPlayer.name + ' was killed!', 'warning', true, true, 'medium'); - if (killedPlayer.userType === globals.USER_TYPES.MODERATOR - && this.stateBucket.currentGameState.client.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (killedPlayer.userType === USER_TYPES.MODERATOR + && this.stateBucket.currentGameState.client.userType !== USER_TYPES.TEMPORARY_MODERATOR) { SharedStateUtil.displayCurrentModerator(killedPlayer); } } - if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) { this.removePlayerListEventListeners(false); } else { this.renderPlayersWithNoRoleInformationUnlessRevealed(false); @@ -209,27 +216,27 @@ export class InProgress { } }); - this.socket.on(globals.EVENT_IDS.REVEAL_PLAYER, (revealData) => { + this.socket.on(EVENT_IDS.REVEAL_PLAYER, (revealData) => { const revealedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === revealData.id); if (revealedPerson) { revealedPerson.revealed = true; revealedPerson.gameRole = revealData.gameRole; revealedPerson.alignment = revealData.alignment; - if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) { if (revealedPerson.id === this.stateBucket.currentGameState.client.id) { toast('You revealed your role.', 'success', true, true, 'medium'); } else { toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium'); } - this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); + this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === STATUS.ENDED); } else { if (revealedPerson.id === this.stateBucket.currentGameState.client.id) { toast('Your role has been revealed!', 'warning', true, true, 'medium'); } else { toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium'); } - if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) { this.renderPlayersWithNoRoleInformationUnlessRevealed(true); } else { this.renderPlayersWithNoRoleInformationUnlessRevealed(false); @@ -238,14 +245,14 @@ export class InProgress { } }); - this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => { + this.socket.on(EVENT_IDS.ADD_SPECTATOR, (spectator) => { this.stateBucket.currentGameState.people.push(spectator); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + this.stateBucket.currentGameState.people.filter(p => p.userType === 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) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) { this.displayAvailableModerators(); } }); @@ -269,13 +276,13 @@ export class InProgress { return a.name >= b.name ? 1 : -1; }); const teamGood = this.stateBucket.currentGameState.people.filter( - (p) => p.alignment === globals.ALIGNMENT.GOOD - && ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR) + (p) => p.alignment === ALIGNMENT.GOOD + && ((p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR) || p.killed) ); - const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL - && ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR) + const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === ALIGNMENT.EVIL + && ((p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR) || p.killed) ); this.renderGroupOfPlayers( @@ -283,7 +290,7 @@ export class InProgress { this.killPlayerHandlers, this.revealRoleHandlers, this.stateBucket.currentGameState.accessCode, - globals.ALIGNMENT.EVIL, + ALIGNMENT.EVIL, this.stateBucket.currentGameState.people.find(person => person.id === this.stateBucket.currentGameState.currentModeratorId).userType, this.socket @@ -293,7 +300,7 @@ export class InProgress { this.killPlayerHandlers, this.revealRoleHandlers, this.stateBucket.currentGameState.accessCode, - globals.ALIGNMENT.GOOD, + ALIGNMENT.GOOD, this.stateBucket.currentGameState.people.find(person => person.id === this.stateBucket.currentGameState.currentModeratorId).userType, this.socket @@ -328,7 +335,7 @@ export class InProgress { const roleElement = playerEl.querySelector('.game-player-role'); // Add role/alignment indicators if necessary - if (moderatorType === globals.USER_TYPES.MODERATOR || player.revealed) { + if (moderatorType === USER_TYPES.MODERATOR || player.revealed) { if (alignment === null) { roleElement.classList.add(player.alignment); } else { @@ -349,10 +356,10 @@ export class InProgress { } else if (!player.out && moderatorType) { killPlayerHandlers[player.id] = () => { Confirmation('Kill \'' + player.name + '\'?', () => { - 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 }); + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) { + socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, 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 }); + socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.KILL_PLAYER, accessCode, { personId: player.id }); } }); }; @@ -368,13 +375,13 @@ export class InProgress { } else if (!player.revealed && moderatorType) { revealRoleHandlers[player.id] = () => { Confirmation('Reveal \'' + player.name + '\'?', () => { - socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.REVEAL_PLAYER, accessCode, { personId: player.id }); + socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.REVEAL_PLAYER, accessCode, { personId: player.id }); }); }; playerEl.querySelector('.reveal-role-button').addEventListener('click', revealRoleHandlers[player.id]); } - const playerListContainerId = moderatorType === globals.USER_TYPES.MODERATOR + const playerListContainerId = moderatorType === USER_TYPES.MODERATOR ? 'player-list-moderator-team-' + alignment : 'game-player-list'; @@ -417,7 +424,7 @@ export class InProgress { function renderPlayerRole (gameState) { const name = document.querySelector('#role-name'); name.innerText = gameState.client.gameRole; - if (gameState.client.alignment === globals.ALIGNMENT.GOOD) { + if (gameState.client.alignment === ALIGNMENT.GOOD) { document.getElementById('game-role').classList.add('game-role-good'); name.classList.add('good'); } else { @@ -498,8 +505,8 @@ function createEndGamePromptComponent (socket, stateBucket) { e.preventDefault(); Confirmation('End the game?', () => { socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.END_GAME, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.END_GAME, stateBucket.currentGameState.accessCode, null, () => { @@ -531,14 +538,14 @@ 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.userType === globals.USER_TYPES.KILLED_PLAYER || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { + if ((member.userType === USER_TYPES.KILLED_PLAYER || member.userType === USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { const container = document.createElement('div'); container.classList.add('potential-moderator'); container.setAttribute('tabindex', '0'); container.dataset.pointer = member.id; container.innerHTML = '
' + - '
' + member.userType + ' ' + globals.USER_TYPE_ICONS[member.userType] + '
'; + '
' + member.userType + ' ' + USER_TYPE_ICONS[member.userType] + '
'; container.querySelector('.potential-mod-name').innerText = member.name; transferModHandlers[member.id] = (e) => { if (e.type === 'click' || e.code === 'Enter') { @@ -549,16 +556,16 @@ function renderPotentialMods (gameState, group, transferModHandlers, socket) { transferPrompt.innerHTML = ''; } socket.timeout(5000).emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.TRANSFER_MODERATOR, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.TRANSFER_MODERATOR, gameState.accessCode, { personId: member.id }, (err) => { if (err) { console.error(err); socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, gameState.accessCode, { personId: gameState.client.cookie }, (gameState) => { diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index e37a548..bcd3ba8 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -1,6 +1,6 @@ import { QRCode } from '../../third_party/qrcode.js'; import { toast } from '../../front_end_components/Toast.js'; -import { globals } from '../../../config/globals.js'; +import { EVENT_IDS, SOCKET_EVENTS, USER_TYPE_ICONS, USER_TYPES } from '../../../config/globals.js'; import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; import { Confirmation } from '../../front_end_components/Confirmation.js'; import { SharedStateUtil } from './shared/SharedStateUtil.js'; @@ -25,15 +25,15 @@ export class Lobby { } Confirmation('Start game and deal roles?', () => { socket.timeout(5000).emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.START_GAME, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.START_GAME, stateBucket.currentGameState.accessCode, null, (err) => { if (err) { socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, stateBucket.currentGameState.accessCode, { personId: stateBucket.currentGameState.client.cookie }, (gameState) => { @@ -52,8 +52,8 @@ export class Lobby { e.preventDefault(); Confirmation('Leave the room?', () => { socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.LEAVE_ROOM, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.LEAVE_ROOM, stateBucket.currentGameState.accessCode, { personId: stateBucket.currentGameState.client.id } ); @@ -86,8 +86,8 @@ export class Lobby { document.querySelector('#role-edit-container-background')?.remove(); document.getElementById('game-content').style.display = 'flex'; this.socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.UPDATE_GAME_ROLES, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.UPDATE_GAME_ROLES, stateBucket.currentGameState.accessCode, { deck: this.gameCreationStepManager.deckManager.deck.filter((card) => card.quantity > 0) }, () => { @@ -130,7 +130,7 @@ export class Lobby { const playerCount = this.container.querySelector('#game-player-count'); playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players'; const inLobbyCount = this.stateBucket.currentGameState.people.filter( - p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR + p => p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR ).length; document.querySelector("label[for='lobby-players']").innerText = 'Participants (' + inLobbyCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; @@ -149,7 +149,7 @@ export class Lobby { if (e.type === 'click' || e.code === 'Enter') { Confirmation( SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people - .filter(p => p.userType === globals.USER_TYPES.SPECTATOR), + .filter(p => p.userType === USER_TYPES.SPECTATOR), this.stateBucket.currentGameState.client, this.socket, this.stateBucket.currentGameState), @@ -162,7 +162,7 @@ export class Lobby { this.container.querySelector('#spectator-count').addEventListener('keyup', spectatorHandler); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length, this.container.querySelector('#spectator-count') ); @@ -180,54 +180,54 @@ export class Lobby { const lobbyPlayersContainer = this.container.querySelector('#lobby-players'); const sorted = this.stateBucket.currentGameState.people.sort( function (a, b) { - if (a.userType === globals.USER_TYPES.MODERATOR || a.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (a.userType === USER_TYPES.MODERATOR || a.userType === USER_TYPES.TEMPORARY_MODERATOR) { return -1; } return 1; } ); - for (const person of sorted.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR)) { + for (const person of sorted.filter(p => p.userType !== USER_TYPES.SPECTATOR)) { lobbyPlayersContainer.appendChild(renderLobbyPerson(person, this.stateBucket.currentGameState, this.socket)); } const playerCount = this.stateBucket.currentGameState.people.filter( - p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR + p => p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR ).length; document.querySelector("label[for='lobby-players']").innerText = 'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; } setSocketHandlers () { - this.socket.on(globals.EVENT_IDS.PLAYER_JOINED, (player, gameIsStartable) => { + this.socket.on(EVENT_IDS.PLAYER_JOINED, (player, gameIsStartable) => { toast(player.name + ' joined!', 'success', true, true, 'short'); this.stateBucket.currentGameState.people.push(player); this.stateBucket.currentGameState.isStartable = gameIsStartable; this.populatePlayers(); if (( - this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) ) { this.displayStartGamePromptForModerators(); } }); - this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => { + this.socket.on(EVENT_IDS.ADD_SPECTATOR, (spectator) => { this.stateBucket.currentGameState.people.push(spectator); SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length, document.getElementById('spectator-count') ); }); - this.socket.on(globals.EVENT_IDS.KICK_PERSON, (kickedId, gameIsStartable) => { + this.socket.on(EVENT_IDS.KICK_PERSON, (kickedId, gameIsStartable) => { if (kickedId === this.stateBucket.currentGameState.client.id) { window.location = '/?message=' + encodeURIComponent('You were kicked by the moderator.'); } else { - this.handlePersonExiting(kickedId, gameIsStartable, globals.EVENT_IDS.KICK_PERSON); + this.handlePersonExiting(kickedId, gameIsStartable, EVENT_IDS.KICK_PERSON); } }); - this.socket.on(globals.EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize, isStartable) => { + this.socket.on(EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize, isStartable) => { this.stateBucket.currentGameState.deck = deck; this.stateBucket.currentGameState.gameSize = gameSize; this.stateBucket.currentGameState.isStartable = isStartable; @@ -235,11 +235,11 @@ export class Lobby { this.setPlayerCount(); }); - this.socket.on(globals.EVENT_IDS.LEAVE_ROOM, (leftId, gameIsStartable) => { + this.socket.on(EVENT_IDS.LEAVE_ROOM, (leftId, gameIsStartable) => { if (leftId === this.stateBucket.currentGameState.client.id) { window.location = '/?message=' + encodeURIComponent('You left the room.'); } else { - this.handlePersonExiting(leftId, gameIsStartable, globals.EVENT_IDS.LEAVE_ROOM); + this.handlePersonExiting(leftId, gameIsStartable, EVENT_IDS.LEAVE_ROOM); } }); } @@ -252,18 +252,18 @@ export class Lobby { } this.stateBucket.currentGameState.isStartable = gameIsStartable; SharedStateUtil.setNumberOfSpectators( - this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length, document.getElementById('spectator-count') ); this.populatePlayers(); if (( - this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) ) { toast( - event === globals.EVENT_IDS.LEAVE_ROOM ? 'A player left.' : 'Player kicked.', - event === globals.EVENT_IDS.LEAVE_ROOM ? 'warning' : 'success', + event === EVENT_IDS.LEAVE_ROOM ? 'A player left.' : 'Player kicked.', + event === EVENT_IDS.LEAVE_ROOM ? 'warning' : 'success', true, true, 'short' @@ -358,17 +358,17 @@ function renderLobbyPerson (person, gameState, socket) { personNameEl.classList.add('lobby-player-name'); const personTypeEl = document.createElement('div'); personNameEl.innerText = person.name; - personTypeEl.innerText = person.userType + globals.USER_TYPE_ICONS[person.userType]; + personTypeEl.innerText = person.userType + USER_TYPE_ICONS[person.userType]; el.classList.add('lobby-player'); - if (person.userType === globals.USER_TYPES.MODERATOR || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (person.userType === USER_TYPES.MODERATOR || person.userType === USER_TYPES.TEMPORARY_MODERATOR) { el.classList.add('moderator'); } el.appendChild(personNameEl); el.appendChild(personTypeEl); - if ((gameState.client.userType === globals.USER_TYPES.MODERATOR || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) - && person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) { + if ((gameState.client.userType === USER_TYPES.MODERATOR || gameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) + && person.userType !== USER_TYPES.MODERATOR && person.userType !== USER_TYPES.TEMPORARY_MODERATOR) { SharedStateUtil.addPlayerOptions(el, person, socket, gameState); el.dataset.pointer = person.id; } diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index 854e245..453dc52 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -1,58 +1,41 @@ -import { XHRUtility } from '../../../utility/XHRUtility.js'; import { UserUtility } from '../../../utility/UserUtility.js'; -import { globals } from '../../../../config/globals.js'; +import { + STATUS, + EVENT_IDS, + ENVIRONMENTS, + SOCKET_EVENTS, + USER_TYPE_ICONS, + USER_TYPES, + ALIGNMENT +} from '../../../../config/globals.js'; import { toast } from '../../../front_end_components/Toast.js'; import { Confirmation } from '../../../front_end_components/Confirmation.js'; -import { Lobby } from '../Lobby.js'; import { stateBucket } from '../../StateBucket.js'; -import { InProgress } from '../InProgress.js'; -import { Ended } from '../Ended.js'; import { HTMLFragments } from '../../../front_end_components/HTMLFragments.js'; import { ModalManager } from '../../../front_end_components/ModalManager.js'; // This constant is meant to house logic that is utilized by more than one game state export const SharedStateUtil = { - gameStateAckFn: (gameState, socket) => { - stateBucket.currentGameState = gameState; - processGameState( - stateBucket.currentGameState, - gameState.client.cookie, - socket, - true, - true - ); - }, - restartHandler: (stateBucket, status = globals.STATUS.IN_PROGRESS) => { - XHRUtility.xhr( + restartHandler: (stateBucket, status = STATUS.IN_PROGRESS) => { + fetch( '/api/games/' + stateBucket.currentGameState.accessCode + '/restart?status=' + status, - 'PATCH', - null, - JSON.stringify({ - playerName: stateBucket.currentGameState.client.name, - accessCode: stateBucket.currentGameState.accessCode, - sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), - localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION) - }) - ) - .then((res) => {}) - .catch((res) => { - toast(res.content, 'error', true, true, 'medium'); - }); - }, - - createRestartButton: (stateBucket) => { - const restartGameButton = document.createElement('button'); - restartGameButton.classList.add('app-button'); - restartGameButton.setAttribute('id', 'restart-game-button'); - restartGameButton.innerText = 'Quick Restart'; - restartGameButton.addEventListener('click', () => { - Confirmation('Restart the game, dealing everyone new roles?', () => { - SharedStateUtil.restartHandler(stateBucket); - }); + { + method: 'PATCH', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + playerName: stateBucket.currentGameState.client.name, + accessCode: stateBucket.currentGameState.accessCode, + sessionCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.LOCAL), + localCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.PRODUCTION) + }) + } + ).catch((res) => { + toast(res.content, 'error', true, true, 'medium'); }); - - return restartGameButton; }, createReturnToLobbyButton: (stateBucket) => { @@ -62,123 +45,18 @@ export const SharedStateUtil = { returnToLobbyButton.innerText = 'Return to Lobby'; returnToLobbyButton.addEventListener('click', () => { Confirmation('Return everyone to the Lobby?', () => { - SharedStateUtil.restartHandler(stateBucket, globals.STATUS.LOBBY); + SharedStateUtil.restartHandler(stateBucket, STATUS.LOBBY); }); }); return returnToLobbyButton; }, - setClientSocketHandlers: (stateBucket, socket) => { - const startGameStateAckFn = (gameState) => { - SharedStateUtil.gameStateAckFn(gameState, socket); - toast('Game started!', 'success'); - }; - - const restartGameStateAckFn = (gameState) => { - SharedStateUtil.gameStateAckFn(gameState, socket); - toast('Everyone has returned to the Lobby!', 'success'); - }; - - const fetchGameStateHandler = (ackFn) => { - socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, - stateBucket.currentGameState.accessCode, - { personId: stateBucket.currentGameState.client.cookie }, - ackFn - ); - }; - - socket.on(globals.EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); }); - - socket.on(globals.EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); }); - - socket.on(globals.EVENT_IDS.SYNC_GAME_STATE, () => { - socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, - stateBucket.currentGameState.accessCode, - { personId: stateBucket.currentGameState.client.cookie }, - function (gameState) { - const oldUserType = stateBucket.currentGameState.client.userType; - stateBucket.currentGameState = gameState; - processGameState( - stateBucket.currentGameState, - gameState.client.cookie, - socket, - true, - gameState.client.userType !== oldUserType - ); - } - ); - }); - - socket.on(globals.COMMANDS.END_GAME, (people) => { - stateBucket.currentGameState.people = people; - stateBucket.currentGameState.status = globals.STATUS.ENDED; - processGameState( - stateBucket.currentGameState, - stateBucket.currentGameState.client.cookie, - socket, - true, - true - ); - }); - }, - - syncWithGame: (socket, cookie, window) => { - const splitUrl = window.location.href.split('/game/'); - const accessCode = splitUrl[1]; - if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { - socket.timeout(5000).emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, - accessCode, - { personId: cookie }, - (err, gameState) => { - if (err) { - SharedStateUtil.retrySync(accessCode, socket, cookie); - } else { - SharedStateUtil.handleGameState(gameState, cookie, socket); - } - } - ); - } else { - window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code'); - } - }, - - retrySync: (accessCode, socket, cookie) => { - socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.FETCH_GAME_STATE, - accessCode, - { personId: cookie }, - (gameState) => { - SharedStateUtil.handleGameState(gameState, cookie, socket); - } - ); - }, - - handleGameState: (gameState, cookie, socket) => { - if (gameState === null) { - window.location = '/not-found?reason=' + encodeURIComponent('game-not-found'); - } else { - stateBucket.currentGameState = gameState; - document.querySelector('.spinner-container')?.remove(); - document.querySelector('.spinner-background')?.remove(); - document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM; - toast('You are connected.', 'success', true, true, 'short'); - processGameState(stateBucket.currentGameState, cookie, socket, true, true); - } - }, - addPlayerOptions: (personEl, person, socket, gameState) => { const optionsButton = document.createElement('img'); const optionsHandler = (e) => { if (e.type === 'click' || e.code === 'Enter') { - document.querySelector('#player-options-modal-title').innerText = person.name + globals.USER_TYPE_ICONS[person.userType]; + document.querySelector('#player-options-modal-title').innerText = person.name + USER_TYPE_ICONS[person.userType]; document.getElementById('player-options-modal-content').innerHTML = ''; const kickOption = document.createElement('button'); kickOption.setAttribute('class', 'player-option'); @@ -187,8 +65,8 @@ export const SharedStateUtil = { ModalManager.dispelModal('player-options-modal', 'player-options-modal-background'); Confirmation('Kick \'' + person.name + '\'?', () => { socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.KICK_PERSON, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.KICK_PERSON, gameState.accessCode, { personId: person.id } ); @@ -216,7 +94,7 @@ export const SharedStateUtil = { buildSpectatorList (people, client, socket, gameState) { const list = document.createElement('div'); - const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR); + const spectators = people.filter(p => p.userType === USER_TYPES.SPECTATOR); if (spectators.length === 0) { list.innerHTML = '
Nobody currently spectating.
'; } else { @@ -224,11 +102,11 @@ export const SharedStateUtil = { const spectatorEl = document.createElement('div'); spectatorEl.classList.add('spectator'); spectatorEl.innerHTML = '
' + - '
' + 'spectator' + globals.USER_TYPE_ICONS.spectator + '
'; + '
' + 'spectator' + USER_TYPE_ICONS.spectator + '
'; spectatorEl.querySelector('.spectator-name').innerText = spectator.name; list.appendChild(spectatorEl); - if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (client.userType === USER_TYPES.MODERATOR || client.userType === USER_TYPES.TEMPORARY_MODERATOR) { this.addPlayerOptions(spectatorEl, spectator, socket, gameState); spectatorEl.dataset.pointer = spectator.id; } @@ -246,126 +124,68 @@ export const SharedStateUtil = { displayCurrentModerator: (moderator) => { document.getElementById('current-moderator-name').innerText = moderator.name; - document.getElementById('current-moderator-type').innerText = moderator.userType + globals.USER_TYPE_ICONS[moderator.userType]; + document.getElementById('current-moderator-type').innerText = moderator.userType + USER_TYPE_ICONS[moderator.userType]; + }, + + returnHumanReadableTime: (milliseconds, tenthsOfSeconds = false) => { + const tenths = Math.floor((milliseconds / 100) % 10); + let seconds = Math.floor((milliseconds / 1000) % 60); + let minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + + hours = hours < 10 ? '0' + hours : hours; + minutes = minutes < 10 ? '0' + minutes : minutes; + seconds = seconds < 10 ? '0' + seconds : seconds; + + return tenthsOfSeconds + ? hours + ':' + minutes + ':' + seconds + '.' + tenths + : hours + ':' + minutes + ':' + seconds; + }, + + activateRoleInfoButton: () => { + document.getElementById('role-info-button').addEventListener('click', (e) => { + const deck = stateBucket.currentGameState.deck; + deck.sort((a, b) => { + return a.team === ALIGNMENT.GOOD ? -1 : 1; + }); + e.preventDefault(); + document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL; + const modalContent = document.getElementById('game-role-info-container'); + for (const card of deck) { + const roleDiv = document.createElement('div'); + const roleNameDiv = document.createElement('div'); + + roleNameDiv.classList.add('role-info-name'); + + const roleName = document.createElement('h5'); + const roleQuantity = document.createElement('h5'); + const roleDescription = document.createElement('p'); + + roleDescription.innerText = card.description; + roleName.innerText = card.role; + roleQuantity.innerText = card.quantity + 'x'; + + if (card.team === ALIGNMENT.GOOD) { + roleName.classList.add(ALIGNMENT.GOOD); + } else { + roleName.classList.add(ALIGNMENT.EVIL); + } + + roleNameDiv.appendChild(roleQuantity); + roleNameDiv.appendChild(roleName); + + roleDiv.appendChild(roleNameDiv); + roleDiv.appendChild(roleDescription); + + modalContent.appendChild(roleDiv); + } + ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button'); + }); + }, + + displayClientInfo: (name, userType) => { + document.getElementById('client-name').innerText = name; + document.getElementById('client-user-type').innerText = userType; + document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[userType]; } }; - -function processGameState ( - currentGameState, - userId, - socket, - refreshPrompt = true, - animateContainer = false -) { - if (animateContainer) { - document.getElementById('game-state-container').animate( - [ - { opacity: '0', transform: 'translateY(10px)' }, - { opacity: '1', transform: 'translateY(0px)' } - ], { - duration: 500, - easing: 'ease-in-out', - fill: 'both' - }); - document.getElementById('client-container').animate([ - { opacity: '0' }, - { opacity: '1' } - ], { - duration: 500, - easing: 'ease-out', - fill: 'both' - }); - } - - displayClientInfo(currentGameState.client.name, currentGameState.client.userType); - - switch (currentGameState.status) { - case globals.STATUS.LOBBY: - const lobby = new Lobby('game-state-container', stateBucket, socket); - if (refreshPrompt) { - lobby.removeStartGameFunctionalityIfPresent(); - } - lobby.populateHeader(); - lobby.populatePlayers(); - globals.LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e)); - lobby.setSocketHandlers(); - if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - if (refreshPrompt) { - lobby.displayStartGamePromptForModerators(); - } - document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL; - } else { - if (refreshPrompt) { - lobby.displayPlayerPrompt(); - } - } - break; - case globals.STATUS.IN_PROGRESS: - if (refreshPrompt) { - document.querySelector('#game-control-prompt')?.remove(); - document.querySelector('#leave-game-prompt')?.remove(); - } - const inProgressGame = new InProgress('game-state-container', stateBucket, socket); - globals.IN_PROGRESS_EVENTS().forEach(e => socket.removeAllListeners(e)); - inProgressGame.setSocketHandlers(); - inProgressGame.setUserView(currentGameState.client.userType); - break; - case globals.STATUS.ENDED: { - const ended = new Ended('game-state-container', stateBucket, socket); - ended.renderEndOfGame(currentGameState); - break; - } - default: - break; - } - - activateRoleInfoButton(); -} - -function activateRoleInfoButton () { - document.getElementById('role-info-button').addEventListener('click', (e) => { - const deck = stateBucket.currentGameState.deck; - deck.sort((a, b) => { - return a.team === globals.ALIGNMENT.GOOD ? -1 : 1; - }); - e.preventDefault(); - document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL; - const modalContent = document.getElementById('game-role-info-container'); - for (const card of deck) { - const roleDiv = document.createElement('div'); - const roleNameDiv = document.createElement('div'); - - roleNameDiv.classList.add('role-info-name'); - - const roleName = document.createElement('h5'); - const roleQuantity = document.createElement('h5'); - const roleDescription = document.createElement('p'); - - roleDescription.innerText = card.description; - roleName.innerText = card.role; - roleQuantity.innerText = card.quantity + 'x'; - - if (card.team === globals.ALIGNMENT.GOOD) { - roleName.classList.add(globals.ALIGNMENT.GOOD); - } else { - roleName.classList.add(globals.ALIGNMENT.EVIL); - } - - roleNameDiv.appendChild(roleQuantity); - roleNameDiv.appendChild(roleName); - - roleDiv.appendChild(roleNameDiv); - roleDiv.appendChild(roleDescription); - - modalContent.appendChild(roleDiv); - } - ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button'); - }); -} - -function displayClientInfo (name, userType) { - document.getElementById('client-name').innerText = name; - document.getElementById('client-user-type').innerText = userType; - document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType]; -} diff --git a/client/src/modules/page_handlers/gameHandler.js b/client/src/modules/page_handlers/gameHandler.js index 1d120aa..829aaef 100644 --- a/client/src/modules/page_handlers/gameHandler.js +++ b/client/src/modules/page_handlers/gameHandler.js @@ -3,41 +3,244 @@ import { stateBucket } from '../game_state/StateBucket.js'; import { UserUtility } from '../utility/UserUtility.js'; import { toast } from '../front_end_components/Toast.js'; import { SharedStateUtil } from '../game_state/states/shared/SharedStateUtil.js'; +import { + EVENT_IDS, + IN_PROGRESS_EVENTS, + LOBBY_EVENTS, + PRIMITIVES, + SOCKET_EVENTS, + STATUS, + USER_TYPES +} from '../../config/globals.js'; +import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; +import { Lobby } from '../game_state/states/Lobby.js'; +import { InProgress } from '../game_state/states/InProgress.js'; +import { Ended } from '../game_state/states/Ended.js'; -export const gameHandler = async (socket, XHRUtility, window, gameDOM) => { +export const gameHandler = (socket, window, gameDOM) => { document.body.innerHTML = gameDOM + document.body.innerHTML; injectNavbar(); + return new Promise((resolve, reject) => { + window.fetch( + '/api/games/environment', + { + method: 'GET', + mode: 'cors' + } + ).catch((res) => { + reject(res.content); + }).then((response) => { + response.text().then((text) => { + stateBucket.environment = text; + socket.on('connect', () => { + if (stateBucket.timerWorker) { + stateBucket.timerWorker.terminate(); + stateBucket.timerWorker = null; + } + syncWithGame( + socket, + UserUtility.validateAnonUserSignature(stateBucket.environment), + window + ); + }); + socket.on('connect_error', (err) => { + toast(err, 'error', true, false); + }); - const response = await XHRUtility.xhr( - '/api/games/environment', - 'GET', - null, - null - ).catch((res) => { - toast(res.content, 'error', true); + socket.on('disconnect', () => { + toast('Disconnected. Attempting reconnect...', 'error', true, false); + }); + setClientSocketHandlers(stateBucket, socket); + resolve(); + }); + }); }); +}; - stateBucket.environment = response.content; +function syncWithGame (socket, cookie, window) { + const splitUrl = window.location.href.split('/game/'); + const accessCode = splitUrl[1]; + if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === PRIMITIVES.ACCESS_CODE_LENGTH) { + socket.timeout(5000).emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, + accessCode, + { personId: cookie }, + (err, gameState) => { + if (err) { + retrySync(accessCode, socket, cookie); + } else { + handleGameState(gameState, cookie, socket); + } + } + ); + } else { + window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code'); + } +} - socket.on('connect', function () { - if (stateBucket.timerWorker) { - stateBucket.timerWorker.terminate(); - stateBucket.timerWorker = null; +function retrySync (accessCode, socket, cookie) { + socket.emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, + accessCode, + { personId: cookie }, + (gameState) => { + handleGameState(gameState, cookie, socket); } - SharedStateUtil.syncWithGame( + ); +} + +function handleGameState (gameState, cookie, socket) { + if (gameState === null) { + window.location = '/not-found?reason=' + encodeURIComponent('game-not-found'); + } else { + stateBucket.currentGameState = gameState; + document.querySelector('.spinner-container')?.remove(); + document.querySelector('.spinner-background')?.remove(); + document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM; + toast('You are connected.', 'success', true, true, 'short'); + processGameState(stateBucket.currentGameState, cookie, socket, true, true); + } +} + +function processGameState ( + currentGameState, + userId, + socket, + refreshPrompt = true, + animateContainer = false +) { + if (animateContainer) { + document.getElementById('game-state-container').animate( + [ + { opacity: '0', transform: 'translateY(10px)' }, + { opacity: '1', transform: 'translateY(0px)' } + ], { + duration: 500, + easing: 'ease-in-out', + fill: 'both' + }); + document.getElementById('client-container').animate([ + { opacity: '0' }, + { opacity: '1' } + ], { + duration: 500, + easing: 'ease-out', + fill: 'both' + }); + } + + SharedStateUtil.displayClientInfo(currentGameState.client.name, currentGameState.client.userType); + + switch (currentGameState.status) { + case STATUS.LOBBY: + const lobby = new Lobby('game-state-container', stateBucket, socket); + if (refreshPrompt) { + lobby.removeStartGameFunctionalityIfPresent(); + } + lobby.populateHeader(); + lobby.populatePlayers(); + LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e)); + lobby.setSocketHandlers(); + if (currentGameState.client.userType === USER_TYPES.MODERATOR + || currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) { + if (refreshPrompt) { + lobby.displayStartGamePromptForModerators(); + } + document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL; + } else { + if (refreshPrompt) { + lobby.displayPlayerPrompt(); + } + } + break; + case STATUS.IN_PROGRESS: + if (refreshPrompt) { + document.querySelector('#game-control-prompt')?.remove(); + document.querySelector('#leave-game-prompt')?.remove(); + } + const inProgressGame = new InProgress('game-state-container', stateBucket, socket); + IN_PROGRESS_EVENTS().forEach(e => socket.removeAllListeners(e)); + inProgressGame.setSocketHandlers(); + inProgressGame.setUserView(currentGameState.client.userType); + break; + case STATUS.ENDED: { + const ended = new Ended('game-state-container', stateBucket, socket); + ended.renderEndOfGame(currentGameState); + break; + } + default: + break; + } + + SharedStateUtil.activateRoleInfoButton(); +} + +function setClientSocketHandlers (stateBucket, socket) { + const commonGameStateAckFn = (gameState, socket) => { + stateBucket.currentGameState = gameState; + processGameState( + stateBucket.currentGameState, + gameState.client.cookie, socket, - UserUtility.validateAnonUserSignature(response.content), - window + true, + true + ); + }; + const startGameStateAckFn = (gameState) => { + commonGameStateAckFn(gameState, socket); + toast('Game started!', 'success'); + }; + + const restartGameStateAckFn = (gameState) => { + commonGameStateAckFn(gameState, socket); + toast('Everyone has returned to the Lobby!', 'success'); + }; + + const fetchGameStateHandler = (ackFn) => { + socket.emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, + stateBucket.currentGameState.accessCode, + { personId: stateBucket.currentGameState.client.cookie }, + ackFn + ); + }; + + socket.on(EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); }); + + socket.on(EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); }); + + socket.on(EVENT_IDS.SYNC_GAME_STATE, () => { + socket.emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.FETCH_GAME_STATE, + stateBucket.currentGameState.accessCode, + { personId: stateBucket.currentGameState.client.cookie }, + function (gameState) { + const oldUserType = stateBucket.currentGameState.client.userType; + stateBucket.currentGameState = gameState; + processGameState( + stateBucket.currentGameState, + gameState.client.cookie, + socket, + true, + gameState.client.userType !== oldUserType + ); + } ); }); - socket.on('connect_error', (err) => { - toast(err, 'error', true, false); + socket.on(EVENT_IDS.END_GAME, (people) => { + stateBucket.currentGameState.people = people; + stateBucket.currentGameState.status = STATUS.ENDED; + processGameState( + stateBucket.currentGameState, + stateBucket.currentGameState.client.cookie, + socket, + true, + true + ); }); - - socket.on('disconnect', () => { - toast('Disconnected. Attempting reconnect...', 'error', true, false); - }); - - SharedStateUtil.setClientSocketHandlers(stateBucket, socket); -}; +} diff --git a/client/src/modules/timer/GameTimerManager.js b/client/src/modules/timer/GameTimerManager.js index ebcf212..ee66784 100644 --- a/client/src/modules/timer/GameTimerManager.js +++ b/client/src/modules/timer/GameTimerManager.js @@ -1,17 +1,18 @@ -import { globals } from '../../config/globals.js'; +import { EVENT_IDS, SOCKET_EVENTS, USER_TYPES, TIMER_EVENTS, PRIMITIVES } from '../../config/globals.js'; import { Confirmation } from '../front_end_components/Confirmation.js'; +import { SharedStateUtil } from '../game_state/states/shared/SharedStateUtil.js'; export class GameTimerManager { constructor (stateBucket, socket) { this.stateBucket = stateBucket; this.playListener = (e) => { if (e.type === 'click' || e.code === 'Enter') { - socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.RESUME_TIMER, this.stateBucket.currentGameState.accessCode); + socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.RESUME_TIMER, this.stateBucket.currentGameState.accessCode); } }; this.pauseListener = (e) => { if (e.type === 'click' || e.code === 'Enter') { - socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.PAUSE_TIMER, this.stateBucket.currentGameState.accessCode); + socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.PAUSE_TIMER, this.stateBucket.currentGameState.accessCode); } }; } @@ -19,8 +20,8 @@ export class GameTimerManager { resumeGameTimer (totalTime, tickRate, soundManager, timerWorker) { if (window.Worker) { if ( - this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) { this.swapToPauseButton(); } @@ -33,8 +34,8 @@ export class GameTimerManager { timer.classList.add('low'); } timer.innerText = totalTime < 60000 - ? returnHumanReadableTime(totalTime, true) - : returnHumanReadableTime(totalTime); + ? SharedStateUtil.returnHumanReadableTime(totalTime, true) + : SharedStateUtil.returnHumanReadableTime(totalTime); timerWorker.onmessage = function (e) { if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds >= 0) { if (e.data.timeRemainingInMilliseconds === 0) { @@ -54,47 +55,31 @@ export class GameTimerManager { pauseGameTimer (timerWorker, timeRemaining) { if (window.Worker) { if ( - this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) { this.swapToPlayButton(); } timerWorker.postMessage('stop'); - const timer = document.getElementById('game-timer'); - if (timeRemaining < 60000) { - timer.innerText = returnHumanReadableTime(timeRemaining, true); - timer.classList.add('paused-low'); - timer.classList.add('low'); - } else { - timer.innerText = returnHumanReadableTime(timeRemaining); - timer.classList.add('paused'); - } + populateTimerElement(timeRemaining); } } displayPausedTime (time) { if ( - this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR ) { this.swapToPlayButton(); } - const timer = document.getElementById('game-timer'); - if (time < 60000) { - timer.innerText = returnHumanReadableTime(time, true); - timer.classList.add('paused-low'); - timer.classList.add('low'); - } else { - timer.innerText = returnHumanReadableTime(time); - timer.classList.add('paused'); - } + populateTimerElement(time); } displayExpiredTime () { - if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR - || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { + if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR + || this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) { const currentBtn = document.querySelector('#timer-container-moderator #play-pause img'); if (currentBtn) { currentBtn.removeEventListener('click', this.pauseListener); @@ -113,31 +98,31 @@ export class GameTimerManager { } const timer = document.getElementById('game-timer'); - timer.innerText = returnHumanReadableTime(0, true); + timer.innerText = SharedStateUtil.returnHumanReadableTime(0, true); } attachTimerSocketListeners (socket, timerWorker) { - globals.TIMER_EVENTS().forEach(e => socket.removeAllListeners(e)); + TIMER_EVENTS().forEach(e => socket.removeAllListeners(e)); - socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => { + socket.on(EVENT_IDS.PAUSE_TIMER, (timeRemaining) => { this.pauseGameTimer(timerWorker, timeRemaining); }); - socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => { - this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); + socket.on(EVENT_IDS.RESUME_TIMER, (timeRemaining) => { + this.resumeGameTimer(timeRemaining, PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); }); - socket.once(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => { + socket.once(EVENT_IDS.GET_TIME_REMAINING, (timeRemaining, paused) => { if (paused) { this.displayPausedTime(timeRemaining); } else if (timeRemaining === 0) { this.displayExpiredTime(); } else { - this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); + this.resumeGameTimer(timeRemaining, PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); } }); - socket.on(globals.COMMANDS.END_TIMER, () => { + socket.on(EVENT_IDS.END_TIMER, () => { Confirmation('The timer has expired!'); }); } @@ -178,17 +163,14 @@ export class GameTimerManager { } } -function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) { - const tenths = Math.floor((milliseconds / 100) % 10); - let seconds = Math.floor((milliseconds / 1000) % 60); - let minutes = Math.floor((milliseconds / (1000 * 60)) % 60); - let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); - - hours = hours < 10 ? '0' + hours : hours; - minutes = minutes < 10 ? '0' + minutes : minutes; - seconds = seconds < 10 ? '0' + seconds : seconds; - - return tenthsOfSeconds - ? hours + ':' + minutes + ':' + seconds + '.' + tenths - : hours + ':' + minutes + ':' + seconds; +function populateTimerElement (timeRemaining) { + const timer = document.getElementById('game-timer'); + if (timeRemaining < 60000) { + timer.innerText = SharedStateUtil.returnHumanReadableTime(timeRemaining, true); + timer.classList.add('paused-low'); + timer.classList.add('low'); + } else { + timer.innerText = SharedStateUtil.returnHumanReadableTime(timeRemaining); + timer.classList.add('paused'); + } } diff --git a/client/src/modules/timer/Timer.js b/client/src/modules/timer/Timer.js index c435754..a80eebf 100644 --- a/client/src/modules/timer/Timer.js +++ b/client/src/modules/timer/Timer.js @@ -80,6 +80,20 @@ class Timer { } } } +function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) { + const tenths = Math.floor((milliseconds / 100) % 10); + let seconds = Math.floor((milliseconds / 1000) % 60); + let minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + + hours = hours < 10 ? '0' + hours : hours; + minutes = minutes < 10 ? '0' + minutes : minutes; + seconds = seconds < 10 ? '0' + seconds : seconds; + + return tenthsOfSeconds + ? hours + ':' + minutes + ':' + seconds + '.' + tenths + : hours + ':' + minutes + ':' + seconds; +} class Singleton { constructor (totalTime, tickInterval) { @@ -105,18 +119,3 @@ class Singleton { } } } - -function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) { - const tenths = Math.floor((milliseconds / 100) % 10); - let seconds = Math.floor((milliseconds / 1000) % 60); - let minutes = Math.floor((milliseconds / (1000 * 60)) % 60); - let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); - - hours = hours < 10 ? '0' + hours : hours; - minutes = minutes < 10 ? '0' + minutes : minutes; - seconds = seconds < 10 ? '0' + seconds : seconds; - - return tenthsOfSeconds - ? hours + ':' + minutes + ':' + seconds + '.' + tenths - : hours + ':' + minutes + ':' + seconds; -} diff --git a/client/src/modules/utility/UserUtility.js b/client/src/modules/utility/UserUtility.js index d9ae4b0..00cd150 100644 --- a/client/src/modules/utility/UserUtility.js +++ b/client/src/modules/utility/UserUtility.js @@ -1,4 +1,4 @@ -import { globals } from '../../config/globals.js'; +import { ENVIRONMENTS, PRIMITIVES } from '../../config/globals.js'; /* we will use sessionStorage during local development to aid in testing, vs. localStorage for production. @@ -7,25 +7,25 @@ import { globals } from '../../config/globals.js'; export const UserUtility = { setAnonymousUserId (id, environment) { - if (environment === globals.ENVIRONMENT.LOCAL) { - sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id); + if (environment === ENVIRONMENTS.LOCAL) { + sessionStorage.setItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY, id); } else { - localStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id); + localStorage.setItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY, id); } }, validateAnonUserSignature (environment) { let userSig; - if (environment === globals.ENVIRONMENT.LOCAL) { - userSig = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY); + if (environment === ENVIRONMENTS.LOCAL) { + userSig = sessionStorage.getItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY); } else { - userSig = localStorage.getItem(globals.PLAYER_ID_COOKIE_KEY); + userSig = localStorage.getItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY); } return ( userSig && typeof userSig === 'string' && /^[a-zA-Z0-9]+$/.test(userSig) - && userSig.length === globals.USER_SIGNATURE_LENGTH + && userSig.length === PRIMITIVES.USER_SIGNATURE_LENGTH ) ? userSig : false; diff --git a/client/src/modules/utility/XHRUtility.js b/client/src/modules/utility/XHRUtility.js deleted file mode 100644 index 0607047..0000000 --- a/client/src/modules/utility/XHRUtility.js +++ /dev/null @@ -1,39 +0,0 @@ -export const XHRUtility = - { - standardHeaders: [['Content-Type', 'application/json'], ['Accept', 'application/json'], ['X-Requested-With', 'XMLHttpRequest']], - - // Easily make XHR calls with a promise wrapper. Defaults to GET and MIME type application/JSON - xhr (url, method = 'GET', headers, body = null) { - if (headers === undefined || headers === null) { - headers = this.standardHeaders; - } - if (typeof url !== 'string' || url.trim().length < 1) { - return Promise.reject('Cannot request with empty URL: ' + url); - } - - const req = new XMLHttpRequest(); - req.open(method, url.trim()); - - for (const hdr of headers) { - if (hdr.length !== 2) continue; - req.setRequestHeader(hdr[0], hdr[1]); - } - - return new Promise((resolve, reject) => { - req.onload = function () { - const response = { - status: this.status, - statusText: this.statusText, - content: this.responseText - }; - if (this.status >= 200 && this.status < 400) { - resolve(response); - } else { - reject(response); - } - }; - body ? req.send(body) : req.send(); - }); - } - - }; diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 09c57df..a714f70 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -1,8 +1,10 @@ -import 'core-js/stable'; -import 'regenerator-runtime/runtime'; -import { gameHandler } from '../modules/page_handlers/gameHandler'; +import { gameHandler } from '../modules/page_handlers/gameHandler.js'; import { io } from 'socket.io-client'; -import { XHRUtility } from '../modules/utility/XHRUtility.js'; import gameTemplate from '../view_templates/GameTemplate.js'; +import { toast } from '../modules/front_end_components/Toast.js'; -await gameHandler(io('/in-game'), XHRUtility, window, gameTemplate); +gameHandler(io('/in-game'), window, gameTemplate).then(() => { + toast('Connecting...', 'warning', true, false); +}).catch((e) => { + toast(e, 'error', true); +}); diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index 0060664..894cfff 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -1,4 +1,3 @@ -import { XHRUtility } from '../modules/utility/XHRUtility.js'; import { toast } from '../modules/front_end_components/Toast.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js'; import { Confirmation } from '../modules/front_end_components/Confirmation.js'; @@ -25,32 +24,33 @@ function attemptToJoinGame (event) { form.removeEventListener('submit', attemptToJoinGame); form.classList.add('submitted'); document.getElementById('join-button')?.setAttribute('value', 'Joining...'); - XHRUtility.xhr( + fetch( '/api/games/' + userCode.toUpperCase().trim() + '/availability', - 'GET', - null, - null - ) - .then((res) => { - if (res.status === 200) { - const json = JSON.parse(res.content); + { + method: 'GET', + mode: 'cors' + } + ).then((res) => { + if (res.status === 200) { + res.json().then(json => { window.location = window.location.protocol + '//' + window.location.host + '/join/' + encodeURIComponent(json.accessCode) + '?playerCount=' + encodeURIComponent(json.playerCount) + '&timer=' + encodeURIComponent(getTimeString(json.timerParams)); - } - }).catch((res) => { - form.addEventListener('submit', attemptToJoinGame); - form.classList.remove('submitted'); - document.getElementById('join-button')?.setAttribute('value', 'Join'); - if (res.status === 404) { - toast('Game not found', 'error', true); - } else if (res.status === 400) { - toast(res.content, 'error', true); - } else { - toast('An unknown error occurred. Please try again later.', 'error', true); - } - }); + }); + } + }).catch((res) => { + form.addEventListener('submit', attemptToJoinGame); + form.classList.remove('submitted'); + document.getElementById('join-button')?.setAttribute('value', 'Join'); + if (res.status === 404) { + toast('Game not found', 'error', true); + } else if (res.status === 400) { + toast(res.content, 'error', true); + } else { + toast('An unknown error occurred. Please try again later.', 'error', true); + } + }); } else { toast('Invalid code. Codes are 4 numbers or letters.', 'error', true, true); } diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index 3496832..6d9efd6 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -1,14 +1,13 @@ import { injectNavbar } from '../modules/front_end_components/Navbar.js'; import { toast } from '../modules/front_end_components/Toast.js'; -import { XHRUtility } from '../modules/utility/XHRUtility.js'; import { UserUtility } from '../modules/utility/UserUtility.js'; -import { globals } from '../config/globals.js'; +import { ENVIRONMENTS, PRIMITIVES } from '../config/globals.js'; const join = () => { injectNavbar(); const splitUrl = window.location.pathname.split('/join/'); const accessCode = splitUrl[1]; - if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { + if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === PRIMITIVES.ACCESS_CODE_LENGTH) { document.getElementById('game-code').innerText = accessCode; document.getElementById('game-time').innerText = decodeURIComponent((new URL(document.location)).searchParams.get('timer')); @@ -29,10 +28,11 @@ const joinHandler = (e) => { if (validateName(name)) { sendJoinRequest(e, name, accessCode) .then((res) => { - const json = JSON.parse(res.content); - UserUtility.setAnonymousUserId(json.cookie, json.environment); - resetJoinButtonState(e, res, joinHandler); - window.location = '/game/' + accessCode; + res.json().then(json => { + UserUtility.setAnonymousUserId(json.cookie, json.environment); + resetJoinButtonState(e, res, joinHandler); + window.location = '/game/' + accessCode; + }); }).catch((res) => { handleJoinError(e, res, joinHandler); }); @@ -46,17 +46,22 @@ function sendJoinRequest (e, name, accessCode) { document.getElementById('join-submit').classList.add('submitted'); document.getElementById('join-submit').setAttribute('value', '...'); - return XHRUtility.xhr( + return fetch( '/api/games/' + accessCode + '/players', - 'PATCH', - null, - JSON.stringify({ - playerName: name, - accessCode: accessCode, - sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), - localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION), - joinAsSpectator: document.getElementById('join-as-spectator').checked - }) + { + method: 'PATCH', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + playerName: name, + accessCode: accessCode, + sessionCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.LOCAL), + localCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.PRODUCTION), + joinAsSpectator: document.getElementById('join-as-spectator').checked + }) + } ); } diff --git a/client/src/view_templates/GameTemplate.js b/client/src/view_templates/GameTemplate.js index 2d6b90f..2bca8ea 100644 --- a/client/src/view_templates/GameTemplate.js +++ b/client/src/view_templates/GameTemplate.js @@ -18,7 +18,7 @@ const template =
-

Connecting to game...

+

Waiting for connection to Room...

diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index f4efe0b..777e1e7 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -3,13 +3,13 @@ const router = express.Router(); const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug'); const logger = require('../modules/Logger')(debugMode); const eventManager = (require('../modules/singletons/EventManager.js')).instance; -const globals = require('../config/globals.js'); const cors = require('cors'); +const { CORS_OPTIONS, CONTENT_TYPE_VALIDATOR } = require('../config/globals'); -router.use(cors(globals.CORS)); +router.use(cors(CORS_OPTIONS)); router.post('/sockets/broadcast', (req, res, next) => { - globals.CONTENT_TYPE_VALIDATOR(req, res, next); + CONTENT_TYPE_VALIDATOR(req, res, next); }); // TODO: implement client-side display of this message. diff --git a/server/api/GamesAPI.js b/server/api/RoomsAPI.js similarity index 81% rename from server/api/GamesAPI.js rename to server/api/RoomsAPI.js index ada8b5a..7744751 100644 --- a/server/api/GamesAPI.js +++ b/server/api/RoomsAPI.js @@ -4,8 +4,8 @@ const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase()) const logger = require('../modules/Logger')(debugMode); const GameManager = require('../modules/singletons/GameManager.js'); const rateLimit = require('express-rate-limit').default; -const globals = require('../config/globals.js'); const cors = require('cors'); +const { CORS_OPTIONS, CONTENT_TYPE_VALIDATOR, PRIMITIVES, ERROR_MESSAGES, ENVIRONMENTS } = require('../config/globals'); const gameManager = GameManager.instance; @@ -20,19 +20,19 @@ const gameEndpointLimiter = rateLimit({ legacyHeaders: false }); -router.use(cors(globals.CORS)); -router.options('/:code/players', cors(globals.CORS)); -router.options('/create', cors(globals.CORS)); -router.options('/restart', cors(globals.CORS)); +router.use(cors(CORS_OPTIONS)); +router.options('/:code/players', cors(CORS_OPTIONS)); +router.options('/create', cors(CORS_OPTIONS)); +router.options('/restart', cors(CORS_OPTIONS)); router.post('/create', (req, res, next) => { - globals.CONTENT_TYPE_VALIDATOR(req, res, next); + CONTENT_TYPE_VALIDATOR(req, res, next); }); router.patch('/players', (req, res, next) => { - globals.CONTENT_TYPE_VALIDATOR(req, res, next); + CONTENT_TYPE_VALIDATOR(req, res, next); }); router.patch('/restart', (req, res, next) => { - globals.CONTENT_TYPE_VALIDATOR(req, res, next); + CONTENT_TYPE_VALIDATOR(req, res, next); }); router.post('/create', gameEndpointLimiter, function (req, res) { @@ -43,8 +43,8 @@ router.post('/create', gameEndpointLimiter, function (req, res) { res.status(201).send(result); // game was created successfully, and access code was returned } }).catch((e) => { - if (e === globals.ERROR_MESSAGE.BAD_CREATE_REQUEST) { - res.status(400).send(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST); + if (e === ERROR_MESSAGES.BAD_CREATE_REQUEST) { + res.status(400).send(ERROR_MESSAGES.BAD_CREATE_REQUEST); } }); }); @@ -78,7 +78,7 @@ router.patch('/:code/players', async function (req, res) { } else { const game = await gameManager.getActiveGame(req.body.accessCode); if (game) { - const inUseCookie = gameManager.environment === globals.ENVIRONMENT.PRODUCTION ? req.body.localCookie : req.body.sessionCookie; + const inUseCookie = gameManager.environment === ENVIRONMENTS.PRODUCTION ? req.body.localCookie : req.body.sessionCookie; gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => { res.status(200).send({ cookie: data, environment: gameManager.environment }); }).catch((data) => { @@ -103,7 +103,7 @@ router.patch('/:code/restart', async function (req, res) { } else { const game = await gameManager.getActiveGame(req.body.accessCode); if (game) { - gameManager.restartGame(game, gameManager.namespace, req.query.status).then((data) => { + gameManager.restartGame(game, gameManager.namespace).then((data) => { res.status(200).send(); }).catch((code) => { res.status(code).send(); @@ -123,11 +123,11 @@ function validateName (name) { } function validateCookie (cookie) { - return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === globals.INSTANCE_ID_LENGTH); + return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === PRIMITIVES.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 === PRIMITIVES.ACCESS_CODE_LENGTH; } function validateSpectatorFlag (spectatorFlag) { diff --git a/server/config/globals.js b/server/config/globals.js index 8ff82a8..29ebc14 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -1,4 +1,4 @@ -const globals = { +const PRIMITIVES = { ACCESS_CODE_CHAR_POOL: 'BCDFGHJLMNPQRSTVWXYZ23456789', INSTANCE_ID_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', ACCESS_CODE_LENGTH: 4, @@ -6,143 +6,169 @@ const globals = { CLOCK_TICK_INTERVAL_MILLIS: 100, MAX_CUSTOM_ROLE_NAME_LENGTH: 50, MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, - ALIGNMENT: { - GOOD: 'good', - EVIL: 'evil' - }, - REDIS_CHANNELS: { - ACTIVE_GAME_STREAM: 'active_game_stream' - }, - CORS: process.env.NODE_ENV?.trim() === 'development' - ? { - origin: '*', - optionsSuccessStatus: 200 - } - : { - origin: 'https://play-werewolf.app', - optionsSuccessStatus: 200 - }, - CONTENT_TYPE_VALIDATOR: (req, res, next) => { - req.accepts(); - if (req.is('application/json')) { - next(); - } else { - res.status(400).send('Request has invalid content type.'); - } - }, STALE_GAME_SECONDS: 86400, - SOCKET_EVENTS: { - IN_GAME_MESSAGE: 'inGameMessage' - }, - EVENT_IDS: { - NEW_GAME: 'newGame', - FETCH_GAME_STATE: 'fetchGameState', - START_GAME: 'startGame', - PAUSE_TIMER: 'pauseTimer', - RESUME_TIMER: 'resumeTimer', - END_TIMER: 'endTimer', - GET_TIME_REMAINING: 'getTimeRemaining', - SOURCE_TIMER_EVENT: 'sourceTimerEvent', - KILL_PLAYER: 'killPlayer', - REVEAL_PLAYER: 'revealPlayer', - TRANSFER_MODERATOR: 'transferModerator', - CHANGE_NAME: 'changeName', - END_GAME: 'endGame', - RESTART_GAME: 'restartGame', - PLAYER_JOINED: 'playerJoined', - UPDATE_SPECTATORS: 'updateSpectators', - ADD_SPECTATOR: 'addSpectator', - SYNC_GAME_STATE: 'syncGameState', - UPDATE_SOCKET: 'updateSocket', - ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', - TIMER_EVENT: 'timerEvent', - KICK_PERSON: 'kickPerson', - UPDATE_GAME_ROLES: 'updateGameRoles', - LEAVE_ROOM: 'leaveRoom' - }, - SYNCABLE_EVENTS: function () { - return [ - this.EVENT_IDS.NEW_GAME, - this.EVENT_IDS.START_GAME, - this.EVENT_IDS.KILL_PLAYER, - this.EVENT_IDS.REVEAL_PLAYER, - this.EVENT_IDS.TRANSFER_MODERATOR, - this.EVENT_IDS.END_GAME, - this.EVENT_IDS.RESTART_GAME, - this.EVENT_IDS.PLAYER_JOINED, - this.EVENT_IDS.ADD_SPECTATOR, - this.EVENT_IDS.SYNC_GAME_STATE, - this.EVENT_IDS.UPDATE_SOCKET, - this.EVENT_IDS.FETCH_GAME_STATE, - this.EVENT_IDS.ASSIGN_DEDICATED_MOD, - this.EVENT_IDS.RESUME_TIMER, - this.EVENT_IDS.PAUSE_TIMER, - this.EVENT_IDS.END_TIMER, - this.EVENT_IDS.KICK_PERSON, - this.EVENT_IDS.UPDATE_GAME_ROLES, - this.EVENT_IDS.LEAVE_ROOM - ]; - }, - TIMER_EVENTS: function () { - return [ - this.EVENT_IDS.RESUME_TIMER, - this.EVENT_IDS.PAUSE_TIMER, - this.EVENT_IDS.END_TIMER, - this.EVENT_IDS.GET_TIME_REMAINING - ]; - }, - MESSAGES: { - ENTER_NAME: 'Client must enter name.' - }, - STATUS: { - LOBBY: 'lobby', - IN_PROGRESS: 'in progress', - ENDED: 'ended' - }, USER_SIGNATURE_LENGTH: 25, INSTANCE_ID_LENGTH: 75, - USER_TYPES: { - MODERATOR: 'moderator', - PLAYER: 'player', - TEMPORARY_MODERATOR: 'temp mod', - KILLED_PLAYER: 'killed', - KILLED_BOT: 'killed bot', - SPECTATOR: 'spectator', - BOT: 'bot' - }, - ERROR_MESSAGE: { - GAME_IS_FULL: 'This game is full', - BAD_CREATE_REQUEST: 'Game has invalid options.', - NO_UNIQUE_ACCESS_CODE: 'Could not generate a unique access code.' - }, - EVENTS: { - PLAYER_JOINED: 'playerJoined', - PLAYER_LEFT: 'playerLeft', - SYNC_GAME_STATE: 'syncGameState', - UPDATE_SPECTATORS: 'newSpectator', - BROADCAST: 'broadcast' - }, - ENVIRONMENT: { - LOCAL: 'local', - PRODUCTION: 'production' - }, - LOG_LEVEL: { - INFO: 'info', - DEBUG: 'debug', - ERROR: 'error', - WARN: 'warn', - TRACE: 'trace' - }, - GAME_PROCESS_COMMANDS: { - END_TIMER: 'endTimer', - START_GAME: 'startGame', - START_TIMER: 'startTimer', - PAUSE_TIMER: 'pauseTimer', - RESUME_TIMER: 'resumeTimer', - GET_TIME_REMAINING: 'getTimeRemaining' - }, - MOCK_AUTH: 'mock_auth', - MAX_SPECTATORS: 25 + MAX_SPECTATORS: 25, + MOCK_AUTH: 'mock_auth' }; -module.exports = globals; +const LOG_LEVEL = { + INFO: 'info', + DEBUG: 'debug', + ERROR: 'error', + WARN: 'warn', + TRACE: 'trace' +}; + +const ALIGNMENT = { + GOOD: 'good', + EVIL: 'evil' +}; + +const REDIS_CHANNELS = { + ACTIVE_GAME_STREAM: 'active_game_stream' +}; + +const CORS_OPTIONS = process.env.NODE_ENV?.trim() === 'development' + ? { + origin: '*', + optionsSuccessStatus: 200 + } + : { + origin: 'https://play-werewolf.app', + optionsSuccessStatus: 200 + }; + +const CONTENT_TYPE_VALIDATOR = (req, res, next) => { + req.accepts(); + if (req.is('application/json')) { + next(); + } else { + res.status(400).send('Request has invalid content type.'); + } +}; + +const SOCKET_EVENTS = { + IN_GAME_MESSAGE: 'inGameMessage' +}; + +const EVENT_IDS = { + NEW_GAME: 'newGame', + FETCH_GAME_STATE: 'fetchGameState', + START_GAME: 'startGame', + PAUSE_TIMER: 'pauseTimer', + RESUME_TIMER: 'resumeTimer', + END_TIMER: 'endTimer', + GET_TIME_REMAINING: 'getTimeRemaining', + SOURCE_TIMER_EVENT: 'sourceTimerEvent', + KILL_PLAYER: 'killPlayer', + REVEAL_PLAYER: 'revealPlayer', + TRANSFER_MODERATOR: 'transferModerator', + CHANGE_NAME: 'changeName', + END_GAME: 'endGame', + RESTART_GAME: 'restartGame', + PLAYER_JOINED: 'playerJoined', + UPDATE_SPECTATORS: 'updateSpectators', + ADD_SPECTATOR: 'addSpectator', + SYNC_GAME_STATE: 'syncGameState', + UPDATE_SOCKET: 'updateSocket', + ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', + TIMER_EVENT: 'timerEvent', + KICK_PERSON: 'kickPerson', + UPDATE_GAME_ROLES: 'updateGameRoles', + LEAVE_ROOM: 'leaveRoom', + BROADCAST: 'broadcast' +}; + +const SYNCABLE_EVENTS = function () { + return [ + EVENT_IDS.NEW_GAME, + EVENT_IDS.START_GAME, + EVENT_IDS.KILL_PLAYER, + EVENT_IDS.REVEAL_PLAYER, + EVENT_IDS.TRANSFER_MODERATOR, + EVENT_IDS.END_GAME, + EVENT_IDS.RESTART_GAME, + EVENT_IDS.PLAYER_JOINED, + EVENT_IDS.ADD_SPECTATOR, + EVENT_IDS.SYNC_GAME_STATE, + EVENT_IDS.UPDATE_SOCKET, + EVENT_IDS.FETCH_GAME_STATE, + EVENT_IDS.ASSIGN_DEDICATED_MOD, + EVENT_IDS.RESUME_TIMER, + EVENT_IDS.PAUSE_TIMER, + EVENT_IDS.END_TIMER, + EVENT_IDS.KICK_PERSON, + EVENT_IDS.UPDATE_GAME_ROLES, + EVENT_IDS.LEAVE_ROOM + ]; +}; + +const TIMER_EVENTS = function () { + return [ + EVENT_IDS.RESUME_TIMER, + EVENT_IDS.PAUSE_TIMER, + EVENT_IDS.END_TIMER, + EVENT_IDS.GET_TIME_REMAINING + ]; +}; + +const MESSAGES = { + ENTER_NAME: 'Client must enter name.' +}; + +const STATUS = { + LOBBY: 'lobby', + IN_PROGRESS: 'in progress', + ENDED: 'ended' +}; + +const USER_TYPES = { + MODERATOR: 'moderator', + PLAYER: 'player', + TEMPORARY_MODERATOR: 'temp mod', + KILLED_PLAYER: 'killed', + KILLED_BOT: 'killed bot', + SPECTATOR: 'spectator', + BOT: 'bot' +}; + +const ERROR_MESSAGES = { + GAME_IS_FULL: 'This game is full', + BAD_CREATE_REQUEST: 'Game has invalid options.', + NO_UNIQUE_ACCESS_CODE: 'Could not generate a unique access code.' +}; + +const ENVIRONMENTS = { + LOCAL: 'local', + PRODUCTION: 'production' +}; + +const GAME_PROCESS_COMMANDS = { + END_TIMER: 'endTimer', + START_GAME: 'startGame', + START_TIMER: 'startTimer', + PAUSE_TIMER: 'pauseTimer', + RESUME_TIMER: 'resumeTimer', + GET_TIME_REMAINING: 'getTimeRemaining' +}; + +module.exports = { + PRIMITIVES, + LOG_LEVEL, + ALIGNMENT, + REDIS_CHANNELS, + CORS_OPTIONS, + CONTENT_TYPE_VALIDATOR, + SOCKET_EVENTS, + EVENT_IDS, + SYNCABLE_EVENTS, + TIMER_EVENTS, + MESSAGES, + STATUS, + USER_TYPES, + ERROR_MESSAGES, + ENVIRONMENTS, + GAME_PROCESS_COMMANDS +}; diff --git a/server/model/GameCreationRequest.js b/server/model/GameCreationRequest.js index 92813a6..9301c8f 100644 --- a/server/model/GameCreationRequest.js +++ b/server/model/GameCreationRequest.js @@ -1,4 +1,4 @@ -const globals = require('../config/globals'); +const { ERROR_MESSAGES, PRIMITIVES, ALIGNMENT } = require('../config/globals'); class GameCreationRequest { constructor ( @@ -24,7 +24,7 @@ class GameCreationRequest { || expectedKeys.some((key) => !Object.keys(gameParams).includes(key)) || !valid(gameParams) ) { - return Promise.reject(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST); + return Promise.reject(ERROR_MESSAGES.BAD_CREATE_REQUEST); } else { return Promise.resolve(); } @@ -37,12 +37,12 @@ class GameCreationRequest { && typeof entry === 'object' && typeof entry.role === 'string' && entry.role.length > 0 - && entry.role.length <= globals.MAX_CUSTOM_ROLE_NAME_LENGTH + && entry.role.length <= PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH && typeof entry.team === 'string' - && (entry.team === globals.ALIGNMENT.GOOD || entry.team === globals.ALIGNMENT.EVIL) + && (entry.team === ALIGNMENT.GOOD || entry.team === ALIGNMENT.EVIL) && typeof entry.description === 'string' && entry.description.length > 0 - && entry.description.length <= globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH + && entry.description.length <= PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH && (!entry.custom || typeof entry.custom === 'boolean') && typeof entry.quantity === 'number' && entry.quantity >= 0 diff --git a/server/model/Person.js b/server/model/Person.js index 21c859b..a27eed2 100644 --- a/server/model/Person.js +++ b/server/model/Person.js @@ -1,5 +1,5 @@ // noinspection DuplicatedCode -const globals = require('../config/globals'); +const { USER_TYPES } = require('../config/globals'); class Person { constructor (id, cookie, name, userType, gameRole = null, gameRoleDescription = null, alignment = null, assigned = false) { @@ -12,7 +12,7 @@ class Person { this.gameRoleDescription = gameRoleDescription; this.alignment = alignment; this.assigned = assigned; - this.out = userType === globals.USER_TYPES.MODERATOR || userType === globals.USER_TYPES.SPECTATOR; + this.out = userType === USER_TYPES.MODERATOR || userType === 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 4b10ab9..b2369a7 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -1,7 +1,6 @@ -const globals = require('../config/globals'); const GameStateCurator = require('./GameStateCurator'); const GameCreationRequest = require('../model/GameCreationRequest'); -const EVENT_IDS = globals.EVENT_IDS; +const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS } = require('../config/globals'); const Events = [ { @@ -12,7 +11,7 @@ const Events = [ }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( - globals.EVENTS.PLAYER_JOINED, + EVENT_IDS.PLAYER_JOINED, GameStateCurator.mapPerson(socketArgs), game.isStartable ); @@ -87,7 +86,7 @@ const Events = [ }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( - globals.EVENT_IDS.ADD_SPECTATOR, + EVENT_IDS.ADD_SPECTATOR, GameStateCurator.mapPerson(socketArgs) ); } @@ -117,7 +116,7 @@ const Events = [ communicate: async (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); + vars.gameManager.namespace.to(matchingPerson.socketId).emit(EVENT_IDS.SYNC_GAME_STATE); } } }, @@ -125,7 +124,7 @@ const Events = [ id: EVENT_IDS.START_GAME, stateChange: async (game, socketArgs, vars) => { if (game.isStartable) { - game.status = globals.STATUS.IN_PROGRESS; + game.status = STATUS.IN_PROGRESS; vars.gameManager.deal(game); if (game.hasTimer) { game.timerParams.paused = true; @@ -137,7 +136,7 @@ const Events = [ if (vars.ackFn) { vars.ackFn(); } - vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME); + vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.START_GAME); } }, { @@ -145,9 +144,9 @@ const Events = [ stateChange: async (game, socketArgs, vars) => { const person = game.people.find((person) => person.id === socketArgs.personId); if (person && !person.out) { - person.userType = person.userType === globals.USER_TYPES.BOT - ? globals.USER_TYPES.KILLED_BOT - : globals.USER_TYPES.KILLED_PLAYER; + person.userType = person.userType === USER_TYPES.BOT + ? USER_TYPES.KILLED_BOT + : USER_TYPES.KILLED_PLAYER; person.out = true; person.killed = true; } @@ -155,7 +154,7 @@ const Events = [ communicate: async (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); + vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, person); } } }, @@ -171,7 +170,7 @@ const Events = [ const person = game.people.find((person) => person.id === socketArgs.personId); if (person) { vars.gameManager.namespace.in(game.accessCode).emit( - globals.EVENT_IDS.REVEAL_PLAYER, + EVENT_IDS.REVEAL_PLAYER, { id: person.id, gameRole: person.gameRole, @@ -184,7 +183,7 @@ const Events = [ { id: EVENT_IDS.END_GAME, stateChange: async (game, socketArgs, vars) => { - game.status = globals.STATUS.ENDED; + game.status = STATUS.ENDED; if (game.hasTimer && vars.timerManager.timerThreads[game.accessCode]) { vars.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode); vars.timerManager.timerThreads[game.accessCode].kill(); @@ -195,7 +194,7 @@ const Events = [ }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode) - .emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); + .emit(EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); if (vars.ackFn) { vars.ackFn(); } @@ -208,14 +207,14 @@ const Events = [ const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); if (currentModerator) { if (currentModerator.gameRole) { - currentModerator.userType = globals.USER_TYPES.KILLED_PLAYER; + currentModerator.userType = USER_TYPES.KILLED_PLAYER; } else { - currentModerator.userType = globals.USER_TYPES.SPECTATOR; + currentModerator.userType = USER_TYPES.SPECTATOR; } game.previousModeratorId = currentModerator.id; } if (toTransferTo) { - toTransferTo.userType = globals.USER_TYPES.MODERATOR; + toTransferTo.userType = USER_TYPES.MODERATOR; game.currentModeratorId = toTransferTo.id; } }, @@ -223,7 +222,7 @@ const Events = [ if (vars.ackFn) { vars.ackFn(); } - vars.gameManager.namespace.to(game.accessCode).emit(globals.EVENT_IDS.SYNC_GAME_STATE); + vars.gameManager.namespace.to(game.accessCode).emit(EVENT_IDS.SYNC_GAME_STATE); } }, { @@ -233,10 +232,10 @@ const Events = [ const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId); if (currentModerator && toTransferTo) { if (currentModerator.id !== toTransferTo.id) { - currentModerator.userType = globals.USER_TYPES.PLAYER; + currentModerator.userType = USER_TYPES.PLAYER; } - toTransferTo.userType = globals.USER_TYPES.MODERATOR; + toTransferTo.userType = USER_TYPES.MODERATOR; toTransferTo.out = true; toTransferTo.killed = true; game.previousModeratorId = currentModerator.id; @@ -247,14 +246,14 @@ const Events = [ 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, moderator); + vars.gameManager.namespace.to(moderator.socketId).emit(EVENT_IDS.SYNC_GAME_STATE); + moderatorSocket.to(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, moderator); } else { - vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, moderator); + vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, moderator); } 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); + vars.gameManager.namespace.to(previousModerator.socketId).emit(EVENT_IDS.SYNC_GAME_STATE); } } }, @@ -274,7 +273,7 @@ const Events = [ if (vars.ackFn) { vars.ackFn(); } - vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME); + vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.RESTART_GAME); } }, { @@ -294,7 +293,7 @@ const Events = [ const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); if (socket) { vars.gameManager.namespace.to(socket.id).emit( - globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused ); @@ -302,10 +301,10 @@ const Events = [ } } else { // we need to consult another container for the timer data await vars.eventManager.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, vars.eventManager.createMessageToPublish( game.accessCode, - globals.EVENT_IDS.SOURCE_TIMER_EVENT, + EVENT_IDS.SOURCE_TIMER_EVENT, vars.instanceId, JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) ) @@ -330,7 +329,7 @@ const Events = [ }); } else { await vars.eventManager.publisher.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, vars.eventManager.createMessageToPublish( game.accessCode, socketArgs.timerEventSubtype, @@ -353,7 +352,7 @@ const Events = [ game.timerParams.timeRemaining = 0; }, communicate: async (game, socketArgs, vars) => { - vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_TIMER); + vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.END_TIMER); } }, { @@ -363,7 +362,7 @@ const Events = [ game.timerParams.timeRemaining = socketArgs.timeRemaining; }, communicate: async (game, socketArgs, vars) => { - vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, socketArgs.timeRemaining); + vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.PAUSE_TIMER, socketArgs.timeRemaining); } }, { @@ -373,7 +372,7 @@ const Events = [ game.timerParams.timeRemaining = socketArgs.timeRemaining; }, communicate: async (game, socketArgs, vars) => { - vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, socketArgs.timeRemaining); + vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.RESUME_TIMER, socketArgs.timeRemaining); } }, { @@ -382,7 +381,7 @@ const Events = [ communicate: async (game, socketArgs, vars) => { const socket = vars.gameManager.namespace.sockets.get(socketArgs.socketId); if (socket) { - vars.gameManager.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, game.timerParams.paused); + vars.gameManager.namespace.to(socket.id).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, game.timerParams.paused); } } } diff --git a/server/modules/GameProcess.js b/server/modules/GameProcess.js index 92ab35a..9d0368a 100644 --- a/server/modules/GameProcess.js +++ b/server/modules/GameProcess.js @@ -1,4 +1,4 @@ -const globals = require('../config/globals.js'); +const { GAME_PROCESS_COMMANDS, PRIMITIVES } = require('../config/globals'); const ServerTimer = require('./ServerTimer.js'); let timer; @@ -7,43 +7,43 @@ let timer; process.on('message', (msg) => { const logger = require('./Logger')(msg.logLevel); switch (msg.command) { - case globals.GAME_PROCESS_COMMANDS.START_TIMER: + case GAME_PROCESS_COMMANDS.START_TIMER: logger.trace('CHILD PROCESS ' + msg.accessCode + ': START TIMER'); timer = new ServerTimer( msg.hours, msg.minutes, - globals.CLOCK_TICK_INTERVAL_MILLIS, + PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, logger ); timer.runTimer().then(() => { logger.debug('Timer finished for ' + msg.accessCode); - process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER }); + process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER }); process.exit(0); }); break; - case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER: + case GAME_PROCESS_COMMANDS.PAUSE_TIMER: timer.stopTimer(); logger.trace('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER'); - process.send({ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis }); + process.send({ command: GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis }); break; - case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER: + case GAME_PROCESS_COMMANDS.RESUME_TIMER: timer.resumeTimer().then(() => { logger.debug('Timer finished for ' + msg.accessCode); - process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER }); + process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER }); process.exit(0); }); logger.trace('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER'); - process.send({ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis }); + process.send({ command: GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis }); break; - case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: + case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: logger.trace('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING'); process.send({ - command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, timeRemaining: timer.currentTimeInMillis, socketId: msg.socketId }); diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index 97c2f25..790924a 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -1,4 +1,4 @@ -const globals = require('../config/globals'); +const { USER_TYPES, STATUS } = require('../config/globals'); /* The purpose of this component is to only return the game state information that is necessary. For example, we only want to return player role information to moderators. This avoids any possibility of a player having access to @@ -12,7 +12,7 @@ const GameStateCurator = { mapPeopleForModerator: (people) => { return people .filter((person) => { - return person.assigned === true || (person.userType === globals.USER_TYPES.SPECTATOR || person.userType === globals.USER_TYPES.MODERATOR); + return person.assigned === true || (person.userType === USER_TYPES.SPECTATOR || person.userType === USER_TYPES.MODERATOR); }) .map((person) => ({ name: person.name, @@ -45,7 +45,7 @@ const GameStateCurator = { }; function getGameStateBasedOnPermissions (game, person) { - const client = game.status === globals.STATUS.LOBBY // people won't be able to know their role until past the lobby stage. + const client = game.status === STATUS.LOBBY // people won't be able to know their role until past the lobby stage. ? { name: person.name, hasEnteredName: person.hasEnteredName, id: person.id, cookie: person.cookie, userType: person.userType } : { name: person.name, @@ -61,7 +61,7 @@ function getGameStateBasedOnPermissions (game, person) { killed: person.killed }; switch (person.userType) { - case globals.USER_TYPES.MODERATOR: + case USER_TYPES.MODERATOR: return { accessCode: game.accessCode, status: game.status, @@ -73,10 +73,10 @@ function getGameStateBasedOnPermissions (game, person) { timerParams: game.timerParams, isStartable: game.isStartable }; - case globals.USER_TYPES.TEMPORARY_MODERATOR: - case globals.USER_TYPES.SPECTATOR: - case globals.USER_TYPES.PLAYER: - case globals.USER_TYPES.KILLED_PLAYER: + case USER_TYPES.TEMPORARY_MODERATOR: + case USER_TYPES.SPECTATOR: + case USER_TYPES.PLAYER: + case USER_TYPES.KILLED_PLAYER: return { accessCode: game.accessCode, status: game.status, @@ -86,7 +86,7 @@ function getGameStateBasedOnPermissions (game, person) { gameSize: game.gameSize, people: game.people .filter((person) => { - return person.assigned === true || person.userType === globals.USER_TYPES.SPECTATOR; + return person.assigned === true || person.userType === USER_TYPES.SPECTATOR; }) .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), timerParams: game.timerParams, diff --git a/server/modules/Logger.js b/server/modules/Logger.js index 72c7899..96aa396 100644 --- a/server/modules/Logger.js +++ b/server/modules/Logger.js @@ -1,6 +1,6 @@ -const globals = require('../config/globals'); +const { LOG_LEVEL } = require('../config/globals'); -module.exports = function (logLevel = globals.LOG_LEVEL.INFO) { +module.exports = function (logLevel = LOG_LEVEL.INFO) { return { logLevel: logLevel, info (message = '') { @@ -10,7 +10,7 @@ module.exports = function (logLevel = globals.LOG_LEVEL.INFO) { error (message = '') { if ( - logLevel === globals.LOG_LEVEL.INFO + logLevel === LOG_LEVEL.INFO ) { return; } const now = new Date(); console.error('ERROR ', now.toGMTString(), ': ', message); @@ -18,25 +18,25 @@ module.exports = function (logLevel = globals.LOG_LEVEL.INFO) { warn (message = '') { if ( - logLevel === globals.LOG_LEVEL.INFO - || logLevel === globals.LOG_LEVEL.ERROR + logLevel === LOG_LEVEL.INFO + || logLevel === LOG_LEVEL.ERROR ) return; const now = new Date(); console.error('WARN ', now.toGMTString(), ': ', message); }, debug (message = '') { - if (logLevel === globals.LOG_LEVEL.INFO || logLevel === globals.LOG_LEVEL.ERROR || logLevel === globals.LOG_LEVEL.WARN) return; + if (logLevel === LOG_LEVEL.INFO || logLevel === LOG_LEVEL.ERROR || logLevel === LOG_LEVEL.WARN) return; const now = new Date(); console.debug('DEBUG ', now.toGMTString(), ': ', message); }, trace (message = '') { if ( - logLevel === globals.LOG_LEVEL.INFO - || logLevel === globals.LOG_LEVEL.WARN - || logLevel === globals.LOG_LEVEL.DEBUG - || logLevel === globals.LOG_LEVEL.ERROR + logLevel === LOG_LEVEL.INFO + || logLevel === LOG_LEVEL.WARN + || logLevel === LOG_LEVEL.DEBUG + || logLevel === LOG_LEVEL.ERROR ) return; const now = new Date(); diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js index 85a3f69..18eb7d2 100644 --- a/server/modules/ServerBootstrapper.js +++ b/server/modules/ServerBootstrapper.js @@ -1,4 +1,4 @@ -const LOG_LEVEL = require('../config/globals').LOG_LEVEL; +const { LOG_LEVEL, ENVIRONMENTS } = require('../config/globals'); const http = require('http'); const https = require('https'); const path = require('path'); @@ -7,7 +7,6 @@ const crypto = require('crypto'); const EventManager = require('./singletons/EventManager.js'); const GameManager = require('./singletons/GameManager.js'); const TimerManager = require('./singletons/TimerManager.js'); -const { ENVIRONMENT } = require('../config/globals.js'); const rateLimit = require('express-rate-limit').default; const ServerBootstrapper = { @@ -17,8 +16,8 @@ const ServerBootstrapper = { timerManager: new TimerManager(logger, instanceId), eventManager: new EventManager(logger, instanceId), gameManager: process.env.NODE_ENV.trim() === 'development' - ? new GameManager(logger, ENVIRONMENT.LOCAL, instanceId) - : new GameManager(logger, ENVIRONMENT.PRODUCTION, instanceId) + ? new GameManager(logger, ENVIRONMENTS.LOCAL, instanceId) + : new GameManager(logger, ENVIRONMENTS.PRODUCTION, instanceId) }; }, @@ -114,7 +113,7 @@ const ServerBootstrapper = { }); // API endpoints - app.use('/api/games', standardRateLimit, require('../api/GamesAPI')); + app.use('/api/games', standardRateLimit, require('../api/RoomsAPI')); app.use('/api/admin', (req, res, next) => { if (isAuthorized(req)) { next(); diff --git a/server/modules/singletons/EventManager.js b/server/modules/singletons/EventManager.js index 951be2e..fd9576a 100644 --- a/server/modules/singletons/EventManager.js +++ b/server/modules/singletons/EventManager.js @@ -1,7 +1,7 @@ -const globals = require('../../config/globals'); const { RateLimiterMemory } = require('rate-limiter-flexible'); const redis = require('redis'); const Events = require('../Events'); +const { EVENT_IDS, REDIS_CHANNELS, PRIMITIVES, SOCKET_EVENTS, TIMER_EVENTS, SYNCABLE_EVENTS } = require('../../config/globals'); class EventManager { constructor (logger, instanceId) { @@ -20,7 +20,7 @@ class EventManager { } broadcast = (message) => { - this.io?.emit(globals.EVENTS.BROADCAST, message); + this.io?.emit(EVENT_IDS.BROADCAST, message); }; createRedisPublisher = async () => { @@ -53,7 +53,7 @@ class EventManager { throw new Error('UNABLE TO CONNECT TO REDIS because: ' + e); } - await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => { + await this.subscriber.subscribe(REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => { this.logger.debug('MESSAGE: ' + message); let messageComponents, args; try { @@ -64,7 +64,7 @@ class EventManager { } args = JSON.parse( message.slice( - message.indexOf(messageComponents[messageComponents.length - 1]) + (globals.INSTANCE_ID_LENGTH + 1) + message.indexOf(messageComponents[messageComponents.length - 1]) + (PRIMITIVES.INSTANCE_ID_LENGTH + 1) ) ); } catch (e) { @@ -133,12 +133,12 @@ class EventManager { }; registerSocketHandler = (namespace, socket, gameManager) => { - socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => { + socket.on(SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => { const game = await gameManager.getActiveGame(accessCode); if (game) { - if (globals.TIMER_EVENTS().includes(eventId)) { + if (TIMER_EVENTS().includes(eventId)) { await this.handleEventById( - globals.EVENT_IDS.TIMER_EVENT, + EVENT_IDS.TIMER_EVENT, null, game, socket.id, @@ -160,10 +160,10 @@ class EventManager { handleAndSyncSocketEvent = async (eventId, game, socket, socketArgs, ackFn) => { await this.handleEventById(eventId, null, 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)) { + if (SYNCABLE_EVENTS().includes(eventId)) { await this.gameManager.refreshGame(game); await this.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, this.createMessageToPublish(game.accessCode, eventId, this.instanceId, JSON.stringify(socketArgs)) ); } @@ -194,7 +194,7 @@ class EventManager { timerEventSubtype: timerEventSubtype }; if (event) { - if (!syncOnly || eventId === globals.EVENT_IDS.RESTART_GAME) { + if (!syncOnly || eventId === EVENT_IDS.RESTART_GAME) { await event.stateChange(game, socketArgs, additionalVars); } await event.communicate(game, socketArgs, additionalVars); diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index f9b0a2b..bed0f40 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -1,4 +1,12 @@ -const globals = require('../../config/globals'); +const { + STATUS, + PRIMITIVES, + ERROR_MESSAGES, + GAME_PROCESS_COMMANDS, + USER_TYPES, + EVENT_IDS, + REDIS_CHANNELS +} = require('../../config/globals'); const Game = require('../../model/Game'); const Person = require('../../model/Person'); const GameStateCurator = require('../GameStateCurator'); @@ -62,9 +70,9 @@ class GameManager { gameParams.hasDedicatedModerator, gameParams.isTestGame ); - const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); + const newAccessCode = await this.generateAccessCode(PRIMITIVES.ACCESS_CODE_CHAR_POOL); if (newAccessCode === null) { - return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); + return Promise.reject(ERROR_MESSAGES.NO_UNIQUE_ACCESS_CODE); } const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator); moderator.assigned = true; @@ -75,7 +83,7 @@ class GameManager { } const newGame = new Game( newAccessCode, - globals.STATUS.LOBBY, + STATUS.LOBBY, null, req.deck, req.hasTimer, @@ -88,7 +96,7 @@ class GameManager { ); newGame.people = initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame, newGame.gameSize); await this.eventManager.publisher.set(newAccessCode, JSON.stringify(newGame), { - EX: globals.STALE_GAME_SECONDS + EX: PRIMITIVES.STALE_GAME_SECONDS }); return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment }); }).catch((message) => { @@ -103,7 +111,7 @@ class GameManager { if (thread && !thread.killed) { this.logger.debug('Timer thread found for game ' + game.accessCode); thread.send({ - command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, + command: GAME_PROCESS_COMMANDS.PAUSE_TIMER, accessCode: game.accessCode, logLevel: this.logger.logLevel }); @@ -115,7 +123,7 @@ class GameManager { if (thread && !thread.killed) { this.logger.debug('Timer thread found for game ' + game.accessCode); thread.send({ - command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, + command: GAME_PROCESS_COMMANDS.RESUME_TIMER, accessCode: game.accessCode, logLevel: this.logger.logLevel }); @@ -127,14 +135,14 @@ class GameManager { const thread = this.timerManager.timerThreads[game.accessCode]; if (thread && (!thread.killed && thread.exitCode === null)) { thread.send({ - command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, accessCode: game.accessCode, socketId: socketId, logLevel: this.logger.logLevel }); } else if (thread) { if (game.timerParams && game.timerParams.timeRemaining === 0) { - this.namespace.to(socketId).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + this.namespace.to(socketId).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); } } } @@ -154,9 +162,9 @@ class GameManager { let codeDigits, accessCode; let attempts = 0; while (!accessCode || ((await this.eventManager.publisher.keys('*')).includes(accessCode) - && attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) { + && attempts < PRIMITIVES.ACCESS_CODE_GENERATION_ATTEMPTS)) { codeDigits = []; - let iterations = globals.ACCESS_CODE_LENGTH; + let iterations = PRIMITIVES.ACCESS_CODE_LENGTH; while (iterations > 0) { iterations --; codeDigits.push(charPool[getRandomInt(charCount)]); @@ -178,7 +186,7 @@ class GameManager { return Promise.reject({ status: 400, reason: 'This name is taken.' }); } if (joinAsSpectator - && game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS + && game.people.filter(person => person.userType === USER_TYPES.SPECTATOR).length === PRIMITIVES.MAX_SPECTATORS ) { return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); } else if (joinAsSpectator || this.isGameStartable(game)) { @@ -196,7 +204,7 @@ class GameManager { createRandomId(), createRandomId(), name, - globals.USER_TYPES.PLAYER, + USER_TYPES.PLAYER, null, null, null, @@ -208,15 +216,15 @@ class GameManager { game.isStartable = this.isGameStartable(game); await this.refreshGame(game); this.namespace.in(game.accessCode).emit( - globals.EVENTS.PLAYER_JOINED, + EVENT_IDS.PLAYER_JOINED, GameStateCurator.mapPerson(moderator || newPlayer), game.isStartable ); await this.eventManager.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, this.eventManager.createMessageToPublish( game.accessCode, - globals.EVENT_IDS.PLAYER_JOINED, + EVENT_IDS.PLAYER_JOINED, this.instanceId, JSON.stringify(moderator || newPlayer) ) @@ -237,7 +245,7 @@ class GameManager { return cards; } - restartGame = async (game, namespace, status = globals.STATUS.IN_PROGRESS) => { + restartGame = async (game, namespace) => { // kill any outstanding timer threads const subProcess = this.timerManager.timerThreads[game.accessCode]; if (subProcess) { @@ -250,16 +258,16 @@ class GameManager { } for (let i = 0; i < game.people.length; i ++) { - if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) { - game.people[i].userType = globals.USER_TYPES.PLAYER; + if (game.people[i].userType === USER_TYPES.KILLED_PLAYER) { + game.people[i].userType = USER_TYPES.PLAYER; game.people[i].out = false; } - if (game.people[i].userType === globals.USER_TYPES.KILLED_BOT) { - game.people[i].userType = globals.USER_TYPES.BOT; + if (game.people[i].userType === USER_TYPES.KILLED_BOT) { + game.people[i].userType = USER_TYPES.BOT; game.people[i].out = false; } - if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) { - game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR; + if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === USER_TYPES.MODERATOR) { + game.people[i].userType = USER_TYPES.TEMPORARY_MODERATOR; game.people[i].out = false; } game.people[i].revealed = false; @@ -270,19 +278,19 @@ class GameManager { game.people[i].customRole = null; } - game.status = globals.STATUS.LOBBY; + game.status = STATUS.LOBBY; await this.refreshGame(game); await this.eventManager.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, this.eventManager.createMessageToPublish( game.accessCode, - globals.EVENT_IDS.RESTART_GAME, + EVENT_IDS.RESTART_GAME, this.instanceId, '{}' ) ); - namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME); + namespace.in(game.accessCode).emit(EVENT_IDS.RESTART_GAME); }; /* @@ -305,9 +313,9 @@ class GameManager { deal = (game) => { const cards = this.prepareDeck(game.deck); let i = 0; - for (const person of game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER - || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR - || person.userType === globals.USER_TYPES.BOT) + for (const person of game.people.filter(person => person.userType === USER_TYPES.PLAYER + || person.userType === USER_TYPES.TEMPORARY_MODERATOR + || person.userType === USER_TYPES.BOT) ) { person.gameRole = cards[i].role; person.customRole = cards[i].custom; @@ -318,9 +326,9 @@ class GameManager { } isGameStartable = (game) => { - return game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER - || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR - || person.userType === globals.USER_TYPES.BOT).length === game.gameSize; + return game.people.filter(person => person.userType === USER_TYPES.PLAYER + || person.userType === USER_TYPES.TEMPORARY_MODERATOR + || person.userType === USER_TYPES.BOT).length === game.gameSize; } findPersonByField = (game, fieldName, value) => { @@ -334,8 +342,8 @@ function getRandomInt (max) { function initializeModerator (name, hasDedicatedModerator) { const userType = hasDedicatedModerator - ? globals.USER_TYPES.MODERATOR - : globals.USER_TYPES.TEMPORARY_MODERATOR; + ? USER_TYPES.MODERATOR + : USER_TYPES.TEMPORARY_MODERATOR; return new Person(createRandomId(), createRandomId(), name, userType); } @@ -343,7 +351,7 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g const people = []; if (isTestGame) { let j = 0; - const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + const number = moderator.userType === USER_TYPES.TEMPORARY_MODERATOR ? gameSize - 1 : gameSize; while (j < number) { @@ -351,7 +359,7 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g createRandomId(), createRandomId(), UsernameGenerator.generate(), - globals.USER_TYPES.BOT, + USER_TYPES.BOT, null, null, null, @@ -369,8 +377,8 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g function createRandomId () { let id = ''; - 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)]; + for (let i = 0; i < PRIMITIVES.INSTANCE_ID_LENGTH; i ++) { + id += PRIMITIVES.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.INSTANCE_ID_CHAR_POOL.length)]; } return id; } @@ -385,21 +393,21 @@ async function addSpectator (game, name, logger, namespace, eventManager, instan createRandomId(), createRandomId(), name, - globals.USER_TYPES.SPECTATOR + USER_TYPES.SPECTATOR ); spectator.assigned = true; logger.trace('new spectator: ' + spectator.name); game.people.push(spectator); await refreshGame(game); namespace.in(game.accessCode).emit( - globals.EVENT_IDS.ADD_SPECTATOR, + EVENT_IDS.ADD_SPECTATOR, GameStateCurator.mapPerson(spectator) ); await eventManager.publisher.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, eventManager.createMessageToPublish( game.accessCode, - globals.EVENT_IDS.ADD_SPECTATOR, + EVENT_IDS.ADD_SPECTATOR, instanceId, JSON.stringify(GameStateCurator.mapPerson(spectator)) ) diff --git a/server/modules/singletons/TimerManager.js b/server/modules/singletons/TimerManager.js index 076b638..e216f78 100644 --- a/server/modules/singletons/TimerManager.js +++ b/server/modules/singletons/TimerManager.js @@ -1,6 +1,6 @@ const { fork } = require('child_process'); const path = require('path'); -const globals = require('../../config/globals'); +const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS } = require('../../config/globals'); class TimerManager { constructor (logger, instanceId) { @@ -25,7 +25,7 @@ class TimerManager { await eventManager.handleEventById(msg.command, null, game, msg.socketId, game.accessCode, msg, null, false); await gameManager.refreshGame(game); await eventManager.publisher.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + REDIS_CHANNELS.ACTIVE_GAME_STREAM, eventManager.createMessageToPublish(game.accessCode, msg.command, this.instanceId, JSON.stringify(msg)) ); }); @@ -34,7 +34,7 @@ class TimerManager { this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode); }); gameProcess.send({ - command: globals.GAME_PROCESS_COMMANDS.START_TIMER, + command: GAME_PROCESS_COMMANDS.START_TIMER, accessCode: game.accessCode, logLevel: this.logger.logLevel, hours: game.timerParams.hours, diff --git a/spec/e2e/game_spec.js b/spec/e2e/game_spec.js index 8774def..f089e5a 100644 --- a/spec/e2e/game_spec.js +++ b/spec/e2e/game_spec.js @@ -1,52 +1,51 @@ import { gameHandler } from '../../client/src/modules/page_handlers/gameHandler.js'; import { mockGames } from '../support/MockGames.js'; import gameTemplate from '../../client/src/view_templates/GameTemplate.js'; -import { globals } from '../../client/src/config/globals.js'; +import { + EVENT_IDS, + SOCKET_EVENTS, + USER_TYPE_ICONS, + USER_TYPES +} from '../../client/src/config/globals.js'; describe('game page', () => { - const XHRUtility = { - xhr (url, method = 'GET', headers, body = null) { - switch (url) { - case '/api/games/environment': - return new Promise((resolve, reject) => { - resolve({ content: 'production' }); - }); - } + const mockSocket = { + eventHandlers: {}, + on: function (message, handler) { + this.eventHandlers[message] = handler; + }, + once: function (message, handler) { + this.eventHandlers[message] = handler; + }, + timeout: (duration) => { + return mockSocket; + }, + removeAllListeners: function (...names) { + + }, + hasListeners: function (listener) { + return false; } }; + const window = { location: { href: 'host/game/ABCD' }, fetch: () => {} }; + + beforeEach(async () => { + document.body.innerHTML = ''; + const response = new Response('production', { status: 200, statusText: 'OK' }); + spyOn(window, 'fetch').and.resolveTo(response); + }); describe('lobby game - moderator view', () => { - let mockSocket; - - beforeEach(async function () { - document.body.innerHTML = ''; - mockSocket = { - eventHandlers: {}, - on: function (message, handler) { - this.eventHandlers[message] = handler; - }, - once: function (message, handler) { - this.eventHandlers[message] = handler; - }, - timeout: (duration) => { - return mockSocket; - }, - emit: function (eventName, ...args) { - switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type - case globals.EVENT_IDS.FETCH_GAME_STATE: - args[args.length - 1](deepCopy(mockGames.gameInLobbyAsModerator)); // copy the game object to prevent leaking of state between specs - } - }, - removeAllListeners: function (...names) { - - }, - hasListeners: function (listener) { - return false; + beforeEach(async () => { + mockSocket.emit = function (eventName, ...args) { + switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type + case EVENT_IDS.FETCH_GAME_STATE: + args[args.length - 1](deepCopy(mockGames.gameInLobbyAsModerator)); // copy the game object to prevent leaking of state between specs } }; - await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); + spyOn(mockSocket, 'emit').and.callThrough(); + await gameHandler(mockSocket, window, gameTemplate); mockSocket.eventHandlers.connect(); - spyOn(mockSocket, 'emit'); }); it('should display the connected client', () => { @@ -59,10 +58,10 @@ describe('game page', () => { }); it('should display a new player when they join', () => { - mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({ + mockSocket.eventHandlers[EVENT_IDS.PLAYER_JOINED]({ name: 'Jane', id: '123', - userType: globals.USER_TYPES.PLAYER, + userType: USER_TYPES.PLAYER, out: false, revealed: false }, false); @@ -83,8 +82,8 @@ describe('game page', () => { document.getElementById('save-role-changes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.UPDATE_GAME_ROLES, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.UPDATE_GAME_ROLES, mockGames.gameInLobbyAsModerator.accessCode, jasmine.any(Object), jasmine.any(Function) @@ -97,42 +96,21 @@ describe('game page', () => { }); describe('lobby game - player view', () => { - let mockSocket; - - beforeEach(async function () { - document.body.innerHTML = ''; - mockSocket = { - eventHandlers: {}, - on: function (message, handler) { - this.eventHandlers[message] = handler; - }, - once: function (message, handler) { - this.eventHandlers[message] = handler; - }, - timeout: (duration) => { - return mockSocket; - }, - emit: function (eventName, ...args) { - switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type - case globals.EVENT_IDS.FETCH_GAME_STATE: - args[args.length - 1](deepCopy(mockGames.gameInLobbyAsPlayer)); // copy the game object to prevent leaking of state between specs - } - }, - removeAllListeners: function (...names) { - - }, - hasListeners: function (listener) { - return false; + beforeEach(async () => { + mockSocket.emit = function (eventName, ...args) { + switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type + case EVENT_IDS.FETCH_GAME_STATE: + args[args.length - 1](deepCopy(mockGames.gameInLobbyAsPlayer)); // copy the game object to prevent leaking of state between specs } }; - await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); + spyOn(mockSocket, 'emit').and.callThrough(); + await gameHandler(mockSocket, window, gameTemplate); mockSocket.eventHandlers.connect(); - spyOn(mockSocket, 'emit'); }); it('should display the connected client', () => { expect(document.getElementById('client-name').innerText).toEqual('Lys'); - expect(document.getElementById('client-user-type').innerText).toEqual('player' + globals.USER_TYPE_ICONS.player); + expect(document.getElementById('client-user-type').innerText).toEqual('player' + USER_TYPE_ICONS.player); }); it('should display the QR Code', () => { @@ -144,18 +122,18 @@ describe('game page', () => { document.getElementById('leave-game-button').click(); document.getElementById('confirmation-yes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.LEAVE_ROOM, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.LEAVE_ROOM, mockGames.gameInLobbyAsModerator.accessCode, { personId: mockGames.gameInLobbyAsPlayer.client.id } ); }); it('should display a new player when they join', () => { - mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({ + mockSocket.eventHandlers[EVENT_IDS.PLAYER_JOINED]({ name: 'Jane', id: '123', - userType: globals.USER_TYPES.PLAYER, + userType: USER_TYPES.PLAYER, out: false, revealed: false }, false); @@ -169,40 +147,20 @@ describe('game page', () => { }); describe('in-progress game - player view', () => { - let mockSocket; - beforeEach(async () => { - document.body.innerHTML = ''; - mockSocket = { - eventHandlers: {}, - on: function (message, handler) { - this.eventHandlers[message] = handler; - }, - timeout: (duration) => { - return mockSocket; - }, - emit: function (eventName, ...args) { - switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type - case globals.EVENT_IDS.FETCH_GAME_STATE: - args[args.length - 1](deepCopy(mockGames.inProgressGame)); // copy the game object to prevent leaking of state between specs - break; - default: - break; - } - }, - hasListeners: function (listener) { - return false; - }, - removeAllListeners: function (...names) { - - }, - once: function (message, handler) { - this.eventHandlers[message] = handler; + mockSocket.emit = function (eventName, ...args) { + switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type + case EVENT_IDS.FETCH_GAME_STATE: + args[args.length - 1](deepCopy(mockGames.inProgressGame)); // copy the game object to prevent leaking of state between specs + break; + default: + break; } }; - await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); + spyOn(mockSocket, 'emit').and.callThrough(); + await gameHandler(mockSocket, window, gameTemplate); mockSocket.eventHandlers.connect(); - mockSocket.eventHandlers.getTimeRemaining(120000, true); + await mockSocket.eventHandlers.getTimeRemaining(120000, true); }); it('should display the game role of the client', () => { @@ -246,44 +204,24 @@ describe('game page', () => { }); describe('in-progress game - moderator view', () => { - let mockSocket; - beforeEach(async () => { document.body.innerHTML = ''; - mockSocket = { - eventHandlers: {}, - on: function (message, handler) { - this.eventHandlers[message] = handler; - }, - timeout: (duration) => { - return mockSocket; - }, - once: function (message, handler) { - this.eventHandlers[message] = handler; - }, - emit: function (eventName, ...args) { - switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type - case globals.EVENT_IDS.FETCH_GAME_STATE: - args[args.length - 1](deepCopy(mockGames.moderatorGame)); // copy the game object to prevent leaking of state between specs - break; - case globals.EVENT_IDS.END_GAME: - args[args.length - 1](); - break; - default: - break; - } - }, - hasListeners: function (listener) { - return false; - }, - removeAllListeners: function (...names) { - + mockSocket.emit = function (eventName, ...args) { + switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type + case EVENT_IDS.FETCH_GAME_STATE: + args[args.length - 1](deepCopy(mockGames.moderatorGame)); // copy the game object to prevent leaking of state between specs + break; + case EVENT_IDS.END_GAME: + args[args.length - 1](); + break; + default: + break; } }; - await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); + spyOn(mockSocket, 'emit').and.callThrough(); + await gameHandler(mockSocket, window, gameTemplate); mockSocket.eventHandlers.connect(); - mockSocket.eventHandlers.getTimeRemaining(120000, true); - spyOn(mockSocket, 'emit'); + await mockSocket.eventHandlers.getTimeRemaining(120000, true); }); it('should display the button to play/pause the timer', () => { @@ -317,14 +255,14 @@ describe('game page', () => { .querySelector('.kill-player-button').click(); document.getElementById('confirmation-yes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.KILL_PLAYER, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.KILL_PLAYER, mockGames.moderatorGame.accessCode, { personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' } ); mockSocket.eventHandlers.killPlayer({ id: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW', - userType: globals.USER_TYPES.KILLED_PLAYER, + userType: USER_TYPES.KILLED_PLAYER, out: true, killed: true, revealed: false, @@ -339,8 +277,8 @@ describe('game page', () => { .querySelector('.reveal-role-button').click(); document.getElementById('confirmation-yes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.REVEAL_PLAYER, + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.REVEAL_PLAYER, mockGames.moderatorGame.accessCode, { personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' } ); diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index 6ce58ec..939f104 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -1,9 +1,6 @@ // TODO: clean up these deep relative paths? jsconfig.json is not working... const Game = require('../../../../server/model/Game'); -const globals = require('../../../../server/config/globals'); -const EVENT_IDS = globals.EVENT_IDS; -const USER_TYPES = globals.USER_TYPES; -const STATUS = globals.STATUS; +const { ENVIRONMENTS, EVENT_IDS, USER_TYPES, STATUS } = require('../../../../server/config/globals.js'); const GameManager = require('../../../../server/modules/singletons/GameManager.js'); const TimerManager = require('../../../../server/modules/singletons/TimerManager.js'); const EventManager = require('../../../../server/modules/singletons/EventManager.js'); @@ -21,7 +18,7 @@ describe('Events', () => { const toObj = { emit: () => {} }; namespace = { in: () => { return inObj; }, to: () => { return toObj; }, sockets: new Map() }; socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; - gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test'); + gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, ENVIRONMENTS.PRODUCTION, 'test'); timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test'); gameManager.setGameSocketNamespace(namespace); @@ -83,7 +80,7 @@ describe('Events', () => { .communicate(game, { id: 'd', assigned: true, userType: USER_TYPES.PLAYER }, { gameManager: gameManager }); expect(namespace.in).toHaveBeenCalledWith(game.accessCode); expect(namespace.in().emit).toHaveBeenCalledWith( - globals.EVENTS.PLAYER_JOINED, + EVENT_IDS.PLAYER_JOINED, GameStateCurator.mapPerson({ id: 'd', assigned: true, userType: USER_TYPES.PLAYER }), game.isStartable );