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(),