From bcfb0afdd1e00c667799db7238f35f36d9f34704 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Thu, 3 Aug 2023 18:08:33 -0400 Subject: [PATCH] redesign how players are added and how cards are dealt --- .../front_end_components/HTMLFragments.js | 4 +- client/src/modules/game_state/states/Lobby.js | 23 +- client/src/styles/game.css | 1 - server/api/GamesAPI.js | 3 +- server/modules/Events.js | 21 +- server/modules/singletons/GameManager.js | 209 ++++++++---------- 6 files changed, 118 insertions(+), 143 deletions(-) diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index 812e08a..bb4fc8c 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -45,8 +45,8 @@ export const HTMLFragments = { `, START_GAME_PROMPT: - ` - `, + ` + `, GAME_CONTROL_PROMPT: `
diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index 9520f17..2ca85cd 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -18,6 +18,11 @@ export class Lobby { this.startGameHandler = (e) => { e.preventDefault(); + if (!stateBucket.currentGameState.isFull) { + toast('The number of players does not match the number of cards. ' + + 'You must either add/remove players or edit roles and their quantities.', 'error'); + return; + } Confirmation('Start game and deal roles?', () => { socket.timeout(5000).emit( globals.SOCKET_EVENTS.IN_GAME_MESSAGE, @@ -217,6 +222,9 @@ export class Lobby { this.socket.on(globals.EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize) => { this.stateBucket.currentGameState.deck = deck; this.stateBucket.currentGameState.gameSize = gameSize; + this.stateBucket.currentGameState.isFull = this.stateBucket.currentGameState.people + .filter(person => person.userType === globals.USER_TYPES.PLAYER + || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === gameSize; this.setLink(getTimeString(this.stateBucket.currentGameState)); this.setPlayerCount(); }); @@ -225,7 +233,7 @@ export class Lobby { displayStartGamePromptForModerators () { const existingPrompt = document.getElementById('start-game-prompt'); if (existingPrompt) { - enableOrDisableStartButton(this.stateBucket.currentGameState, existingPrompt, this.startGameHandler); + enableStartButton(existingPrompt, this.startGameHandler); document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler); } else { const newPrompt = document.createElement('div'); @@ -233,7 +241,7 @@ export class Lobby { newPrompt.innerHTML = HTMLFragments.START_GAME_PROMPT; document.body.appendChild(newPrompt); - enableOrDisableStartButton(this.stateBucket.currentGameState, newPrompt, this.startGameHandler); + enableStartButton(newPrompt, this.startGameHandler); document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler); } } @@ -244,14 +252,9 @@ export class Lobby { } } -function enableOrDisableStartButton (gameState, buttonContainer, handler) { - if (gameState.isFull) { - buttonContainer.querySelector('#start-game-button').addEventListener('click', handler); - buttonContainer.querySelector('#start-game-button').classList.remove('disabled'); - } else { - buttonContainer.querySelector('#start-game-button').removeEventListener('click', handler); - buttonContainer.querySelector('#start-game-button').classList.add('disabled'); - } +function enableStartButton (buttonContainer, handler) { + buttonContainer.querySelector('#start-game-button').addEventListener('click', handler); + buttonContainer.querySelector('#start-game-button').classList.remove('disabled'); } function activateLink (linkContainer, link) { diff --git a/client/src/styles/game.css b/client/src/styles/game.css index 7eeb61f..b11c5d5 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -641,7 +641,6 @@ label[for='moderator'] { #start-game-button { background-color: #1c8a36; - animation: shadow-pulse 1.5s infinite ease-out; } #end-game-button { diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 95429b8..ada8b5a 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -82,7 +82,8 @@ router.patch('/:code/players', async function (req, res) { gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => { res.status(200).send({ cookie: data, environment: gameManager.environment }); }).catch((data) => { - res.status(data.status).send(data.reason); + console.error(data); + res.status(data.status || 500).send(data.reason); }); } else { res.status(404).send(); diff --git a/server/modules/Events.js b/server/modules/Events.js index fc02fd0..0ee647e 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -1,6 +1,5 @@ const globals = require('../config/globals'); const GameStateCurator = require('./GameStateCurator'); -const UsernameGenerator = require('./UsernameGenerator'); const GameCreationRequest = require('../model/GameCreationRequest'); const EVENT_IDS = globals.EVENT_IDS; @@ -31,23 +30,8 @@ const Events = [ (person) => person.id === socketArgs.personId && person.assigned === true ); if (toBeClearedIndex >= 0) { - const toBeCleared = game.people[toBeClearedIndex]; - if (toBeCleared.userType === globals.USER_TYPES.SPECTATOR) { - game.people.splice(toBeClearedIndex, 1); - } else { - toBeCleared.assigned = false; - toBeCleared.socketId = null; - toBeCleared.cookie = (() => { - 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)]; - } - return id; - })(); - toBeCleared.hasEnteredName = false; - toBeCleared.name = UsernameGenerator.generate(); - game.isFull = vars.gameManager.isGameFull(game); - } + game.people.splice(toBeClearedIndex, 1); + game.isFull = vars.gameManager.isGameFull(game); } }, communicate: async (game, socketArgs, vars) => { @@ -126,6 +110,7 @@ const Events = [ stateChange: async (game, socketArgs, vars) => { if (game.isFull) { game.status = globals.STATUS.IN_PROGRESS; + vars.gameManager.deal(game); if (game.hasTimer) { game.timerParams.paused = true; await vars.timerManager.runTimer(game, vars.gameManager.namespace, vars.eventManager, vars.gameManager); diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 5443f14..33df774 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -76,7 +76,7 @@ class GameManager { const newGame = new Game( newAccessCode, globals.STATUS.LOBBY, - initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame), + null, req.deck, req.hasTimer, moderator.id, @@ -86,6 +86,7 @@ class GameManager { req.timerParams, req.isTestGame ); + 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 }); @@ -142,7 +143,7 @@ class GameManager { checkAvailability = async (code) => { const game = await this.getActiveGame(code.toUpperCase().trim()); if (game) { - return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams }); + return Promise.resolve({ accessCode: code, playerCount: game.gameSize, timerParams: game.timerParams }); } else { return Promise.resolve(404); } @@ -180,40 +181,62 @@ class GameManager { && game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS ) { return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); - } else if (joinAsSpectator) { + } else if (joinAsSpectator || this.isGameFull(game)) { + console.log('game is full'); return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame); } - const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false - ? this.findPersonByField(game, 'id', game.currentModeratorId) - : game.people.find((person) => person.assigned === false && person.userType === globals.USER_TYPES.PLAYER); - if (unassignedPerson) { - this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name); - unassignedPerson.assigned = true; - unassignedPerson.name = name; - game.isFull = this.isGameFull(game); - await this.refreshGame(game); - this.namespace.in(game.accessCode).emit( - globals.EVENTS.PLAYER_JOINED, - GameStateCurator.mapPerson(unassignedPerson), - game.isFull - ); - await this.eventManager.publisher?.publish( - globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, - this.eventManager.createMessageToPublish( - game.accessCode, - globals.EVENT_IDS.PLAYER_JOINED, - this.instanceId, - JSON.stringify(unassignedPerson) - ) - ); - return Promise.resolve(unassignedPerson.cookie); + let moderator, newPlayer; + const isModeratorJoining = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false; + if (isModeratorJoining) { + moderator = this.findPersonByField(game, 'id', game.currentModeratorId); + this.logger.trace('Moderator joining. Assigning: ' + name); + moderator.assigned = true; + moderator.name = name; } else { - if (game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS) { - return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' }); - } - return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame); + newPlayer = new Person( + createRandomId(), + createRandomId(), + name, + globals.USER_TYPES.PLAYER, + null, + null, + null, + game.isTestGame + ); + newPlayer.assigned = true; + game.people.push(newPlayer); } - }; + game.isFull = this.isGameFull(game); + await this.refreshGame(game); + this.namespace.in(game.accessCode).emit( + globals.EVENTS.PLAYER_JOINED, + GameStateCurator.mapPerson(moderator || newPlayer), + game.isFull + ); + await this.eventManager.publisher?.publish( + globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, + this.eventManager.createMessageToPublish( + game.accessCode, + globals.EVENT_IDS.PLAYER_JOINED, + this.instanceId, + JSON.stringify(moderator || newPlayer) + ) + ); + return Promise.resolve(moderator?.cookie || newPlayer?.cookie); + } + + prepareDeck (deck) { + const cards = []; + for (const card of deck) { + for (let i = 0; i < card.quantity; i ++) { + cards.push(card); + } + } + + this.shuffle(cards); + + return cards; + } restartGame = async (game, namespace, status = globals.STATUS.IN_PROGRESS) => { // kill any outstanding timer threads @@ -227,17 +250,6 @@ class GameManager { delete this.timerManager.timerThreads[game.accessCode]; } - // re-shuffle the deck - const cards = []; - for (const card of game.deck) { - for (let i = 0; i < card.quantity; i ++) { - cards.push(card); - } - } - - this.shuffle(cards); - - // make sure no players are marked as out or revealed, and give them new cards. for (let i = 0; i < game.people.length; i ++) { if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) { game.people[i].userType = globals.USER_TYPES.PLAYER; @@ -247,30 +259,19 @@ class GameManager { game.people[i].userType = globals.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; + game.people[i].out = false; + } game.people[i].revealed = false; game.people[i].killed = false; - if (game.people[i].gameRole) { - game.people[i].gameRole = cards[i].role; - game.people[i].gameRoleDescription = cards[i].description; - game.people[i].alignment = cards[i].team; - if (game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) { - game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR; - game.people[i].out = false; - } - } + game.people[i].gameRole = null; + game.people[i].gameRoleDescription = null; + game.people[i].alignment = null; + game.people[i].customRole = null; } - if (status === globals.STATUS.IN_PROGRESS) { - game.status = globals.STATUS.IN_PROGRESS; - if (game.hasTimer) { - game.timerParams.paused = true; - game.timerParams.timeRemaining = convertFromHoursToMilliseconds(game.timerParams.hours) + - convertFromMinutesToMilliseconds(game.timerParams.minutes); - await this.timerManager.runTimer(game, namespace, this.eventManager, this); - } - } else { - game.status = globals.STATUS.LOBBY; - } + game.status = globals.STATUS.LOBBY; await this.refreshGame(game); await this.eventManager.publisher?.publish( @@ -302,12 +303,24 @@ class GameManager { return array; }; - deal = () => { - + 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) + ) { + person.gameRole = cards[i].role; + person.customRole = cards[i].custom; + person.gameRoleDescription = cards[i].description; + person.alignment = cards[i].team; + i ++; + } } isGameFull = (game) => { - return !game.people.find((person) => person.userType === globals.USER_TYPES.PLAYER && person.assigned === false); + return game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER + || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === game.gameSize; } findPersonByField = (game, fieldName, value) => { @@ -326,46 +339,29 @@ function initializeModerator (name, hasDedicatedModerator) { return new Person(createRandomId(), createRandomId(), name, userType); } -function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame) { +function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, gameSize) { const people = []; - - const cards = []; - for (const role of uniqueRoles) { - for (let i = 0; i < role.quantity; i ++) { - cards.push(role); + if (isTestGame) { + let j = 0; + const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ? gameSize - 1 + : gameSize; + while (j < number) { + const person = new Person( + createRandomId(), + createRandomId(), + UsernameGenerator.generate(), + globals.USER_TYPES.BOT, + null, + null, + null, + isTestGame + ); + people.push(person); + j ++; } } - shuffle(cards); // this shuffles in-place. - - let j = 0; - const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR - ? cards.length - 1 - : cards.length; - while (j < number) { - const person = new Person( - createRandomId(), - createRandomId(), - UsernameGenerator.generate(), - isTestGame ? globals.USER_TYPES.BOT : globals.USER_TYPES.PLAYER, - cards[j].role, - cards[j].description, - cards[j].team, - isTestGame - ); - person.customRole = cards[j].custom; - person.hasEnteredName = false; - people.push(person); - j ++; - } - - if (moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - moderator.gameRole = cards[cards.length - 1].role; - moderator.customRole = cards[cards.length - 1].custom; - moderator.gameRoleDescription = cards[cards.length - 1].description; - moderator.alignment = cards[cards.length - 1].team; - } - people.push(moderator); return people; @@ -384,15 +380,6 @@ function isNameTaken (game, name) { return game.people.find((person) => person.name.toLowerCase().trim() === processedName); } -function getGameSize (cards) { - let quantity = 0; - for (const card of cards) { - quantity += card.quantity; - } - - return quantity; -} - async function addSpectator (game, name, logger, namespace, eventManager, instanceId, refreshGame) { const spectator = new Person( createRandomId(),