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