further redis effort

This commit is contained in:
AlecM33
2023-01-12 18:07:33 -05:00
parent a0607d2c9a
commit 91fbed7859
16 changed files with 452 additions and 413 deletions

View File

@@ -1,6 +1,6 @@
export const globals = { export const globals = {
CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
USER_SIGNATURE_LENGTH: 25, USER_SIGNATURE_LENGTH: 75,
CLOCK_TICK_INTERVAL_MILLIS: 100, CLOCK_TICK_INTERVAL_MILLIS: 100,
MAX_CUSTOM_ROLE_NAME_LENGTH: 50, MAX_CUSTOM_ROLE_NAME_LENGTH: 50,
MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000,
@@ -50,8 +50,10 @@ export const globals = {
SYNC_GAME_STATE: 'syncGameState', SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer', START_TIMER: 'startTimer',
PLAYER_LEFT: 'playerLeft', PLAYER_LEFT: 'playerLeft',
UPDATE_SPECTATORS: 'newSpectator', ADD_SPECTATOR: 'addSpectator',
RESTART_GAME: 'restartGame' UPDATE_SPECTATORS: 'updateSpectators',
RESTART_GAME: 'restartGame',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod'
}, },
USER_TYPES: { USER_TYPES: {
MODERATOR: 'moderator', MODERATOR: 'moderator',

View File

@@ -27,7 +27,11 @@ export class Ended {
// sortPeopleByStatus(this.stateBucket.currentGameState.people); // sortPeopleByStatus(this.stateBucket.currentGameState.people);
const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null;
renderGroupOfPlayers( 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, this.stateBucket.currentGameState.accessCode,
null, null,
modType, modType,
@@ -35,7 +39,7 @@ export class Ended {
); );
document.getElementById('players-alive-label').innerText = document.getElementById('players-alive-label').innerText =
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
this.stateBucket.currentGameState.people.length + ' Alive'; this.stateBucket.currentGameState.gameSize + ' Alive';
} }
} }

View File

@@ -61,11 +61,11 @@ export class InProgress {
if (spectatorCount) { if (spectatorCount) {
spectatorCount?.addEventListener('click', () => { 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( SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.spectators.length, this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
spectatorCount spectatorCount
); );
} }
@@ -90,9 +90,16 @@ export class InProgress {
/* TODO: UX issue - it's easier to parse visually when players are sorted this way, /* 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. */ but shifting players around when they are killed or revealed is bad UX for the moderator. */
// sortPeopleByStatus(this.stateBucket.currentGameState.people); // 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.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.killPlayerHandlers,
this.revealRoleHandlers, this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode, this.stateBucket.currentGameState.accessCode,
@@ -102,7 +109,7 @@ export class InProgress {
); );
document.getElementById('players-alive-label').innerText = document.getElementById('players-alive-label').innerText =
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
this.stateBucket.currentGameState.people.length + ' Alive'; this.stateBucket.currentGameState.gameSize + ' Alive';
} }
removePlayerListEventListeners (removeEl = true) { removePlayerListEventListeners (removeEl = true) {
@@ -155,6 +162,7 @@ export class InProgress {
const killedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === id); const killedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === id);
if (killedPerson) { if (killedPerson) {
killedPerson.out = true; killedPerson.out = true;
killedPerson.killed = true;
killedPerson.userType = globals.USER_TYPES.KILLED_PLAYER; killedPerson.userType = globals.USER_TYPES.KILLED_PLAYER;
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPerson.name + ' killed.', 'success', true, true, 'medium'); toast(killedPerson.name + ' killed.', 'success', true, true, 'medium');
@@ -203,14 +211,27 @@ export class InProgress {
} }
}); });
if (this.socket.hasListeners(globals.EVENT_IDS.UPDATE_SPECTATORS)) { if (this.socket.hasListeners(globals.EVENT_IDS.ADD_SPECTATOR)) {
this.socket.removeAllListeners(globals.EVENT_IDS.UPDATE_SPECTATORS); this.socket.removeAllListeners(globals.EVENT_IDS.ADD_SPECTATOR);
} }
this.socket.on(globals.EVENT_IDS.UPDATE_SPECTATORS, (updatedSpectatorList) => { this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => {
stateBucket.currentGameState.spectators = updatedSpectatorList; stateBucket.currentGameState.people.push(spectator);
SharedStateUtil.setNumberOfSpectators( 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') document.getElementById('spectator-count')
); );
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
@@ -231,15 +252,26 @@ export class InProgress {
this.stateBucket.currentGameState.people.sort((a, b) => { this.stateBucket.currentGameState.people.sort((a, b) => {
return a.name >= b.name ? 1 : -1; return a.name >= b.name ? 1 : -1;
}); });
const teamGood = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD); const teamGood = this.stateBucket.currentGameState.people.filter(
const teamEvil = this.stateBucket.currentGameState.people.filter((person) => person.alignment === globals.ALIGNMENT.EVIL); (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( this.renderGroupOfPlayers(
teamEvil, teamEvil,
this.killPlayerHandlers, this.killPlayerHandlers,
this.revealRoleHandlers, this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode, this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.EVIL, globals.ALIGNMENT.EVIL,
this.stateBucket.currentGameState.moderator.userType, this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket this.socket
); );
this.renderGroupOfPlayers( this.renderGroupOfPlayers(
@@ -248,12 +280,13 @@ export class InProgress {
this.revealRoleHandlers, this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode, this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.GOOD, globals.ALIGNMENT.GOOD,
this.stateBucket.currentGameState.moderator.userType, this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket this.socket
); );
document.getElementById('players-alive-label').innerText = document.getElementById('players-alive-label').innerText =
'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' +
this.stateBucket.currentGameState.people.length + ' Alive'; this.stateBucket.currentGameState.gameSize + ' Alive';
} }
renderGroupOfPlayers ( renderGroupOfPlayers (
@@ -302,7 +335,11 @@ export class InProgress {
} else if (!player.out && moderatorType) { } else if (!player.out && moderatorType) {
killPlayerHandlers[player.id] = () => { killPlayerHandlers[player.id] = () => {
Confirmation('Kill \'' + player.name + '\'?', () => { 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]); playerEl.querySelector('.kill-player-button').addEventListener('click', killPlayerHandlers[player.id]);
@@ -347,12 +384,6 @@ export class InProgress {
this.transferModHandlers, this.transferModHandlers,
this.socket 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) { if (document.querySelectorAll('.potential-moderator').length === 0) {
document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.'; 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) { function renderPotentialMods (gameState, group, transferModHandlers, socket) {
const modalContent = document.getElementById('transfer-mod-modal-content'); const modalContent = document.getElementById('transfer-mod-modal-content');
for (const member of group) { 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'); const container = document.createElement('div');
container.classList.add('potential-moderator'); container.classList.add('potential-moderator');
container.setAttribute('tabindex', '0'); container.setAttribute('tabindex', '0');

View File

@@ -48,11 +48,11 @@ export class Lobby {
playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players'; playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players';
this.container.querySelector('#spectator-count').addEventListener('click', () => { 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( 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') this.container.querySelector('#spectator-count')
); );
@@ -68,18 +68,20 @@ export class Lobby {
populatePlayers () { populatePlayers () {
document.querySelectorAll('.lobby-player').forEach((el) => el.remove()); document.querySelectorAll('.lobby-player').forEach((el) => el.remove());
const lobbyPlayersContainer = this.container.querySelector('#lobby-players'); const lobbyPlayersContainer = this.container.querySelector('#lobby-players');
if (this.stateBucket.currentGameState.moderator.userType === globals.USER_TYPES.MODERATOR) { const sorted = this.stateBucket.currentGameState.people.sort(
lobbyPlayersContainer.appendChild( function (a, b) {
renderLobbyPerson( if (a.userType === globals.USER_TYPES.MODERATOR) {
this.stateBucket.currentGameState.moderator.name, return -1;
this.stateBucket.currentGameState.moderator.userType }
) return 1;
); }
} );
for (const person of this.stateBucket.currentGameState.people) { for (const person of sorted.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR)) {
lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType)); 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 = document.querySelector("label[for='lobby-players']").innerText =
'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; 'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)';
} }
@@ -99,10 +101,10 @@ export class Lobby {
} }
}); });
this.socket.on(globals.EVENT_IDS.UPDATE_SPECTATORS, (updatedSpectatorList) => { this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.stateBucket.currentGameState.spectators = updatedSpectatorList; this.stateBucket.currentGameState.people.push(spectator);
SharedStateUtil.setNumberOfSpectators( SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.spectators.length, this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
document.getElementById('spectator-count') document.getElementById('spectator-count')
); );
}); });
@@ -193,6 +195,9 @@ function renderLobbyPerson (name, userType) {
personNameEl.innerText = name; personNameEl.innerText = name;
personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType];
el.classList.add('lobby-player'); el.classList.add('lobby-player');
if (userType === globals.USER_TYPES.MODERATOR) {
el.classList.add('moderator');
}
el.appendChild(personNameEl); el.appendChild(personNameEl);
el.appendChild(personTypeEl); el.appendChild(personTypeEl);

View File

@@ -132,8 +132,9 @@ export const SharedStateUtil = {
} }
}, },
buildSpectatorList (spectators) { buildSpectatorList (people) {
const list = document.createElement('div'); const list = document.createElement('div');
const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR);
if (spectators.length === 0) { if (spectators.length === 0) {
list.innerHTML = '<div>Nobody currently spectating.</div>'; list.innerHTML = '<div>Nobody currently spectating.</div>';
} else { } else {
@@ -173,8 +174,18 @@ function processGameState (
easing: 'ease-in-out', easing: 'ease-in-out',
fill: 'both' fill: 'both'
}); });
const clientAnimation = document.getElementById('client-container').animate([
{ opacity: '0' },
{ opacity: '1' }
], {
duration: 500,
easing: 'ease-out',
fill: 'both'
});
if (animateContainer) { if (animateContainer) {
containerAnimation.play(); containerAnimation.play();
clientAnimation.play();
} }
displayClientInfo(currentGameState.client.name, currentGameState.client.userType); displayClientInfo(currentGameState.client.name, currentGameState.client.userType);

View File

@@ -13,6 +13,10 @@
margin: 0 auto 0.25em auto; margin: 0 auto 0.25em auto;
} }
.moderator {
border: 2px solid #c58f13;
}
.potential-moderator { .potential-moderator {
margin: 0.5em auto; margin: 0.5em auto;
} }
@@ -952,3 +956,18 @@ canvas {
transform: translateY(0px); 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);
}
}

View File

@@ -3,7 +3,7 @@ const router = express.Router();
const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug'); const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug');
const logger = require('../modules/Logger')(debugMode); const logger = require('../modules/Logger')(debugMode);
const socketManager = (require('../modules/singletons/SocketManager.js')).instance; 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 globals = require('../config/globals.js');
const cors = require('cors'); const cors = require('cors');
@@ -22,8 +22,8 @@ router.post('/sockets/broadcast', function (req, res) {
router.get('/games/state', async (req, res) => { router.get('/games/state', async (req, res) => {
const gamesArray = []; const gamesArray = [];
await this.client.hGetAll('activeGames').then(async (r) => { await activeGameRunner.client.keys('*').then(async (r) => {
Object.values(r).forEach((v) => gamesArray.push(v)); Object.values(r).forEach((v) => gamesArray.push(JSON.parse(v)));
}); });
res.status(200).send(gamesArray); res.status(200).send(gamesArray);
}); });

View File

@@ -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 ( if (
req.body === null req.body === null
|| !validateAccessCode(req.body.accessCode) || !validateAccessCode(req.body.accessCode)
@@ -101,7 +101,7 @@ router.patch('/:code/restart', function (req, res) {
) { ) {
res.status(400).send(); res.status(400).send();
} else { } else {
const game = gameManager.activeGameRunner.getActiveGame(req.body.accessCode); const game = await gameManager.activeGameRunner.getActiveGame(req.body.accessCode);
if (game) { if (game) {
gameManager.restartGame(game, gameManager.namespace).then((data) => { gameManager.restartGame(game, gameManager.namespace).then((data) => {
res.status(200).send(); res.status(200).send();
@@ -123,11 +123,11 @@ function validateName (name) {
} }
function validateCookie (cookie) { 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) { 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) { function validateSpectatorFlag (spectatorFlag) {

View File

@@ -11,7 +11,7 @@ const globals = {
EVIL: 'evil' EVIL: 'evil'
}, },
REDIS_CHANNELS: { REDIS_CHANNELS: {
ACTIVE_GAME_STREAM: 'active_game_stream' ACTIVE_GAME_STREAM: 'active_game_stream'
}, },
CORS: process.env.NODE_ENV?.trim() === 'development' CORS: process.env.NODE_ENV?.trim() === 'development'
? { ? {
@@ -30,7 +30,7 @@ const globals = {
res.status(400).send('Request has invalid content type.'); res.status(400).send('Request has invalid content type.');
} }
}, },
STALE_GAME_HOURS: 24, STALE_GAME_SECONDS: 86400,
SOCKET_EVENTS: { SOCKET_EVENTS: {
IN_GAME_MESSAGE: 'inGameMessage' IN_GAME_MESSAGE: 'inGameMessage'
}, },
@@ -49,8 +49,10 @@ const globals = {
RESTART_GAME: 'restartGame', RESTART_GAME: 'restartGame',
PLAYER_JOINED: 'playerJoined', PLAYER_JOINED: 'playerJoined',
UPDATE_SPECTATORS: 'updateSpectators', UPDATE_SPECTATORS: 'updateSpectators',
ADD_SPECTATOR: 'addSpectator',
SYNC_GAME_STATE: 'syncGameState', SYNC_GAME_STATE: 'syncGameState',
UPDATE_SOCKET: 'updateSocket' UPDATE_SOCKET: 'updateSocket',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod'
}, },
SYNCABLE_EVENTS: function () { SYNCABLE_EVENTS: function () {
return [ return [
@@ -65,10 +67,13 @@ const globals = {
this.EVENT_IDS.END_GAME, this.EVENT_IDS.END_GAME,
this.EVENT_IDS.RESTART_GAME, this.EVENT_IDS.RESTART_GAME,
this.EVENT_IDS.PLAYER_JOINED, 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.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: { MESSAGES: {
ENTER_NAME: 'Client must enter name.' ENTER_NAME: 'Client must enter name.'

View File

@@ -5,7 +5,7 @@ class Game {
people, people,
deck, deck,
hasTimer, hasTimer,
moderator, currentModeratorId,
hasDedicatedModerator, hasDedicatedModerator,
originalModeratorId, originalModeratorId,
createTime, createTime,
@@ -13,7 +13,7 @@ class Game {
) { ) {
this.accessCode = accessCode; this.accessCode = accessCode;
this.status = status; this.status = status;
this.moderator = moderator; this.currentModeratorId = currentModeratorId;
this.people = people; this.people = people;
this.deck = deck; this.deck = deck;
this.gameSize = deck.reduce( this.gameSize = deck.reduce(
@@ -23,11 +23,11 @@ class Game {
this.hasTimer = hasTimer; this.hasTimer = hasTimer;
this.hasDedicatedModerator = hasDedicatedModerator; this.hasDedicatedModerator = hasDedicatedModerator;
this.originalModeratorId = originalModeratorId; this.originalModeratorId = originalModeratorId;
this.previousModeratorId = null;
this.createTime = createTime; this.createTime = createTime;
this.timerParams = timerParams; this.timerParams = timerParams;
this.isFull = this.gameSize === 1 && !this.hasDedicatedModerator; this.isFull = this.gameSize === 1 && !this.hasDedicatedModerator;
this.timeRemaining = null; this.timeRemaining = null;
this.spectators = [];
} }
} }

View File

@@ -1,4 +1,6 @@
// noinspection DuplicatedCode // noinspection DuplicatedCode
const globals = require('../config/globals');
class Person { class Person {
constructor (id, cookie, name, userType, gameRole = null, gameRoleDescription = null, alignment = null, assigned = false) { constructor (id, cookie, name, userType, gameRole = null, gameRoleDescription = null, alignment = null, assigned = false) {
this.id = id; this.id = id;
@@ -10,7 +12,8 @@ class Person {
this.gameRoleDescription = gameRoleDescription; this.gameRoleDescription = gameRoleDescription;
this.alignment = alignment; this.alignment = alignment;
this.assigned = assigned; 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.revealed = false;
this.hasEnteredName = false; this.hasEnteredName = false;
} }

View File

@@ -1,66 +1,242 @@
const globals = require('../config/globals'); const globals = require('../config/globals');
const GameStateCurator = require("./GameStateCurator"); const GameStateCurator = require('./GameStateCurator');
const EVENT_IDS = globals.EVENT_IDS; const EVENT_IDS = globals.EVENT_IDS;
const Events = [ const Events = [
{ {
id: EVENT_IDS.PLAYER_JOINED, id: EVENT_IDS.PLAYER_JOINED,
stateChange: (game, args, gameManager) => { stateChange: (game, socketArgs, vars) => {
let toBeAssignedIndex = game.people.findIndex( const toBeAssignedIndex = game.people.findIndex(
(person) => person.id === args.id && person.assigned === false (person) => person.id === socketArgs.id && person.assigned === false
); );
if (toBeAssignedIndex >= 0) { if (toBeAssignedIndex >= 0) {
game.people[toBeAssignedIndex] = args; game.people[toBeAssignedIndex] = socketArgs;
game.isFull = gameManager.isGameFull(game); game.isFull = vars.gameManager.isGameFull(game);
} }
}, },
communicate: (game, args, gameManager) => { communicate: (game, socketArgs, vars) => {
gameManager.namespace.in(game.accessCode).emit( vars.gameManager.namespace.in(game.accessCode).emit(
globals.EVENTS.PLAYER_JOINED, globals.EVENTS.PLAYER_JOINED,
GameStateCurator.mapPerson(args), GameStateCurator.mapPerson(socketArgs),
game.isFull game.isFull
); );
} }
}, },
{ {
id: EVENT_IDS.UPDATE_SPECTATORS, id: EVENT_IDS.ADD_SPECTATOR,
stateChange: (game, args, gameManager) => { stateChange: (game, socketArgs, vars) => {
game.spectators = args; game.people.push(socketArgs);
}, },
communicate: (game, args, gameManager) => { communicate: (game, socketArgs, vars) => {
gameManager.namespace.in(game.accessCode).emit( vars.gameManager.namespace.in(game.accessCode).emit(
globals.EVENTS.UPDATE_SPECTATORS, globals.EVENT_IDS.ADD_SPECTATOR,
game.spectators.map((spectator) => { return GameStateCurator.mapPerson(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, id: EVENT_IDS.FETCH_GAME_STATE,
stateChange: (game, args, gameManager) => { stateChange: (game, socketArgs, vars) => {
const matchingPerson = gameManager.findPersonByField(game, 'cookie', args.personId); const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId);
if (matchingPerson) { if (matchingPerson && matchingPerson.socketId !== vars.socketId) {
if (matchingPerson.socketId === socketId) { matchingPerson.socketId = vars.socketId;
logger.debug('matching person found with an established connection to the room: ' + matchingPerson.name); vars.gameManager.namespace.sockets.get(vars.socketId)?.join(game.accessCode);
if (ackFn) { }
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson)); },
} communicate: (game, socketArgs, vars) => {
} else { if (!vars.ackFn) return;
logger.debug('matching person found with a new connection to the room: ' + matchingPerson.name); const matchingPerson = vars.gameManager.findPersonByField(game, 'cookie', socketArgs.personId);
this.namespace.sockets.get(socketId).join(accessCode); if (matchingPerson && vars.gameManager.namespace.sockets.get(matchingPerson.socketId)) {
matchingPerson.socketId = socketId; vars.ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson));
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 { } else {
if (ackFn) { vars.ackFn(null);
rejectClientRequestForGameState(ackFn); }
}
},
// {
// 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);
} }
} }
]; ];

View File

@@ -12,7 +12,7 @@ const GameStateCurator = {
mapPeopleForModerator: (people) => { mapPeopleForModerator: (people) => {
return people return people
.filter((person) => { .filter((person) => {
return person.assigned === true; return person.assigned === true || (person.userType === globals.USER_TYPES.SPECTATOR || person.userType === globals.USER_TYPES.MODERATOR);
}) })
.map((person) => ({ .map((person) => ({
name: person.name, name: person.name,
@@ -22,6 +22,7 @@ const GameStateCurator = {
gameRoleDescription: person.gameRoleDescription, gameRoleDescription: person.gameRoleDescription,
alignment: person.alignment, alignment: person.alignment,
out: person.out, out: person.out,
killed: person.killed,
revealed: person.revealed revealed: person.revealed
})); }));
}, },
@@ -32,12 +33,13 @@ const GameStateCurator = {
id: person.id, id: person.id,
userType: person.userType, userType: person.userType,
out: person.out, out: person.out,
killed: person.killed,
revealed: person.revealed, revealed: person.revealed,
gameRole: person.gameRole, gameRole: person.gameRole,
alignment: person.alignment alignment: person.alignment
}; };
} else { } 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, gameRoleDescription: person.gameRoleDescription,
customRole: person.customRole, customRole: person.customRole,
alignment: person.alignment, alignment: person.alignment,
out: person.out out: person.out,
killed: person.killed
}; };
switch (person.userType) { switch (person.userType) {
case globals.USER_TYPES.MODERATOR: case globals.USER_TYPES.MODERATOR:
return { return {
accessCode: game.accessCode, accessCode: game.accessCode,
status: game.status, status: game.status,
moderator: GameStateCurator.mapPerson(game.moderator), currentModeratorId: game.currentModeratorId,
client: client, client: client,
deck: game.deck, deck: game.deck,
gameSize: game.gameSize, gameSize: game.gameSize,
people: GameStateCurator.mapPeopleForModerator(game.people, client), people: GameStateCurator.mapPeopleForModerator(game.people, client),
timerParams: game.timerParams, timerParams: game.timerParams,
isFull: game.isFull, isFull: game.isFull
spectators: game.spectators.map((filteredPerson) =>
GameStateCurator.mapPerson(filteredPerson)
)
}; };
case globals.USER_TYPES.TEMPORARY_MODERATOR: case globals.USER_TYPES.TEMPORARY_MODERATOR:
case globals.USER_TYPES.SPECTATOR: case globals.USER_TYPES.SPECTATOR:
@@ -80,20 +80,17 @@ function getGameStateBasedOnPermissions (game, person) {
return { return {
accessCode: game.accessCode, accessCode: game.accessCode,
status: game.status, status: game.status,
moderator: GameStateCurator.mapPerson(game.moderator), currentModeratorId: game.currentModeratorId,
client: client, client: client,
deck: game.deck, deck: game.deck,
gameSize: game.gameSize, gameSize: game.gameSize,
people: game.people people: game.people
.filter((person) => { .filter((person) => {
return person.assigned === true; return person.assigned === true || person.userType === globals.USER_TYPES.SPECTATOR;
}) })
.map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)),
timerParams: game.timerParams, timerParams: game.timerParams,
isFull: game.isFull, isFull: game.isFull
spectators: game.spectators.map((filteredPerson) =>
GameStateCurator.mapPerson(filteredPerson)
)
}; };
default: default:
break; break;

View File

@@ -19,8 +19,8 @@ class ActiveGameRunner {
} }
getActiveGame = async (accessCode) => { getActiveGame = async (accessCode) => {
const r = await this.client.hGet('activeGames', accessCode); const r = await this.client.get(accessCode);
return JSON.parse(r); return r === null ? r : JSON.parse(r);
} }
createGameSyncSubscriber = async (gameManager, socketManager) => { createGameSyncSubscriber = async (gameManager, socketManager) => {
@@ -28,7 +28,7 @@ class ActiveGameRunner {
await this.subscriber.connect(); await this.subscriber.connect();
await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => { await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => {
this.logger.info('MESSAGE: ' + message); this.logger.info('MESSAGE: ' + message);
let messageComponents = message.split(';'); const messageComponents = message.split(';');
if (messageComponents[messageComponents.length - 1] === this.instanceId) { if (messageComponents[messageComponents.length - 1] === this.instanceId) {
this.logger.trace('Disregarding self-authored message'); this.logger.trace('Disregarding self-authored message');
return; return;
@@ -44,10 +44,10 @@ class ActiveGameRunner {
game, game,
null, null,
game?.accessCode || messageComponents[0], game?.accessCode || messageComponents[0],
args ? args : null, args || null,
null, null,
true true
) );
} }
}); });
this.logger.info('ACTIVE GAME RUNNER - CREATED GAME SYNC SUBSCRIBER'); this.logger.info('ACTIVE GAME RUNNER - CREATED GAME SYNC SUBSCRIBER');

View File

@@ -6,7 +6,6 @@ const UsernameGenerator = require('../UsernameGenerator');
const GameCreationRequest = require('../../model/GameCreationRequest'); const GameCreationRequest = require('../../model/GameCreationRequest');
const redis = require('redis'); const redis = require('redis');
class GameManager { class GameManager {
constructor (logger, environment, instanceId) { constructor (logger, environment, instanceId) {
if (GameManager.instance) { if (GameManager.instance) {
@@ -34,8 +33,8 @@ class GameManager {
}; };
refreshGame = async (game) => { refreshGame = async (game) => {
this.logger.debug('PUSHING REFRESH OF ' + game.accessCode); this.logger.debug('PUSHING REFRESH OF ' + game.accessCode);
await this.activeGameRunner.client.hSet('activeGames', game.accessCode, JSON.stringify(game)); await this.activeGameRunner.client.set(game.accessCode, JSON.stringify(game));
} }
createGame = async (gameParams) => { createGame = async (gameParams) => {
@@ -48,12 +47,12 @@ class GameManager {
gameParams.moderatorName, gameParams.moderatorName,
gameParams.hasDedicatedModerator gameParams.hasDedicatedModerator
); );
//await this.pruneStaleGames();
const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL);
if (newAccessCode === null) { if (newAccessCode === null) {
return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE);
} }
const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator); const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator);
console.log(moderator);
moderator.assigned = true; moderator.assigned = true;
if (req.timerParams !== null) { if (req.timerParams !== null) {
req.timerParams.paused = false; req.timerParams.paused = false;
@@ -64,13 +63,15 @@ class GameManager {
initializePeopleForGame(req.deck, moderator, this.shuffle), initializePeopleForGame(req.deck, moderator, this.shuffle),
req.deck, req.deck,
req.hasTimer, req.hasTimer,
moderator, moderator.id,
req.hasDedicatedModerator, req.hasDedicatedModerator,
moderator.id, moderator.id,
new Date().toJSON(), new Date().toJSON(),
req.timerParams 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 }); return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment });
}).catch((message) => { }).catch((message) => {
console.log(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) => { pauseTimer = async (game, logger) => {
const thread = this.activeGameRunner.timerThreads[game.accessCode]; const thread = this.activeGameRunner.timerThreads[game.accessCode];
if (thread && !thread.killed) { 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) => { checkAvailability = async (code) => {
const game = await this.activeGameRunner.getActiveGame(code.toUpperCase().trim()); const game = await this.activeGameRunner.getActiveGame(code.toUpperCase().trim());
if (game) { if (game) {
@@ -173,7 +135,7 @@ class GameManager {
const charCount = charPool.length; const charCount = charPool.length;
let codeDigits, accessCode; let codeDigits, accessCode;
let attempts = 0; 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)) { && attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) {
codeDigits = []; codeDigits = [];
let iterations = globals.ACCESS_CODE_LENGTH; let iterations = globals.ACCESS_CODE_LENGTH;
@@ -184,76 +146,11 @@ class GameManager {
accessCode = codeDigits.join(''); accessCode = codeDigits.join('');
attempts ++; attempts ++;
} }
return (await this.activeGameRunner.client.hKeys('activeGames')).includes(accessCode) return (await this.activeGameRunner.client.keys('*')).includes(accessCode)
? null ? null
: accessCode; : 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) => { joinGame = async (game, name, cookie, joinAsSpectator) => {
const matchingPerson = this.findPersonByField(game, 'cookie', cookie); const matchingPerson = this.findPersonByField(game, 'cookie', cookie);
if (matchingPerson) { if (matchingPerson) {
@@ -262,14 +159,16 @@ class GameManager {
if (isNameTaken(game, name)) { if (isNameTaken(game, name)) {
return Promise.reject({ status: 400, reason: 'This name is taken.' }); 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.' }); return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' });
} else if (joinAsSpectator) { } else if (joinAsSpectator) {
return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame); return await addSpectator(game, name, this.logger, this.namespace, this.publisher, this.instanceId, this.refreshGame);
} }
const unassignedPerson = game.moderator.assigned === false const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false
? game.moderator ? this.findPersonByField(game, 'id', game.currentModeratorId)
: game.people.find((person) => person.assigned === false); : game.people.find((person) => person.assigned === false && person.userType === globals.USER_TYPES.PLAYER);
if (unassignedPerson) { if (unassignedPerson) {
this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name); this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name);
unassignedPerson.assigned = true; unassignedPerson.assigned = true;
@@ -287,7 +186,7 @@ class GameManager {
); );
return Promise.resolve(unassignedPerson.cookie); return Promise.resolve(unassignedPerson.cookie);
} else { } 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 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); 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) => { restartGame = async (game, namespace) => {
// kill any outstanding timer threads // kill any outstanding timer threads
const subProcess = this.activeGameRunner.timerThreads[game.accessCode]; // const subProcess = this.activeGameRunner.timerThreads[game.accessCode];
if (subProcess) { // if (subProcess) {
if (!subProcess.killed) { // if (!subProcess.killed) {
this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); // this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode);
this.activeGameRunner.timerThreads[game.accessCode].kill(); // this.activeGameRunner.timerThreads[game.accessCode].kill();
} // }
this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); // this.logger.debug('Deleting reference to subprocess ' + subProcess.pid);
delete this.activeGameRunner.timerThreads[game.accessCode]; // delete this.activeGameRunner.timerThreads[game.accessCode];
} // }
// re-shuffle the deck // re-shuffle the deck
const cards = []; const cards = [];
@@ -318,23 +217,21 @@ class GameManager {
// make sure no players are marked as out or revealed, and give them new 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 ++) { 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) { if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) {
game.people[i].userType = globals.USER_TYPES.PLAYER; game.people[i].userType = globals.USER_TYPES.PLAYER;
game.people[i].out = false;
} }
game.people[i].revealed = false; game.people[i].revealed = false;
game.people[i].gameRole = cards[i].role; game.people[i].killed = false;
game.people[i].gameRoleDescription = cards[i].description; if (game.people[i].gameRole) {
game.people[i].alignment = cards[i].team; 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 if (game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) {
them a temporary mod for the restarted game. game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
*/ game.people[i].out = false;
if (game.moderator.gameRole && game.moderator.userType === globals.USER_TYPES.MODERATOR) { }
game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR; }
} }
// start the new game // start the new game
@@ -345,36 +242,13 @@ class GameManager {
} }
await this.refreshGame(game); 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); 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): -- To shuffle an array a of n elements (indices 0..n-1):
for i from n1 downto 1 do for i from n1 downto 1 do
@@ -392,39 +266,12 @@ class GameManager {
return array; 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) => { 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) => { findPersonByField = (game, fieldName, value) => {
let person; return game.people.find(person => person[fieldName] === value);
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;
} }
} }
@@ -439,30 +286,23 @@ function initializeModerator (name, hasDedicatedModerator) {
return new Person(createRandomId(), createRandomId(), name, userType); return new Person(createRandomId(), createRandomId(), name, userType);
} }
function initializePeopleForGame (uniqueCards, moderator, shuffle) { function initializePeopleForGame (uniqueRoles, moderator, shuffle) {
const people = []; const people = [];
const cards = []; const cards = [];
let numberOfRoles = 0; for (const role of uniqueRoles) {
for (const card of uniqueCards) { for (let i = 0; i < role.quantity; i ++) {
for (let i = 0; i < card.quantity; i ++) { cards.push(role);
cards.push(card);
numberOfRoles ++;
} }
} }
shuffle(cards); // this shuffles in-place. shuffle(cards); // this shuffles in-place.
let j = 0; let j = 0;
if (moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { // temporary moderators should be dealt in. const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
moderator.gameRole = cards[j].role; ? cards.length - 1
moderator.customRole = cards[j].custom; : cards.length;
moderator.gameRoleDescription = cards[j].description; while (j < number) {
moderator.alignment = cards[j].team;
people.push(moderator);
j ++;
}
while (j < numberOfRoles) {
const person = new Person( const person = new Person(
createRandomId(), createRandomId(),
createRandomId(), createRandomId(),
@@ -478,30 +318,29 @@ function initializePeopleForGame (uniqueCards, moderator, shuffle) {
j ++; 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; return people;
} }
function createRandomId () { function createRandomId () {
let id = ''; let id = '';
for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i ++) { for (let i = 0; i < globals.INSTANCE_ID_LENGTH; i ++) {
id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)]; id += globals.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * globals.INSTANCE_ID_CHAR_POOL.length)];
} }
return id; 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) { function isNameTaken (game, name) {
const processedName = name.toLowerCase().trim(); const processedName = name.toLowerCase().trim();
return (game.people.find((person) => person.name.toLowerCase().trim() === processedName)) 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));
} }
function getGameSize (cards) { function getGameSize (cards) {
@@ -513,12 +352,6 @@ function getGameSize (cards) {
return quantity; 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) { async function addSpectator (game, name, logger, namespace, publisher, instanceId, refreshGame) {
const spectator = new Person( const spectator = new Person(
createRandomId(), createRandomId(),
@@ -527,15 +360,15 @@ async function addSpectator (game, name, logger, namespace, publisher, instanceI
globals.USER_TYPES.SPECTATOR globals.USER_TYPES.SPECTATOR
); );
logger.trace('new spectator: ' + spectator.name); logger.trace('new spectator: ' + spectator.name);
game.spectators.push(spectator); game.people.push(spectator);
await refreshGame(game); await refreshGame(game);
namespace.in(game.accessCode).emit( namespace.in(game.accessCode).emit(
globals.EVENTS.UPDATE_SPECTATORS, globals.EVENT_IDS.ADD_SPECTATOR,
game.spectators.map((spectator) => { return GameStateCurator.mapPerson(spectator); }) GameStateCurator.mapPerson(spectator)
); );
await publisher.publish( await publisher.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, 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); return Promise.resolve(spectator.cookie);
} }

View File

@@ -2,8 +2,7 @@ const globals = require('../../config/globals');
const EVENT_IDS = globals.EVENT_IDS; const EVENT_IDS = globals.EVENT_IDS;
const { RateLimiterMemory } = require('rate-limiter-flexible'); const { RateLimiterMemory } = require('rate-limiter-flexible');
const redis = require('redis'); const redis = require('redis');
const GameStateCurator = require("../GameStateCurator"); const Events = require('../Events');
const Events = require("../Events");
class SocketManager { class SocketManager {
constructor (logger, instanceId) { constructor (logger, instanceId) {
@@ -73,58 +72,33 @@ class SocketManager {
}); });
}; };
handleAndSyncEvent = async (eventId, game, socket, args, ackFn) => { handleAndSyncEvent = async (eventId, game, socket, socketArgs, ackFn) => {
await this.handleEventById(eventId, game, socket?.id, game.accessCode, args, ackFn, false); 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. */ /* This server should publish events initiated by a connected socket to Redis for consumption by other instances. */
if (globals.SYNCABLE_EVENTS().includes(eventId)) { if (globals.SYNCABLE_EVENTS().includes(eventId)) {
await this.gameManager.refreshGame(game); await this.gameManager.refreshGame(game);
this.publisher?.publish( await this.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, 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) => { handleEventById = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => {
this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(args)); this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(socketArgs));
const event = Events.find((event) => event.id === eventId); const event = Events.find((event) => event.id === eventId);
const additionalVars = {
gameManager: this.gameManager,
socketId: socketId,
ackFn: ackFn
};
if (event) { if (event) {
if (!syncOnly) { 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) { 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: case EVENT_IDS.PAUSE_TIMER:
await this.gameManager.pauseTimer(game, this.logger); await this.gameManager.pauseTimer(game, this.logger);
break; break;
@@ -134,27 +108,6 @@ class SocketManager {
case EVENT_IDS.GET_TIME_REMAINING: case EVENT_IDS.GET_TIME_REMAINING:
await this.gameManager.getTimeRemaining(game, socketId); await this.gameManager.getTimeRemaining(game, socketId);
break; 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: default:
break; break;
} }