From 3b14ae3978eed05ecae273f9c59b30a3986099a6 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Mon, 10 Jan 2022 21:02:29 -0500 Subject: [PATCH] clean up state management --- client/src/config/globals.js | 7 +- client/src/modules/GameStateRenderer.js | 32 ++-- client/src/modules/Navbar.js | 2 +- client/src/modules/StateBucket.js | 3 +- client/src/modules/Templates.js | 4 +- client/src/scripts/game.js | 229 ++++++++++++++++-------- client/src/scripts/howToUse.js | 9 + client/src/styles/GLOBAL.css | 10 ++ client/src/views/how-to-use.html | 47 +++++ client/webpack/webpack-dev.config.js | 3 +- package.json | 2 +- server/config/globals.js | 4 +- server/main.js | 4 + server/modules/GameManager.js | 73 ++++++-- server/modules/GameStateCurator.js | 45 +++-- server/routes/router.js | 4 + 16 files changed, 349 insertions(+), 129 deletions(-) create mode 100644 client/src/scripts/howToUse.js create mode 100644 client/src/views/how-to-use.html diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 47b16e9..e50b2f7 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -18,7 +18,8 @@ export const globals = { REVEAL_PLAYER: 'revealPlayer', TRANSFER_MODERATOR: 'transferModerator', CHANGE_NAME: 'changeName', - END_GAME: 'endGame' + END_GAME: 'endGame', + FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState' }, STATUS: { LOBBY: "lobby", @@ -38,7 +39,9 @@ export const globals = { START_TIMER: "startTimer", KILL_PLAYER: "killPlayer", REVEAL_PLAYER: 'revealPlayer', - CHANGE_NAME: 'changeName' + CHANGE_NAME: 'changeName', + START_GAME: 'startGame', + PLAYER_LEFT: 'playerLeft' }, USER_TYPES: { MODERATOR: "moderator", diff --git a/client/src/modules/GameStateRenderer.js b/client/src/modules/GameStateRenderer.js index ad16d30..3b95c6d 100644 --- a/client/src/modules/GameStateRenderer.js +++ b/client/src/modules/GameStateRenderer.js @@ -10,6 +10,12 @@ export class GameStateRenderer { this.killPlayerHandlers = {}; this.revealRoleHandlers = {}; this.transferModHandlers = {}; + this.startGameHandler = (e) => { + e.preventDefault(); + if (confirm("Start the game and deal roles?")) { + socket.emit(globals.COMMANDS.START_GAME, this.stateBucket.currentGameState.accessCode); + } + } } renderLobbyPlayers() { @@ -465,16 +471,18 @@ function removeExistingPlayerElements(killPlayerHandlers, revealRoleHandlers) { } function createEndGamePromptComponent(socket, stateBucket) { - let div = document.createElement("div"); - div.innerHTML = templates.END_GAME_PROMPT; - div.querySelector("#end-game-button").addEventListener('click', (e) => { - e.preventDefault(); - if (confirm("End the game?")) { - socket.emit( - globals.COMMANDS.END_GAME, - stateBucket.currentGameState.accessCode - ); - } - }); - document.getElementById("game-content").appendChild(div); + if (document.querySelector("#end-game-prompt") === null) { + let div = document.createElement("div"); + div.innerHTML = templates.END_GAME_PROMPT; + div.querySelector("#end-game-button").addEventListener('click', (e) => { + e.preventDefault(); + if (confirm("End the game?")) { + socket.emit( + globals.COMMANDS.END_GAME, + stateBucket.currentGameState.accessCode + ); + } + }); + document.getElementById("game-content").appendChild(div); + } } diff --git a/client/src/modules/Navbar.js b/client/src/modules/Navbar.js index 2d3472e..c22411a 100644 --- a/client/src/modules/Navbar.js +++ b/client/src/modules/Navbar.js @@ -42,7 +42,7 @@ function getNavbarLinks (page=null, device) { '' + 'Home' + 'Create' + - 'How to Use' + + 'How to Use' + 'Contact' + 'Support the App' } diff --git a/client/src/modules/StateBucket.js b/client/src/modules/StateBucket.js index 7571fc2..fa71c56 100644 --- a/client/src/modules/StateBucket.js +++ b/client/src/modules/StateBucket.js @@ -4,5 +4,6 @@ */ export const stateBucket = { currentGameState: null, - timerWorker: null + timerWorker: null, + gameStateRequestInFlight: false } diff --git a/client/src/modules/Templates.js b/client/src/modules/Templates.js index f3bd3b1..4fe03b8 100644 --- a/client/src/modules/Templates.js +++ b/client/src/modules/Templates.js @@ -52,7 +52,7 @@ export const templates = { "

" + "" + "
" + - "

Click to reveal your role

" + + "

Click to show your role

" + "

(click again to hide)

" + "
" + "
" + @@ -138,7 +138,7 @@ export const templates = { "

" + "
" + "
" + - "

Click to reveal your role

" + + "

Click to show your role

" + "

(click again to hide)

" + "
" + "
" + diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 9f75c87..299f199 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -13,16 +13,21 @@ const game = () => { injectNavbar(); let timerWorker; const socket = io('/in-game'); + stateBucket.gameStateRequestInFlight = false; socket.on('disconnect', () => { + stateBucket.gameStateRequestInFlight = false; if (timerWorker) { timerWorker.terminate(); } toast('Disconnected. Attempting reconnect...', 'error', true, false); }); socket.on('connect', () => { - socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function(returnedEnvironment) { - timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url)); - prepareGamePage(returnedEnvironment, socket, timerWorker); + console.log('fired connect event'); + socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function (returnedEnvironment) { + if (!stateBucket.gameStateRequestInFlight) { + timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url)); + prepareGamePage(returnedEnvironment, socket, timerWorker); + } }); }) }; @@ -32,46 +37,51 @@ function prepareGamePage(environment, socket, timerWorker) { const splitUrl = window.location.href.split('/game/'); const accessCode = splitUrl[1]; if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { + stateBucket.gameStateRequestInFlight = true; socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) { + stateBucket.gameStateRequestInFlight = false; stateBucket.currentGameState = gameState; document.querySelector('.spinner-container')?.remove(); document.querySelector('.spinner-background')?.remove(); if (gameState === null) { window.location = '/not-found?reason=' + encodeURIComponent('game-not-found'); - } + } else { + document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM; + toast('You are connected.', 'success', true, true, 2); + userId = gameState.client.cookie; + UserUtility.setAnonymousUserId(userId, environment); + let gameStateRenderer = new GameStateRenderer(stateBucket, socket); + let gameTimerManager; + if (stateBucket.currentGameState.timerParams) { + gameTimerManager = new GameTimerManager(stateBucket, socket); + } + initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager); - document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM; - toast('You are connected.', 'success', true, true, 2); - userId = gameState.client.cookie; - UserUtility.setAnonymousUserId(userId, environment); - let gameStateRenderer = new GameStateRenderer(stateBucket, socket); - let gameTimerManager; - if (stateBucket.currentGameState.timerParams) { - gameTimerManager = new GameTimerManager(stateBucket, socket); - } - initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager); - - if (!gameState.client.hasEnteredName) { - document.getElementById("prompt").innerHTML = templates.NAME_CHANGE_MODAL; - document.getElementById("change-name-form").onsubmit = (e) => { - e.preventDefault(); - let name = document.getElementById("player-new-name").value; - if (validateName(name)) { - socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, { name: name, personId: gameState.client.id }, (result) => { - switch (result) { - case "taken": - toast('This name is already taken.', 'error', true, true, 8); - break; - case "changed": - ModalManager.dispelModal("change-name-modal", "change-name-modal-background") - toast('Name set.', 'success', true, true, 5); - propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id); - processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker); - } - }) - } else { - toast("Name must be between 1 and 30 characters.", 'error', true, true, 8); + if (!gameState.client.hasEnteredName) { + document.getElementById("prompt").innerHTML = templates.NAME_CHANGE_MODAL; + document.getElementById("change-name-form").onsubmit = (e) => { + e.preventDefault(); + let name = document.getElementById("player-new-name").value; + if (validateName(name)) { + socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, { + name: name, + personId: gameState.client.id + }, (result) => { + switch (result) { + case "taken": + toast('This name is already taken.', 'error', true, true, 8); + break; + case "changed": + ModalManager.dispelModal("change-name-modal", "change-name-modal-background") + toast('Name set.', 'success', true, true, 5); + propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id); + processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker); + } + }) + } else { + toast("Name must be between 1 and 30 characters.", 'error', true, true, 8); + } } } } @@ -86,10 +96,12 @@ function initializeGame(stateBucket, socket, timerWorker, userId, gameStateRende processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker); } -function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker) { +function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker, refreshPrompt=true) { displayClientInfo(currentGameState.client.name, currentGameState.client.userType); - document.querySelector("#start-game-prompt")?.remove(); - document.querySelector("#end-game-prompt")?.remove(); + if (refreshPrompt) { + removeStartGameFunctionalityIfPresent(gameStateRenderer); + document.querySelector("#end-game-prompt")?.remove(); + } switch (currentGameState.status) { case globals.STATUS.LOBBY: document.getElementById("game-state-container").innerHTML = templates.LOBBY; @@ -101,8 +113,9 @@ function processGameState (currentGameState, userId, socket, gameStateRenderer, currentGameState.client.userType === globals.USER_TYPES.MODERATOR || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR ) + && refreshPrompt ) { - displayStartGamePromptForModerators(currentGameState, socket); + displayStartGamePromptForModerators(currentGameState, gameStateRenderer); } break; case globals.STATUS.IN_PROGRESS: @@ -174,21 +187,69 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo || stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR ) ) { - displayStartGamePromptForModerators(stateBucket.currentGameState, socket); + displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer); } }); } + + if (!socket.hasListeners(globals.EVENTS.PLAYER_LEFT)) { + socket.on(globals.EVENTS.PLAYER_LEFT, (player) => { + removeStartGameFunctionalityIfPresent(gameStateRenderer); + toast(player.name + " has left!", "error", false, true, 3); + let index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id); + if (index >= 0) { + stateBucket.currentGameState.people.splice( + index, + 1 + ); + gameStateRenderer.renderLobbyPlayers(); + } + }); + } + + if (!socket.hasListeners(globals.EVENTS.START_GAME)) { + socket.on(globals.EVENTS.START_GAME, () => { + socket.emit( + globals.COMMANDS.FETCH_IN_PROGRESS_STATE, + stateBucket.currentGameState.accessCode, + stateBucket.currentGameState.client.cookie, + function (gameState) { + stateBucket.gameStateRequestInFlight = false; + stateBucket.currentGameState = gameState; + processGameState( + stateBucket.currentGameState, + gameState.client.cookie, + socket, + gameStateRenderer, + gameTimerManager, + timerWorker + ); + } + ); + }); + } if (!socket.hasListeners(globals.EVENTS.SYNC_GAME_STATE)) { socket.on(globals.EVENTS.SYNC_GAME_STATE, () => { - socket.emit( - globals.COMMANDS.FETCH_GAME_STATE, - stateBucket.currentGameState.accessCode, - stateBucket.currentGameState.client.cookie, - function (gameState) { - stateBucket.currentGameState = gameState; - processGameState(stateBucket.currentGameState, gameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker); - } - ); + if (!stateBucket.gameStateRequestInFlight) { + stateBucket.gameStateRequestInFlight = true; + socket.emit( + globals.COMMANDS.FETCH_IN_PROGRESS_STATE, + stateBucket.currentGameState.accessCode, + stateBucket.currentGameState.client.cookie, + function (gameState) { + stateBucket.gameStateRequestInFlight = false; + stateBucket.currentGameState = gameState; + processGameState( + stateBucket.currentGameState, + gameState.client.cookie, + socket, + gameStateRenderer, + gameTimerManager, + timerWorker + ); + } + ); + } }); } @@ -255,7 +316,15 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => { propagateNameChange(stateBucket.currentGameState, name, personId); updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer); - processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker); + processGameState( + stateBucket.currentGameState, + stateBucket.currentGameState.client.cookie, + socket, + gameStateRenderer, + gameTimerManager, + timerWorker, + false + ); }); } @@ -263,21 +332,22 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo 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, gameStateRenderer, gameTimerManager, timerWorker); + processGameState( + stateBucket.currentGameState, + stateBucket.currentGameState.client.cookie, + socket, + gameStateRenderer, + gameTimerManager, + timerWorker + ); }); } } -function displayStartGamePromptForModerators(gameState, socket) { +function displayStartGamePromptForModerators(gameState, gameStateRenderer) { let div = document.createElement("div"); div.innerHTML = templates.START_GAME_PROMPT; - div.querySelector('#start-game-button').addEventListener('click', (e) => { - e.preventDefault(); - if (confirm("Start the game and deal roles?")) { - socket.emit(globals.COMMANDS.START_GAME, gameState.accessCode); - } - - }); + div.querySelector('#start-game-button').addEventListener('click', gameStateRenderer.startGameHandler); document.body.appendChild(div); } @@ -296,8 +366,15 @@ function validateName(name) { return typeof name === 'string' && name.length > 0 && name.length <= 30; } +function removeStartGameFunctionalityIfPresent(gameStateRenderer) { + document.querySelector("#start-game-prompt")?.removeEventListener('click', gameStateRenderer.startGameHandler); + document.querySelector("#start-game-prompt")?.remove(); +} + function propagateNameChange(gameState, name, personId) { - gameState.client.name = name; + if (gameState.client.id === personId) { + gameState.client.name = name; + } let matchingPerson = gameState.people.find((person) => person.id === personId); if (matchingPerson) { matchingPerson.name = name; @@ -314,20 +391,24 @@ function propagateNameChange(gameState, name, personId) { } function updateDOMWithNameChange(gameState, gameStateRenderer) { - switch (gameState.client.userType) { - case globals.USER_TYPES.PLAYER: - case globals.USER_TYPES.KILLED_PLAYER: - case globals.USER_TYPES.SPECTATOR: - gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false); - break; - case globals.USER_TYPES.MODERATOR: - gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED); - break; - case globals.USER_TYPES.TEMPORARY_MODERATOR: - gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true); - break; - default: - break; + if (gameState.status === globals.STATUS.IN_PROGRESS) { + switch (gameState.client.userType) { + case globals.USER_TYPES.PLAYER: + case globals.USER_TYPES.KILLED_PLAYER: + case globals.USER_TYPES.SPECTATOR: + gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false); + break; + case globals.USER_TYPES.MODERATOR: + gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED); + break; + case globals.USER_TYPES.TEMPORARY_MODERATOR: + gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true); + break; + default: + break; + } + } else { + gameStateRenderer.renderLobbyPlayers(); } } diff --git a/client/src/scripts/howToUse.js b/client/src/scripts/howToUse.js new file mode 100644 index 0000000..ee25480 --- /dev/null +++ b/client/src/scripts/howToUse.js @@ -0,0 +1,9 @@ +import { injectNavbar } from "../modules/Navbar.js"; + +const howToUse = () => { injectNavbar(); }; + +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = howToUse; +} else { + howToUse(); +} diff --git a/client/src/styles/GLOBAL.css b/client/src/styles/GLOBAL.css index 17957de..04dbae5 100644 --- a/client/src/styles/GLOBAL.css +++ b/client/src/styles/GLOBAL.css @@ -221,6 +221,16 @@ input { z-index: 53000; } +#how-to-use-container { + color: #d7d7d7; + display: flex; + flex-direction: column; + margin: 0 auto; + width: 95%; + max-width: 70em; + line-height: 1.5; +} + #desktop-links > a:nth-child(1), #mobile-links a:nth-child(1) { margin: 0 0.5em; width: 50px; diff --git a/client/src/views/how-to-use.html b/client/src/views/how-to-use.html new file mode 100644 index 0000000..5d8d770 --- /dev/null +++ b/client/src/views/how-to-use.html @@ -0,0 +1,47 @@ + + + + + + Create A Game + + + + + + + + + + + + + + + +
+ +
+

Purpose of the Application

+
This app serves as a means of running games in a social setting where a traditional + running of the game is hindered. This might be when people are meeting virtually, and thus roles can't be handed + out in-person, or when people are in-person but don't have Werewolf cards with them. You can use a deck of regular + playing cards, but it can be difficult for players to remember which card signifies which role, especially if + you want to build a crazy game with many different roles. Even when people are together and have cards, there's + information that would be great to centralize for everyone - a timer, role descriptions, and the in/out status of + players. This app attempts to provide the utilities necessary to run Werewolf with all the different roles you want, + wherever you can access the internet. +
+

Creating a Game

+
+ Creating a game through the app is a 4-step process: +
+

Step One: Choosing method of moderation

+
+ You have two options for moderation during the game. If the moderator isn't playing, you can choose the "dedicated + moderator" option. +
+
+ + + diff --git a/client/webpack/webpack-dev.config.js b/client/webpack/webpack-dev.config.js index 07ad9c0..a68125c 100644 --- a/client/webpack/webpack-dev.config.js +++ b/client/webpack/webpack-dev.config.js @@ -5,7 +5,8 @@ module.exports = { game: './client/src/scripts/game.js', home: './client/src/scripts/home.js', create: './client/src/scripts/create.js', - notFound: './client/src/scripts/notFound.js' + notFound: './client/src/scripts/notFound.js', + howToUse: './client/src/scripts/howToUse.js' }, output: { path: path.resolve(__dirname, '../dist'), diff --git a/package.json b/package.json index 42b1d8d..463e854 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start:dev:no-hot-reload": "NODE_ENV=development && node server/main.js", "start:dev:windows": "SET NODE_ENV=development && nodemon server/main.js", "start:dev:windows:no-hot-reload": "SET NODE_ENV=development && node server/main.js", - "start": "NODE_ENV=production node server/main.js -- loglevel=debug port=8080", + "start": "NODE_ENV=production node server/main.js -- loglevel=trace port=8080", "start:windows": "SET NODE_ENV=production && node server/main.js -- loglevel=warn port=8080", "test": "jasmine" }, diff --git a/server/config/globals.js b/server/config/globals.js index 131659e..4ee35bc 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -14,7 +14,8 @@ const globals = { REVEAL_PLAYER: 'revealPlayer', TRANSFER_MODERATOR: 'transferModerator', CHANGE_NAME: 'changeName', - END_GAME: 'endGame' + END_GAME: 'endGame', + FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState' }, MESSAGES: { ENTER_NAME: "Client must enter name." @@ -38,6 +39,7 @@ const globals = { }, EVENTS: { PLAYER_JOINED: "playerJoined", + PLAYER_LEFT: "playerLeft", SYNC_GAME_STATE: "syncGameState" }, ENVIRONMENT: { diff --git a/server/main.js b/server/main.js index 94f97e5..cb742a9 100644 --- a/server/main.js +++ b/server/main.js @@ -23,6 +23,10 @@ app.set('port', args.port); const inGameSocketServer = ServerBootstrapper.createSocketServer(main, app, args.port); inGameSocketServer.on('connection', function (socket) { + socket.on("disconnecting", (reason) => { + logger.trace('client socket disconnecting because: ' + reason) + gameManager.removeClientFromLobbyIfApplicable(socket); + }); gameManager.addGameSocketHandlers(inGameSocketServer, socket); }); diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index fe9dd23..6438a62 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -11,7 +11,6 @@ class GameManager { this.environment = environment; this.activeGameRunner = new ActiveGameRunner(logger).getInstance(); this.namespace = null; - //this.gameSocketUtility = GameSocketUtility; } addGameSocketHandlers = (namespace, socket) => { @@ -29,19 +28,36 @@ class GameManager { ); }); + /* this event handler will call handleRequestForGameState() with the 'handleNoMatch' arg as false - only + connections that match a participant in the game at that time will have the game state sent to them. + */ + socket.on(globals.CLIENT_COMMANDS.FETCH_IN_PROGRESS_STATE, (accessCode, personId, ackFn) => { + this.logger.trace('request for game state for accessCode ' + accessCode + ', person ' + personId); + this.handleRequestForGameState( + this.namespace, + this.logger, + this.activeGameRunner, + accessCode, + personId, + ackFn, + socket, + false + ); + }); + socket.on(globals.CLIENT_COMMANDS.GET_ENVIRONMENT, (ackFn) => { ackFn(this.environment); }); socket.on(globals.CLIENT_COMMANDS.START_GAME, (accessCode) => { let game = this.activeGameRunner.activeGames[accessCode]; - if (game) { + if (game && game.isFull) { game.status = globals.STATUS.IN_PROGRESS; - namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE); if (game.hasTimer) { game.timerParams.paused = true; this.activeGameRunner.runGame(game, namespace); } + namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.START_GAME); } }); @@ -142,7 +158,7 @@ class GameManager { ackFn("changed"); person.name = data.name.trim(); person.hasEnteredName = true; - socket.to(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE); + namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.CHANGE_NAME, person.id, person.name); } else { ackFn("taken"); } @@ -261,14 +277,14 @@ class GameManager { } - handleRequestForGameState = (namespace, logger, gameRunner, accessCode, personCookie, ackFn, socket) => { + handleRequestForGameState = (namespace, logger, gameRunner, accessCode, personCookie, ackFn, socket, handleNoMatch=true) => { const game = gameRunner.activeGames[accessCode]; if (game) { let matchingPerson = game.people.find((person) => person.cookie === personCookie); if (!matchingPerson) { matchingPerson = game.spectators.find((spectator) => spectator.cookie === personCookie); } - if (game.moderator.cookie === personCookie) { + if (!matchingPerson && game.moderator.cookie === personCookie) { matchingPerson = game.moderator; } if (matchingPerson) { @@ -281,7 +297,7 @@ class GameManager { matchingPerson.socketId = socket.id; ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger)); } - } else { + } else if (handleNoMatch) { this.handleRequestFromNonMatchingPerson(game, socket, gameRunner, ackFn, logger); } } else { @@ -291,7 +307,7 @@ class GameManager { } handleRequestFromNonMatchingPerson = (game, socket, gameRunner, ackFn, logger) => { - let personWithMatchingSocketId = findPersonWithMatchingSocketId(game.people, socket.id); + let personWithMatchingSocketId = findPersonWithMatchingSocketId(game, socket.id); if (personWithMatchingSocketId) { logger.trace("matching person found whose cookie got cleared after establishing a connection to the room: " + personWithMatchingSocketId.name); ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, personWithMatchingSocketId, gameRunner, socket, logger)); @@ -308,7 +324,7 @@ class GameManager { game.isFull = isFull; socket.to(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, - {name: unassignedPerson.name, userType: unassignedPerson.userType}, + GameStateCurator.mapPerson(unassignedPerson), isFull ); } else { // if the game is full, make them a spectator. @@ -326,6 +342,30 @@ class GameManager { } } + removeClientFromLobbyIfApplicable(socket) { + socket.rooms.forEach((room) => { + if (this.activeGameRunner.activeGames[room]) { + this.logger.trace('disconnected socket is in a game'); + let game = this.activeGameRunner.activeGames[room]; + if (game.status === globals.STATUS.LOBBY) { + let matchingPlayer = findPlayerBySocketId(game.people, socket.id); + if (matchingPlayer) { + this.logger.trace("un-assigning disconnected player: " + matchingPlayer.name); + matchingPlayer.assigned = false; + matchingPlayer.socketId = null; + matchingPlayer.cookie = createRandomId(); + matchingPlayer.hasEnteredName = false; + socket.to(game.accessCode).emit( + globals.EVENTS.PLAYER_LEFT, + GameStateCurator.mapPerson(matchingPlayer) + ); + game.isFull = isGameFull(game); + matchingPlayer.name = UsernameGenerator.generate(); + } + } + } + }) + } } function getRandomInt (max) { @@ -403,8 +443,19 @@ function rejectClientRequestForGameState(acknowledgementFunction) { return acknowledgementFunction(null); } -function findPersonWithMatchingSocketId(people, socketId) { - return people.find((person) => person.socketId === socketId); +function findPersonWithMatchingSocketId(game, socketId) { + let person = game.people.find((person) => person.socketId === socketId); + if (!person) { + person = game.spectators.find((spectator) => spectator.socketId === socketId); + } + if (!person && game.moderator.socketId === socketId) { + person = game.moderator; + } + return person; +} + +function findPlayerBySocketId(people, socketId) { + return people.find((person) => person.socketId === socketId && person.userType === globals.USER_TYPES.PLAYER); } function isGameFull(game) { diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index b7f4fa6..74859f7 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -24,6 +24,21 @@ const GameStateCurator = { out: person.out, revealed: person.revealed })); + }, + mapPerson: (person) => { + if (person.revealed) { + return { + name: person.name, + id: person.id, + userType: person.userType, + out: person.out, + revealed: person.revealed, + gameRole: person.gameRole, + alignment: person.alignment + }; + } else { + return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed }; + } } } @@ -48,7 +63,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { let state = { accessCode: game.accessCode, status: game.status, - moderator: mapPerson(game.moderator), + moderator: GameStateCurator.mapPerson(game.moderator), client: client, deck: game.deck, people: game.people @@ -56,7 +71,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { return person.assigned === true }) .map((filteredPerson) => - mapPerson(filteredPerson) + GameStateCurator.mapPerson(filteredPerson) ), timerParams: game.timerParams, isFull: game.isFull, @@ -69,7 +84,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { return { accessCode: game.accessCode, status: game.status, - moderator: mapPerson(game.moderator), + moderator: GameStateCurator.mapPerson(game.moderator), client: client, deck: game.deck, people: GameStateCurator.mapPeopleForModerator(game.people, client), @@ -81,14 +96,14 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { return { accessCode: game.accessCode, status: game.status, - moderator: mapPerson(game.moderator), + moderator: GameStateCurator.mapPerson(game.moderator), client: client, deck: game.deck, people: game.people .filter((person) => { return person.assigned === true }) - .map((filteredPerson) => mapPerson(filteredPerson)), + .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), timerParams: game.timerParams, isFull: game.isFull } @@ -96,14 +111,14 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { return { accessCode: game.accessCode, status: game.status, - moderator: mapPerson(game.moderator), + moderator: GameStateCurator.mapPerson(game.moderator), client: client, deck: game.deck, people: game.people .filter((person) => { return person.assigned === true }) - .map((filteredPerson) => mapPerson(filteredPerson)), + .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), timerParams: game.timerParams, isFull: game.isFull, } @@ -112,20 +127,4 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { } } -function mapPerson(person) { - if (person.revealed) { - return { - name: person.name, - id: person.id, - userType: person.userType, - out: person.out, - revealed: person.revealed, - gameRole: person.gameRole, - alignment: person.alignment - }; - } else { - return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed }; - } -} - module.exports = GameStateCurator; diff --git a/server/routes/router.js b/server/routes/router.js index 473368a..d60c298 100644 --- a/server/routes/router.js +++ b/server/routes/router.js @@ -10,6 +10,10 @@ router.get('/create', function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/create.html')); }); +router.get('/how-to-use', function (request, response) { + response.sendFile(path.join(__dirname, '../../client/src/views/how-to-use.html')); +}); + router.get('/game/:code', function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/game.html')); });