various refactors

This commit is contained in:
AlecM33
2023-08-13 17:51:41 -04:00
parent 29431565b4
commit 0b7dd9f4d7
34 changed files with 1097 additions and 1164 deletions

View File

@@ -1,4 +1,4 @@
export const globals = {
export const PRIMITIVES = {
CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
USER_SIGNATURE_LENGTH: 75,
CLOCK_TICK_INTERVAL_MILLIS: 50,
@@ -6,100 +6,99 @@ export const globals = {
MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000,
TOAST_DURATION_DEFAULT: 6,
ACCESS_CODE_LENGTH: 4,
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
END_GAME: 'endGame',
END_TIMER: 'endTimer'
},
STATUS: {
LOBBY: 'lobby',
IN_PROGRESS: 'in progress',
ENDED: 'ended'
},
ALIGNMENT: {
GOOD: 'good',
EVIL: 'evil'
},
MESSAGES: {
ENTER_NAME: 'Client must enter name.'
},
SOCKET_EVENTS: {
IN_GAME_MESSAGE: 'inGameMessage'
},
EVENT_IDS: {
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
END_GAME: 'endGame',
PLAYER_JOINED: 'playerJoined',
SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer',
PLAYER_LEFT: 'playerLeft',
ADD_SPECTATOR: 'addSpectator',
UPDATE_SPECTATORS: 'updateSpectators',
RESTART_GAME: 'restartGame',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
KICK_PERSON: 'kickPerson',
UPDATE_GAME_ROLES: 'updateGameRoles',
LEAVE_ROOM: 'leaveRoom'
},
TIMER_EVENTS: function () {
return [
this.EVENT_IDS.PAUSE_TIMER,
this.EVENT_IDS.RESUME_TIMER,
this.EVENT_IDS.GET_TIME_REMAINING,
this.EVENT_IDS.END_TIMER
];
},
LOBBY_EVENTS: function () {
return [
this.EVENT_IDS.PLAYER_JOINED,
this.EVENT_IDS.ADD_SPECTATOR,
this.EVENT_IDS.KICK_PERSON,
this.EVENT_IDS.UPDATE_GAME_ROLES,
this.EVENT_IDS.LEAVE_ROOM
];
},
IN_PROGRESS_EVENTS: function () {
return [
this.EVENT_IDS.KILL_PLAYER,
this.EVENT_IDS.REVEAL_PLAYER,
this.EVENT_IDS.ADD_SPECTATOR
];
},
USER_TYPES: {
MODERATOR: 'moderator',
PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed',
SPECTATOR: 'spectator',
BOT: 'bot'
},
ENVIRONMENT: {
LOCAL: 'local',
PRODUCTION: 'production'
},
USER_TYPE_ICONS: {
player: ' \uD83C\uDFAE',
moderator: ' \uD83D\uDC51',
'temp mod': ' \uD83C\uDFAE\uD83D\uDC51',
spectator: ' \uD83D\uDC7B',
killed: ' \uD83D\uDC80',
bot: ' \uD83E\uDD16'
}
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id'
};
export const STATUS = {
LOBBY: 'lobby',
IN_PROGRESS: 'in progress',
ENDED: 'ended'
};
export const ALIGNMENT = {
GOOD: 'good',
EVIL: 'evil'
};
export const MESSAGES = {
ENTER_NAME: 'Client must enter name.'
};
export const SOCKET_EVENTS = {
IN_GAME_MESSAGE: 'inGameMessage'
};
export const USER_TYPES = {
MODERATOR: 'moderator',
PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed',
SPECTATOR: 'spectator',
BOT: 'bot'
};
export const ENVIRONMENTS = {
LOCAL: 'local',
PRODUCTION: 'production'
};
export const USER_TYPE_ICONS = {
player: ' \uD83C\uDFAE',
moderator: ' \uD83D\uDC51',
'temp mod': ' \uD83C\uDFAE\uD83D\uDC51',
spectator: ' \uD83D\uDC7B',
killed: ' \uD83D\uDC80',
bot: ' \uD83E\uDD16'
};
export const EVENT_IDS = {
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
END_TIMER: 'endTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
END_GAME: 'endGame',
PLAYER_JOINED: 'playerJoined',
SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer',
PLAYER_LEFT: 'playerLeft',
ADD_SPECTATOR: 'addSpectator',
UPDATE_SPECTATORS: 'updateSpectators',
RESTART_GAME: 'restartGame',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
KICK_PERSON: 'kickPerson',
UPDATE_GAME_ROLES: 'updateGameRoles',
LEAVE_ROOM: 'leaveRoom'
};
export const TIMER_EVENTS = function () {
return [
EVENT_IDS.PAUSE_TIMER,
EVENT_IDS.RESUME_TIMER,
EVENT_IDS.GET_TIME_REMAINING,
EVENT_IDS.END_TIMER
];
};
export const LOBBY_EVENTS = function () {
return [
EVENT_IDS.PLAYER_JOINED,
EVENT_IDS.ADD_SPECTATOR,
EVENT_IDS.KICK_PERSON,
EVENT_IDS.UPDATE_GAME_ROLES,
EVENT_IDS.LEAVE_ROOM
];
};
export const IN_PROGRESS_EVENTS = function () {
return [
EVENT_IDS.KILL_PLAYER,
EVENT_IDS.REVEAL_PLAYER,
EVENT_IDS.ADD_SPECTATOR
];
};

View File

@@ -1,5 +1,12 @@
export class Game {
constructor (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams = null, isTestGame = false) {
constructor (
deck,
hasTimer,
hasDedicatedModerator,
moderatorName,
timerParams = null,
isTestGame = false
) {
this.deck = deck;
this.hasTimer = hasTimer;
this.timerParams = timerParams;

View File

@@ -1,4 +1,4 @@
import { globals } from '../../config/globals.js';
import { ALIGNMENT } from '../../config/globals.js';
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { toast } from '../front_end_components/Toast.js';
import { ModalManager } from '../front_end_components/ModalManager.js';
@@ -147,7 +147,7 @@ export class DeckStateManager {
}
const sortedDeck = this.deck.sort((a, b) => {
if (a.team !== b.team) {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
return a.team === ALIGNMENT.GOOD ? -1 : 1;
}
return a.role.localeCompare(b.role);
});
@@ -187,10 +187,10 @@ export class DeckStateManager {
roleEl.dataset.roleId = sortedDeck[i].id;
roleEl.classList.add('added-role');
roleEl.innerHTML = HTMLFragments.DECK_SELECT_ROLE_ADDED_TO_DECK;
if (sortedDeck[i].team === globals.ALIGNMENT.GOOD) {
roleEl.classList.add(globals.ALIGNMENT.GOOD);
if (sortedDeck[i].team === ALIGNMENT.GOOD) {
roleEl.classList.add(ALIGNMENT.GOOD);
} else {
roleEl.classList.add(globals.ALIGNMENT.EVIL);
roleEl.classList.add(ALIGNMENT.EVIL);
}
populateRoleElementInfo(roleEl, sortedDeck, i);
document.getElementById('deck-list').appendChild(roleEl);
@@ -219,10 +219,10 @@ export class DeckStateManager {
if (e.type === 'click' || e.code === 'Enter') {
const alignmentEl = document.getElementById('custom-role-info-modal-alignment');
const nameEl = document.getElementById('custom-role-info-modal-name');
alignmentEl.classList.remove(globals.ALIGNMENT.GOOD);
alignmentEl.classList.remove(globals.ALIGNMENT.EVIL);
nameEl.classList.remove(globals.ALIGNMENT.GOOD);
nameEl.classList.remove(globals.ALIGNMENT.EVIL);
alignmentEl.classList.remove(ALIGNMENT.GOOD);
alignmentEl.classList.remove(ALIGNMENT.EVIL);
nameEl.classList.remove(ALIGNMENT.GOOD);
nameEl.classList.remove(ALIGNMENT.EVIL);
e.preventDefault();
nameEl.innerText = sortedDeck[i].role;
nameEl.classList.add(sortedDeck[i].team);

View File

@@ -1,8 +1,7 @@
import { Game } from '../../model/Game.js';
import { cancelCurrentToast, toast } from '../front_end_components/Toast.js';
import { ModalManager } from '../front_end_components/ModalManager.js';
import { XHRUtility } from '../utility/XHRUtility.js';
import { globals } from '../../config/globals.js';
import { ALIGNMENT } from '../../config/globals.js';
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { UserUtility } from '../utility/UserUtility.js';
import { RoleBox } from './RoleBox.js';
@@ -118,50 +117,42 @@ export class GameCreationStepManager {
button.classList.remove('submitted');
button.addEventListener('click', this.steps['5'].forwardHandler);
};
XHRUtility.xhr(
fetch(
'/api/games/create',
'POST',
null,
JSON.stringify(
new Game(
this.currentGame.deck.filter((card) => card.quantity > 0),
this.currentGame.hasTimer,
this.currentGame.hasDedicatedModerator,
this.currentGame.moderatorName,
this.currentGame.timerParams,
this.currentGame.isTestGame
{
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(
new Game(
this.currentGame.deck.filter((card) => card.quantity > 0),
this.currentGame.hasTimer,
this.currentGame.hasDedicatedModerator,
this.currentGame.moderatorName,
this.currentGame.timerParams,
this.currentGame.isTestGame
)
)
)
)
.then((res) => {
if (res.status === 201) {
try {
const json = JSON.parse(res.content);
UserUtility.setAnonymousUserId(json.cookie, json.environment);
window.location.replace(
window.location.protocol + '//' + window.location.host +
'/game/' + json.accessCode
);
} catch (e) {
restoreButton();
toast(
'There was a problem creating the game. Please contact the developers.',
'error',
true,
true
);
}
}
}).catch((e) => {
restoreButton();
if (e.status === 429) {
toast('You\'ve sent this request too many times.', 'error', true, true, 'medium');
} else if (e.status === 413) {
toast('Your request is too large.', 'error', true, true);
} else {
toast(e.content, 'error', true, true, 'medium');
}
restoreButton();
if (e.status === 429) {
toast('You\'ve sent this request too many times.', 'error', true, true, 'medium');
} else if (e.status === 413) {
toast('Your request is too large.', 'error', true, true);
} else {
toast(e.content, 'error', true, true, 'medium');
}
}).then(res => {
res.json().then(json => {
UserUtility.setAnonymousUserId(json.cookie, json.environment);
window.location.replace(
window.location.protocol + '//' + window.location.host +
'/game/' + json.accessCode
);
});
});
}
}
};
@@ -442,10 +433,10 @@ function renderReviewAndCreateStep (containerId, stepNumber, game, deckManager)
for (const card of game.deck) {
const roleEl = document.createElement('div');
roleEl.innerText = card.quantity + 'x ' + card.role;
if (card.team === globals.ALIGNMENT.GOOD) {
roleEl.classList.add(globals.ALIGNMENT.GOOD);
if (card.team === ALIGNMENT.GOOD) {
roleEl.classList.add(ALIGNMENT.GOOD);
} else {
roleEl.classList.add(globals.ALIGNMENT.EVIL);
roleEl.classList.add(ALIGNMENT.EVIL);
}
div.querySelector('#roles-option').appendChild(roleEl);
}
@@ -552,7 +543,7 @@ function initializeRemainingEventListeners (deckManager, roleBox) {
roleBox.createMode = true;
roleBox.currentlyEditingRoleName = null;
document.getElementById('role-name').value = '';
document.getElementById('role-alignment').value = globals.ALIGNMENT.GOOD;
document.getElementById('role-alignment').value = ALIGNMENT.GOOD;
document.getElementById('role-description').value = '';
ModalManager.displayModal(
'role-modal',

View File

@@ -1,5 +1,5 @@
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { globals } from '../../config/globals.js';
import { ALIGNMENT, PRIMITIVES } from '../../config/globals.js';
import { defaultRoles } from '../../config/defaultRoles.js';
import { toast } from '../front_end_components/Toast.js';
import { ModalManager } from '../front_end_components/ModalManager.js';
@@ -8,7 +8,6 @@ import { Confirmation } from '../front_end_components/Confirmation.js';
export class RoleBox {
constructor (container, deckManager) {
this.createMode = false;
this.currentlyEditingRoleName = null;
this.category = 'default';
this.deckManager = deckManager;
this.defaultRoles = [];
@@ -32,7 +31,7 @@ export class RoleBox {
loadDefaultRoles = () => {
this.defaultRoles = defaultRoles.sort((a, b) => {
if (a.team !== b.team) {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
return a.team === ALIGNMENT.GOOD ? -1 : 1;
}
return a.role.localeCompare(b.role);
}).map((role) => {
@@ -174,9 +173,9 @@ export class RoleBox {
defaultRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE_DEFAULT;
defaultRole.classList.add('default-role');
defaultRole.dataset.roleId = this.defaultRoles[i].id;
const alignmentClass = this.defaultRoles[i].team === globals.ALIGNMENT.GOOD
? globals.ALIGNMENT.GOOD
: globals.ALIGNMENT.EVIL;
const alignmentClass = this.defaultRoles[i].team === ALIGNMENT.GOOD
? ALIGNMENT.GOOD
: ALIGNMENT.EVIL;
defaultRole.classList.add(alignmentClass);
defaultRole.querySelector('.role-name').innerText = this.defaultRoles[i].role;
selectEl.appendChild(defaultRole);
@@ -190,7 +189,7 @@ export class RoleBox {
this.categoryTransition.play();
this.customRoles.sort((a, b) => {
if (a.team !== b.team) {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
return a.team === ALIGNMENT.GOOD ? -1 : 1;
}
return a.role.localeCompare(b.role);
});
@@ -200,7 +199,7 @@ export class RoleBox {
customRole.innerHTML = HTMLFragments.DECK_SELECT_ROLE;
customRole.classList.add('custom-role');
customRole.dataset.roleId = this.customRoles[i].id;
const alignmentClass = this.customRoles[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL;
const alignmentClass = this.customRoles[i].team === ALIGNMENT.GOOD ? ALIGNMENT.GOOD : ALIGNMENT.EVIL;
customRole.classList.add(alignmentClass);
customRole.querySelector('.role-name').innerText = this.customRoles[i].role;
selectEl.appendChild(customRole);
@@ -267,10 +266,10 @@ export class RoleBox {
if (e.type === 'click' || e.code === 'Enter') {
const alignmentEl = document.getElementById('custom-role-info-modal-alignment');
const nameEl = document.getElementById('custom-role-info-modal-name');
alignmentEl.classList.remove(globals.ALIGNMENT.GOOD);
alignmentEl.classList.remove(globals.ALIGNMENT.EVIL);
nameEl.classList.remove(globals.ALIGNMENT.GOOD);
nameEl.classList.remove(globals.ALIGNMENT.EVIL);
alignmentEl.classList.remove(ALIGNMENT.GOOD);
alignmentEl.classList.remove(ALIGNMENT.EVIL);
nameEl.classList.remove(ALIGNMENT.GOOD);
nameEl.classList.remove(ALIGNMENT.EVIL);
e.preventDefault();
let role;
if (isCustom) {
@@ -351,7 +350,7 @@ export class RoleBox {
function createRandomId () {
let id = '';
for (let i = 0; i < 50; i ++) {
id += globals.CHAR_POOL[Math.floor(Math.random() * globals.CHAR_POOL.length)];
id += PRIMITIVES.CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.CHAR_POOL.length)];
}
return id;
}
@@ -365,9 +364,9 @@ function validateCustomRoleCookie (cookie) {
if (Array.isArray(cookieJSON)) {
for (const entry of cookieJSON) {
if (entry !== null && typeof entry === 'object') {
if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH
|| typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL)
|| typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
if (typeof entry.role !== 'string' || entry.role.length > PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH
|| typeof entry.team !== 'string' || (entry.team !== ALIGNMENT.GOOD && entry.team !== ALIGNMENT.EVIL)
|| typeof entry.description !== 'string' || entry.description.length > PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
) {
return false;
}

View File

@@ -1,10 +1 @@
/* It started getting confusing where I am reading/writing to the game state, and thus the state started to get inconsistent.
Creating a bucket to hold it so I can overwrite the gameState object whilst still preserving a reference to the containing bucket.
Now several components can read a shared game state.
*/
export const stateBucket = {
joinRequestInFlight: true,
accessCode: null,
currentGameState: null,
environment: null
};
export const stateBucket = {};

View File

@@ -1,4 +1,4 @@
import { globals } from '../../../config/globals.js';
import { USER_TYPES } from '../../../config/globals.js';
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
import { SharedStateUtil } from './shared/SharedStateUtil.js';
@@ -12,14 +12,14 @@ export class Ended {
renderEndOfGame (gameState) {
if (
gameState.client.userType === globals.USER_TYPES.MODERATOR
|| gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
gameState.client.userType === USER_TYPES.MODERATOR
|| gameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
) {
document.getElementById('end-of-game-buttons').prepend(SharedStateUtil.createReturnToLobbyButton(this.stateBucket));
}
SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR));
.find((person) => person.userType === USER_TYPES.MODERATOR
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR));
this.renderPlayersWithRoleInformation();
}
@@ -31,7 +31,7 @@ export class Ended {
const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null;
renderGroupOfPlayers(
this.stateBucket.currentGameState.people.filter(
p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
p => (p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR)
|| p.killed
),
this.stateBucket.currentGameState.accessCode,

View File

@@ -1,5 +1,12 @@
import { toast } from '../../front_end_components/Toast.js';
import { globals } from '../../../config/globals.js';
import {
STATUS,
EVENT_IDS,
SOCKET_EVENTS,
USER_TYPE_ICONS,
USER_TYPES,
ALIGNMENT
} from '../../../config/globals.js';
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
import { Confirmation } from '../../front_end_components/Confirmation.js';
import { ModalManager } from '../../front_end_components/ModalManager.js';
@@ -18,25 +25,25 @@ export class InProgress {
setUserView (userType) {
switch (userType) {
case globals.USER_TYPES.PLAYER:
case USER_TYPES.PLAYER:
this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
this.renderPlayerView();
break;
case globals.USER_TYPES.KILLED_PLAYER:
case USER_TYPES.KILLED_PLAYER:
this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
this.renderPlayerView(true);
break;
case globals.USER_TYPES.MODERATOR:
case USER_TYPES.MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
this.container.innerHTML = HTMLFragments.MODERATOR_GAME_VIEW;
this.renderModeratorView();
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
case USER_TYPES.TEMPORARY_MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
this.container.innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW;
this.renderTempModView();
break;
case globals.USER_TYPES.SPECTATOR:
case USER_TYPES.SPECTATOR:
this.container.innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW;
this.renderSpectatorView();
break;
@@ -46,12 +53,12 @@ export class InProgress {
if (this.stateBucket.currentGameState.timerParams) {
this.socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.GET_TIME_REMAINING,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.GET_TIME_REMAINING,
this.stateBucket.currentGameState.accessCode
);
setTimeout(() => {
if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) {
if (this.socket.hasListeners(EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) {
document.getElementById('game-timer').innerText = 'Timer not found.';
document.getElementById('game-timer').classList.add('timer-error');
}
@@ -68,7 +75,7 @@ export class InProgress {
Confirmation(
SharedStateUtil.buildSpectatorList(
this.stateBucket.currentGameState.people
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR),
.filter(p => p.userType === USER_TYPES.SPECTATOR),
this.stateBucket.currentGameState.client,
this.socket,
this.stateBucket.currentGameState),
@@ -80,7 +87,7 @@ export class InProgress {
if (spectatorCount) {
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length,
spectatorCount
);
spectatorCount?.addEventListener('click', spectatorHandler);
@@ -92,13 +99,13 @@ export class InProgress {
if (isKilled) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
clientUserType.innerText = USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
}
renderPlayerRole(this.stateBucket.currentGameState);
SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR));
.find((person) => person.userType === USER_TYPES.MODERATOR
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR));
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
@@ -116,7 +123,7 @@ export class InProgress {
: null;
this.renderGroupOfPlayers(
this.stateBucket.currentGameState.people.filter(
p => (p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
p => (p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR)
|| p.killed
),
this.killPlayerHandlers,
@@ -174,34 +181,34 @@ export class InProgress {
renderSpectatorView () {
SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR));
.find((person) => person.userType === USER_TYPES.MODERATOR
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR));
this.renderPlayersWithNoRoleInformationUnlessRevealed();
}
setSocketHandlers () {
this.socket.on(globals.EVENT_IDS.KILL_PLAYER, (killedPlayer) => {
this.socket.on(EVENT_IDS.KILL_PLAYER, (killedPlayer) => {
this.stateBucket.currentGameState.people = this.stateBucket.currentGameState.people
.map(person => person.id === killedPlayer.id ? killedPlayer : person);
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) {
toast(killedPlayer.name + ' killed.', 'success', true, true, 'medium');
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED);
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === STATUS.ENDED);
} else {
if (killedPlayer.id === this.stateBucket.currentGameState.client.id) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
clientUserType.innerText = USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
this.updatePlayerCardToKilledState();
toast('You have been killed!', 'warning', true, true, 'medium');
} else {
toast(killedPlayer.name + ' was killed!', 'warning', true, true, 'medium');
if (killedPlayer.userType === globals.USER_TYPES.MODERATOR
&& this.stateBucket.currentGameState.client.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (killedPlayer.userType === USER_TYPES.MODERATOR
&& this.stateBucket.currentGameState.client.userType !== USER_TYPES.TEMPORARY_MODERATOR) {
SharedStateUtil.displayCurrentModerator(killedPlayer);
}
}
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
this.removePlayerListEventListeners(false);
} else {
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
@@ -209,27 +216,27 @@ export class InProgress {
}
});
this.socket.on(globals.EVENT_IDS.REVEAL_PLAYER, (revealData) => {
this.socket.on(EVENT_IDS.REVEAL_PLAYER, (revealData) => {
const revealedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === revealData.id);
if (revealedPerson) {
revealedPerson.revealed = true;
revealedPerson.gameRole = revealData.gameRole;
revealedPerson.alignment = revealData.alignment;
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) {
if (revealedPerson.id === this.stateBucket.currentGameState.client.id) {
toast('You revealed your role.', 'success', true, true, 'medium');
} else {
toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium');
}
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED);
this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === STATUS.ENDED);
} else {
if (revealedPerson.id === this.stateBucket.currentGameState.client.id) {
toast('Your role has been revealed!', 'warning', true, true, 'medium');
} else {
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium');
}
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
@@ -238,14 +245,14 @@ export class InProgress {
}
});
this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.socket.on(EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.stateBucket.currentGameState.people.push(spectator);
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
this.stateBucket.currentGameState.people.filter(p => p.userType === 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) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
this.displayAvailableModerators();
}
});
@@ -269,13 +276,13 @@ export class InProgress {
return a.name >= b.name ? 1 : -1;
});
const teamGood = this.stateBucket.currentGameState.people.filter(
(p) => p.alignment === globals.ALIGNMENT.GOOD
&& ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
(p) => p.alignment === ALIGNMENT.GOOD
&& ((p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR)
|| p.killed)
);
const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === globals.ALIGNMENT.EVIL
&& ((p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR)
const teamEvil = this.stateBucket.currentGameState.people.filter((p) => p.alignment === ALIGNMENT.EVIL
&& ((p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR)
|| p.killed)
);
this.renderGroupOfPlayers(
@@ -283,7 +290,7 @@ export class InProgress {
this.killPlayerHandlers,
this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.EVIL,
ALIGNMENT.EVIL,
this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket
@@ -293,7 +300,7 @@ export class InProgress {
this.killPlayerHandlers,
this.revealRoleHandlers,
this.stateBucket.currentGameState.accessCode,
globals.ALIGNMENT.GOOD,
ALIGNMENT.GOOD,
this.stateBucket.currentGameState.people.find(person =>
person.id === this.stateBucket.currentGameState.currentModeratorId).userType,
this.socket
@@ -328,7 +335,7 @@ export class InProgress {
const roleElement = playerEl.querySelector('.game-player-role');
// Add role/alignment indicators if necessary
if (moderatorType === globals.USER_TYPES.MODERATOR || player.revealed) {
if (moderatorType === USER_TYPES.MODERATOR || player.revealed) {
if (alignment === null) {
roleElement.classList.add(player.alignment);
} else {
@@ -349,10 +356,10 @@ export class InProgress {
} else if (!player.out && moderatorType) {
killPlayerHandlers[player.id] = () => {
Confirmation('Kill \'' + player.name + '\'?', () => {
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 });
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, 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 });
socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.KILL_PLAYER, accessCode, { personId: player.id });
}
});
};
@@ -368,13 +375,13 @@ export class InProgress {
} else if (!player.revealed && moderatorType) {
revealRoleHandlers[player.id] = () => {
Confirmation('Reveal \'' + player.name + '\'?', () => {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.REVEAL_PLAYER, accessCode, { personId: player.id });
socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.REVEAL_PLAYER, accessCode, { personId: player.id });
});
};
playerEl.querySelector('.reveal-role-button').addEventListener('click', revealRoleHandlers[player.id]);
}
const playerListContainerId = moderatorType === globals.USER_TYPES.MODERATOR
const playerListContainerId = moderatorType === USER_TYPES.MODERATOR
? 'player-list-moderator-team-' + alignment
: 'game-player-list';
@@ -417,7 +424,7 @@ export class InProgress {
function renderPlayerRole (gameState) {
const name = document.querySelector('#role-name');
name.innerText = gameState.client.gameRole;
if (gameState.client.alignment === globals.ALIGNMENT.GOOD) {
if (gameState.client.alignment === ALIGNMENT.GOOD) {
document.getElementById('game-role').classList.add('game-role-good');
name.classList.add('good');
} else {
@@ -498,8 +505,8 @@ function createEndGamePromptComponent (socket, stateBucket) {
e.preventDefault();
Confirmation('End the game?', () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.END_GAME,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.END_GAME,
stateBucket.currentGameState.accessCode,
null,
() => {
@@ -531,14 +538,14 @@ 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.userType === globals.USER_TYPES.KILLED_PLAYER || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
if ((member.userType === USER_TYPES.KILLED_PLAYER || member.userType === USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
const container = document.createElement('div');
container.classList.add('potential-moderator');
container.setAttribute('tabindex', '0');
container.dataset.pointer = member.id;
container.innerHTML =
'<div class=\'potential-mod-name\'></div>' +
'<div>' + member.userType + ' ' + globals.USER_TYPE_ICONS[member.userType] + ' </div>';
'<div>' + member.userType + ' ' + USER_TYPE_ICONS[member.userType] + ' </div>';
container.querySelector('.potential-mod-name').innerText = member.name;
transferModHandlers[member.id] = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
@@ -549,16 +556,16 @@ function renderPotentialMods (gameState, group, transferModHandlers, socket) {
transferPrompt.innerHTML = '';
}
socket.timeout(5000).emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.TRANSFER_MODERATOR,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.TRANSFER_MODERATOR,
gameState.accessCode,
{ personId: member.id },
(err) => {
if (err) {
console.error(err);
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
gameState.accessCode,
{ personId: gameState.client.cookie },
(gameState) => {

View File

@@ -1,6 +1,6 @@
import { QRCode } from '../../third_party/qrcode.js';
import { toast } from '../../front_end_components/Toast.js';
import { globals } from '../../../config/globals.js';
import { EVENT_IDS, SOCKET_EVENTS, USER_TYPE_ICONS, USER_TYPES } from '../../../config/globals.js';
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
import { Confirmation } from '../../front_end_components/Confirmation.js';
import { SharedStateUtil } from './shared/SharedStateUtil.js';
@@ -25,15 +25,15 @@ export class Lobby {
}
Confirmation('Start game and deal roles?', () => {
socket.timeout(5000).emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.START_GAME,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.START_GAME,
stateBucket.currentGameState.accessCode,
null,
(err) => {
if (err) {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
(gameState) => {
@@ -52,8 +52,8 @@ export class Lobby {
e.preventDefault();
Confirmation('Leave the room?', () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.LEAVE_ROOM,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.LEAVE_ROOM,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.id }
);
@@ -86,8 +86,8 @@ export class Lobby {
document.querySelector('#role-edit-container-background')?.remove();
document.getElementById('game-content').style.display = 'flex';
this.socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.UPDATE_GAME_ROLES,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.UPDATE_GAME_ROLES,
stateBucket.currentGameState.accessCode,
{ deck: this.gameCreationStepManager.deckManager.deck.filter((card) => card.quantity > 0) },
() => {
@@ -130,7 +130,7 @@ export class Lobby {
const playerCount = this.container.querySelector('#game-player-count');
playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players';
const inLobbyCount = this.stateBucket.currentGameState.people.filter(
p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR
p => p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR
).length;
document.querySelector("label[for='lobby-players']").innerText =
'Participants (' + inLobbyCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)';
@@ -149,7 +149,7 @@ export class Lobby {
if (e.type === 'click' || e.code === 'Enter') {
Confirmation(
SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR),
.filter(p => p.userType === USER_TYPES.SPECTATOR),
this.stateBucket.currentGameState.client,
this.socket,
this.stateBucket.currentGameState),
@@ -162,7 +162,7 @@ export class Lobby {
this.container.querySelector('#spectator-count').addEventListener('keyup', spectatorHandler);
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length,
this.container.querySelector('#spectator-count')
);
@@ -180,54 +180,54 @@ export class Lobby {
const lobbyPlayersContainer = this.container.querySelector('#lobby-players');
const sorted = this.stateBucket.currentGameState.people.sort(
function (a, b) {
if (a.userType === globals.USER_TYPES.MODERATOR || a.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (a.userType === USER_TYPES.MODERATOR || a.userType === USER_TYPES.TEMPORARY_MODERATOR) {
return -1;
}
return 1;
}
);
for (const person of sorted.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR)) {
for (const person of sorted.filter(p => p.userType !== USER_TYPES.SPECTATOR)) {
lobbyPlayersContainer.appendChild(renderLobbyPerson(person, this.stateBucket.currentGameState, this.socket));
}
const playerCount = this.stateBucket.currentGameState.people.filter(
p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR
p => p.userType !== USER_TYPES.MODERATOR && p.userType !== USER_TYPES.SPECTATOR
).length;
document.querySelector("label[for='lobby-players']").innerText =
'Participants (' + playerCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)';
}
setSocketHandlers () {
this.socket.on(globals.EVENT_IDS.PLAYER_JOINED, (player, gameIsStartable) => {
this.socket.on(EVENT_IDS.PLAYER_JOINED, (player, gameIsStartable) => {
toast(player.name + ' joined!', 'success', true, true, 'short');
this.stateBucket.currentGameState.people.push(player);
this.stateBucket.currentGameState.isStartable = gameIsStartable;
this.populatePlayers();
if ((
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
)
) {
this.displayStartGamePromptForModerators();
}
});
this.socket.on(globals.EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.socket.on(EVENT_IDS.ADD_SPECTATOR, (spectator) => {
this.stateBucket.currentGameState.people.push(spectator);
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length,
document.getElementById('spectator-count')
);
});
this.socket.on(globals.EVENT_IDS.KICK_PERSON, (kickedId, gameIsStartable) => {
this.socket.on(EVENT_IDS.KICK_PERSON, (kickedId, gameIsStartable) => {
if (kickedId === this.stateBucket.currentGameState.client.id) {
window.location = '/?message=' + encodeURIComponent('You were kicked by the moderator.');
} else {
this.handlePersonExiting(kickedId, gameIsStartable, globals.EVENT_IDS.KICK_PERSON);
this.handlePersonExiting(kickedId, gameIsStartable, EVENT_IDS.KICK_PERSON);
}
});
this.socket.on(globals.EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize, isStartable) => {
this.socket.on(EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize, isStartable) => {
this.stateBucket.currentGameState.deck = deck;
this.stateBucket.currentGameState.gameSize = gameSize;
this.stateBucket.currentGameState.isStartable = isStartable;
@@ -235,11 +235,11 @@ export class Lobby {
this.setPlayerCount();
});
this.socket.on(globals.EVENT_IDS.LEAVE_ROOM, (leftId, gameIsStartable) => {
this.socket.on(EVENT_IDS.LEAVE_ROOM, (leftId, gameIsStartable) => {
if (leftId === this.stateBucket.currentGameState.client.id) {
window.location = '/?message=' + encodeURIComponent('You left the room.');
} else {
this.handlePersonExiting(leftId, gameIsStartable, globals.EVENT_IDS.LEAVE_ROOM);
this.handlePersonExiting(leftId, gameIsStartable, EVENT_IDS.LEAVE_ROOM);
}
});
}
@@ -252,18 +252,18 @@ export class Lobby {
}
this.stateBucket.currentGameState.isStartable = gameIsStartable;
SharedStateUtil.setNumberOfSpectators(
this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length,
this.stateBucket.currentGameState.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length,
document.getElementById('spectator-count')
);
this.populatePlayers();
if ((
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
)
) {
toast(
event === globals.EVENT_IDS.LEAVE_ROOM ? 'A player left.' : 'Player kicked.',
event === globals.EVENT_IDS.LEAVE_ROOM ? 'warning' : 'success',
event === EVENT_IDS.LEAVE_ROOM ? 'A player left.' : 'Player kicked.',
event === EVENT_IDS.LEAVE_ROOM ? 'warning' : 'success',
true,
true,
'short'
@@ -358,17 +358,17 @@ function renderLobbyPerson (person, gameState, socket) {
personNameEl.classList.add('lobby-player-name');
const personTypeEl = document.createElement('div');
personNameEl.innerText = person.name;
personTypeEl.innerText = person.userType + globals.USER_TYPE_ICONS[person.userType];
personTypeEl.innerText = person.userType + USER_TYPE_ICONS[person.userType];
el.classList.add('lobby-player');
if (person.userType === globals.USER_TYPES.MODERATOR || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (person.userType === USER_TYPES.MODERATOR || person.userType === USER_TYPES.TEMPORARY_MODERATOR) {
el.classList.add('moderator');
}
el.appendChild(personNameEl);
el.appendChild(personTypeEl);
if ((gameState.client.userType === globals.USER_TYPES.MODERATOR || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR)
&& person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) {
if ((gameState.client.userType === USER_TYPES.MODERATOR || gameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR)
&& person.userType !== USER_TYPES.MODERATOR && person.userType !== USER_TYPES.TEMPORARY_MODERATOR) {
SharedStateUtil.addPlayerOptions(el, person, socket, gameState);
el.dataset.pointer = person.id;
}

View File

@@ -1,58 +1,41 @@
import { XHRUtility } from '../../../utility/XHRUtility.js';
import { UserUtility } from '../../../utility/UserUtility.js';
import { globals } from '../../../../config/globals.js';
import {
STATUS,
EVENT_IDS,
ENVIRONMENTS,
SOCKET_EVENTS,
USER_TYPE_ICONS,
USER_TYPES,
ALIGNMENT
} from '../../../../config/globals.js';
import { toast } from '../../../front_end_components/Toast.js';
import { Confirmation } from '../../../front_end_components/Confirmation.js';
import { Lobby } from '../Lobby.js';
import { stateBucket } from '../../StateBucket.js';
import { InProgress } from '../InProgress.js';
import { Ended } from '../Ended.js';
import { HTMLFragments } from '../../../front_end_components/HTMLFragments.js';
import { ModalManager } from '../../../front_end_components/ModalManager.js';
// This constant is meant to house logic that is utilized by more than one game state
export const SharedStateUtil = {
gameStateAckFn: (gameState, socket) => {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
true,
true
);
},
restartHandler: (stateBucket, status = globals.STATUS.IN_PROGRESS) => {
XHRUtility.xhr(
restartHandler: (stateBucket, status = STATUS.IN_PROGRESS) => {
fetch(
'/api/games/' + stateBucket.currentGameState.accessCode + '/restart?status=' + status,
'PATCH',
null,
JSON.stringify({
playerName: stateBucket.currentGameState.client.name,
accessCode: stateBucket.currentGameState.accessCode,
sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL),
localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION)
})
)
.then((res) => {})
.catch((res) => {
toast(res.content, 'error', true, true, 'medium');
});
},
createRestartButton: (stateBucket) => {
const restartGameButton = document.createElement('button');
restartGameButton.classList.add('app-button');
restartGameButton.setAttribute('id', 'restart-game-button');
restartGameButton.innerText = 'Quick Restart';
restartGameButton.addEventListener('click', () => {
Confirmation('Restart the game, dealing everyone new roles?', () => {
SharedStateUtil.restartHandler(stateBucket);
});
{
method: 'PATCH',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
playerName: stateBucket.currentGameState.client.name,
accessCode: stateBucket.currentGameState.accessCode,
sessionCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.LOCAL),
localCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.PRODUCTION)
})
}
).catch((res) => {
toast(res.content, 'error', true, true, 'medium');
});
return restartGameButton;
},
createReturnToLobbyButton: (stateBucket) => {
@@ -62,123 +45,18 @@ export const SharedStateUtil = {
returnToLobbyButton.innerText = 'Return to Lobby';
returnToLobbyButton.addEventListener('click', () => {
Confirmation('Return everyone to the Lobby?', () => {
SharedStateUtil.restartHandler(stateBucket, globals.STATUS.LOBBY);
SharedStateUtil.restartHandler(stateBucket, STATUS.LOBBY);
});
});
return returnToLobbyButton;
},
setClientSocketHandlers: (stateBucket, socket) => {
const startGameStateAckFn = (gameState) => {
SharedStateUtil.gameStateAckFn(gameState, socket);
toast('Game started!', 'success');
};
const restartGameStateAckFn = (gameState) => {
SharedStateUtil.gameStateAckFn(gameState, socket);
toast('Everyone has returned to the Lobby!', 'success');
};
const fetchGameStateHandler = (ackFn) => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
ackFn
);
};
socket.on(globals.EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); });
socket.on(globals.EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); });
socket.on(globals.EVENT_IDS.SYNC_GAME_STATE, () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
const oldUserType = stateBucket.currentGameState.client.userType;
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
true,
gameState.client.userType !== oldUserType
);
}
);
});
socket.on(globals.COMMANDS.END_GAME, (people) => {
stateBucket.currentGameState.people = people;
stateBucket.currentGameState.status = globals.STATUS.ENDED;
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
true,
true
);
});
},
syncWithGame: (socket, cookie, window) => {
const splitUrl = window.location.href.split('/game/');
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
socket.timeout(5000).emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
accessCode,
{ personId: cookie },
(err, gameState) => {
if (err) {
SharedStateUtil.retrySync(accessCode, socket, cookie);
} else {
SharedStateUtil.handleGameState(gameState, cookie, socket);
}
}
);
} else {
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
}
},
retrySync: (accessCode, socket, cookie) => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
accessCode,
{ personId: cookie },
(gameState) => {
SharedStateUtil.handleGameState(gameState, cookie, socket);
}
);
},
handleGameState: (gameState, cookie, socket) => {
if (gameState === null) {
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
} else {
stateBucket.currentGameState = gameState;
document.querySelector('.spinner-container')?.remove();
document.querySelector('.spinner-background')?.remove();
document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 'short');
processGameState(stateBucket.currentGameState, cookie, socket, true, true);
}
},
addPlayerOptions: (personEl, person, socket, gameState) => {
const optionsButton = document.createElement('img');
const optionsHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
document.querySelector('#player-options-modal-title').innerText = person.name + globals.USER_TYPE_ICONS[person.userType];
document.querySelector('#player-options-modal-title').innerText = person.name + USER_TYPE_ICONS[person.userType];
document.getElementById('player-options-modal-content').innerHTML = '';
const kickOption = document.createElement('button');
kickOption.setAttribute('class', 'player-option');
@@ -187,8 +65,8 @@ export const SharedStateUtil = {
ModalManager.dispelModal('player-options-modal', 'player-options-modal-background');
Confirmation('Kick \'' + person.name + '\'?', () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.KICK_PERSON,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.KICK_PERSON,
gameState.accessCode,
{ personId: person.id }
);
@@ -216,7 +94,7 @@ export const SharedStateUtil = {
buildSpectatorList (people, client, socket, gameState) {
const list = document.createElement('div');
const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR);
const spectators = people.filter(p => p.userType === USER_TYPES.SPECTATOR);
if (spectators.length === 0) {
list.innerHTML = '<div>Nobody currently spectating.</div>';
} else {
@@ -224,11 +102,11 @@ export const SharedStateUtil = {
const spectatorEl = document.createElement('div');
spectatorEl.classList.add('spectator');
spectatorEl.innerHTML = '<div class=\'spectator-name\'></div>' +
'<div>' + 'spectator' + globals.USER_TYPE_ICONS.spectator + '</div>';
'<div>' + 'spectator' + USER_TYPE_ICONS.spectator + '</div>';
spectatorEl.querySelector('.spectator-name').innerText = spectator.name;
list.appendChild(spectatorEl);
if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (client.userType === USER_TYPES.MODERATOR || client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
this.addPlayerOptions(spectatorEl, spectator, socket, gameState);
spectatorEl.dataset.pointer = spectator.id;
}
@@ -246,126 +124,68 @@ export const SharedStateUtil = {
displayCurrentModerator: (moderator) => {
document.getElementById('current-moderator-name').innerText = moderator.name;
document.getElementById('current-moderator-type').innerText = moderator.userType + globals.USER_TYPE_ICONS[moderator.userType];
document.getElementById('current-moderator-type').innerText = moderator.userType + USER_TYPE_ICONS[moderator.userType];
},
returnHumanReadableTime: (milliseconds, tenthsOfSeconds = false) => {
const tenths = Math.floor((milliseconds / 100) % 10);
let seconds = Math.floor((milliseconds / 1000) % 60);
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
return tenthsOfSeconds
? hours + ':' + minutes + ':' + seconds + '.' + tenths
: hours + ':' + minutes + ':' + seconds;
},
activateRoleInfoButton: () => {
document.getElementById('role-info-button').addEventListener('click', (e) => {
const deck = stateBucket.currentGameState.deck;
deck.sort((a, b) => {
return a.team === ALIGNMENT.GOOD ? -1 : 1;
});
e.preventDefault();
document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
const modalContent = document.getElementById('game-role-info-container');
for (const card of deck) {
const roleDiv = document.createElement('div');
const roleNameDiv = document.createElement('div');
roleNameDiv.classList.add('role-info-name');
const roleName = document.createElement('h5');
const roleQuantity = document.createElement('h5');
const roleDescription = document.createElement('p');
roleDescription.innerText = card.description;
roleName.innerText = card.role;
roleQuantity.innerText = card.quantity + 'x';
if (card.team === ALIGNMENT.GOOD) {
roleName.classList.add(ALIGNMENT.GOOD);
} else {
roleName.classList.add(ALIGNMENT.EVIL);
}
roleNameDiv.appendChild(roleQuantity);
roleNameDiv.appendChild(roleName);
roleDiv.appendChild(roleNameDiv);
roleDiv.appendChild(roleDescription);
modalContent.appendChild(roleDiv);
}
ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
});
},
displayClientInfo: (name, userType) => {
document.getElementById('client-name').innerText = name;
document.getElementById('client-user-type').innerText = userType;
document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[userType];
}
};
function processGameState (
currentGameState,
userId,
socket,
refreshPrompt = true,
animateContainer = false
) {
if (animateContainer) {
document.getElementById('game-state-container').animate(
[
{ opacity: '0', transform: 'translateY(10px)' },
{ opacity: '1', transform: 'translateY(0px)' }
], {
duration: 500,
easing: 'ease-in-out',
fill: 'both'
});
document.getElementById('client-container').animate([
{ opacity: '0' },
{ opacity: '1' }
], {
duration: 500,
easing: 'ease-out',
fill: 'both'
});
}
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
switch (currentGameState.status) {
case globals.STATUS.LOBBY:
const lobby = new Lobby('game-state-container', stateBucket, socket);
if (refreshPrompt) {
lobby.removeStartGameFunctionalityIfPresent();
}
lobby.populateHeader();
lobby.populatePlayers();
globals.LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e));
lobby.setSocketHandlers();
if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (refreshPrompt) {
lobby.displayStartGamePromptForModerators();
}
document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL;
} else {
if (refreshPrompt) {
lobby.displayPlayerPrompt();
}
}
break;
case globals.STATUS.IN_PROGRESS:
if (refreshPrompt) {
document.querySelector('#game-control-prompt')?.remove();
document.querySelector('#leave-game-prompt')?.remove();
}
const inProgressGame = new InProgress('game-state-container', stateBucket, socket);
globals.IN_PROGRESS_EVENTS().forEach(e => socket.removeAllListeners(e));
inProgressGame.setSocketHandlers();
inProgressGame.setUserView(currentGameState.client.userType);
break;
case globals.STATUS.ENDED: {
const ended = new Ended('game-state-container', stateBucket, socket);
ended.renderEndOfGame(currentGameState);
break;
}
default:
break;
}
activateRoleInfoButton();
}
function activateRoleInfoButton () {
document.getElementById('role-info-button').addEventListener('click', (e) => {
const deck = stateBucket.currentGameState.deck;
deck.sort((a, b) => {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
});
e.preventDefault();
document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
const modalContent = document.getElementById('game-role-info-container');
for (const card of deck) {
const roleDiv = document.createElement('div');
const roleNameDiv = document.createElement('div');
roleNameDiv.classList.add('role-info-name');
const roleName = document.createElement('h5');
const roleQuantity = document.createElement('h5');
const roleDescription = document.createElement('p');
roleDescription.innerText = card.description;
roleName.innerText = card.role;
roleQuantity.innerText = card.quantity + 'x';
if (card.team === globals.ALIGNMENT.GOOD) {
roleName.classList.add(globals.ALIGNMENT.GOOD);
} else {
roleName.classList.add(globals.ALIGNMENT.EVIL);
}
roleNameDiv.appendChild(roleQuantity);
roleNameDiv.appendChild(roleName);
roleDiv.appendChild(roleNameDiv);
roleDiv.appendChild(roleDescription);
modalContent.appendChild(roleDiv);
}
ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
});
}
function displayClientInfo (name, userType) {
document.getElementById('client-name').innerText = name;
document.getElementById('client-user-type').innerText = userType;
document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType];
}

View File

@@ -3,41 +3,244 @@ import { stateBucket } from '../game_state/StateBucket.js';
import { UserUtility } from '../utility/UserUtility.js';
import { toast } from '../front_end_components/Toast.js';
import { SharedStateUtil } from '../game_state/states/shared/SharedStateUtil.js';
import {
EVENT_IDS,
IN_PROGRESS_EVENTS,
LOBBY_EVENTS,
PRIMITIVES,
SOCKET_EVENTS,
STATUS,
USER_TYPES
} from '../../config/globals.js';
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { Lobby } from '../game_state/states/Lobby.js';
import { InProgress } from '../game_state/states/InProgress.js';
import { Ended } from '../game_state/states/Ended.js';
export const gameHandler = async (socket, XHRUtility, window, gameDOM) => {
export const gameHandler = (socket, window, gameDOM) => {
document.body.innerHTML = gameDOM + document.body.innerHTML;
injectNavbar();
return new Promise((resolve, reject) => {
window.fetch(
'/api/games/environment',
{
method: 'GET',
mode: 'cors'
}
).catch((res) => {
reject(res.content);
}).then((response) => {
response.text().then((text) => {
stateBucket.environment = text;
socket.on('connect', () => {
if (stateBucket.timerWorker) {
stateBucket.timerWorker.terminate();
stateBucket.timerWorker = null;
}
syncWithGame(
socket,
UserUtility.validateAnonUserSignature(stateBucket.environment),
window
);
});
socket.on('connect_error', (err) => {
toast(err, 'error', true, false);
});
const response = await XHRUtility.xhr(
'/api/games/environment',
'GET',
null,
null
).catch((res) => {
toast(res.content, 'error', true);
socket.on('disconnect', () => {
toast('Disconnected. Attempting reconnect...', 'error', true, false);
});
setClientSocketHandlers(stateBucket, socket);
resolve();
});
});
});
};
stateBucket.environment = response.content;
function syncWithGame (socket, cookie, window) {
const splitUrl = window.location.href.split('/game/');
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === PRIMITIVES.ACCESS_CODE_LENGTH) {
socket.timeout(5000).emit(
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
accessCode,
{ personId: cookie },
(err, gameState) => {
if (err) {
retrySync(accessCode, socket, cookie);
} else {
handleGameState(gameState, cookie, socket);
}
}
);
} else {
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
}
}
socket.on('connect', function () {
if (stateBucket.timerWorker) {
stateBucket.timerWorker.terminate();
stateBucket.timerWorker = null;
function retrySync (accessCode, socket, cookie) {
socket.emit(
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
accessCode,
{ personId: cookie },
(gameState) => {
handleGameState(gameState, cookie, socket);
}
SharedStateUtil.syncWithGame(
);
}
function handleGameState (gameState, cookie, socket) {
if (gameState === null) {
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
} else {
stateBucket.currentGameState = gameState;
document.querySelector('.spinner-container')?.remove();
document.querySelector('.spinner-background')?.remove();
document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 'short');
processGameState(stateBucket.currentGameState, cookie, socket, true, true);
}
}
function processGameState (
currentGameState,
userId,
socket,
refreshPrompt = true,
animateContainer = false
) {
if (animateContainer) {
document.getElementById('game-state-container').animate(
[
{ opacity: '0', transform: 'translateY(10px)' },
{ opacity: '1', transform: 'translateY(0px)' }
], {
duration: 500,
easing: 'ease-in-out',
fill: 'both'
});
document.getElementById('client-container').animate([
{ opacity: '0' },
{ opacity: '1' }
], {
duration: 500,
easing: 'ease-out',
fill: 'both'
});
}
SharedStateUtil.displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
switch (currentGameState.status) {
case STATUS.LOBBY:
const lobby = new Lobby('game-state-container', stateBucket, socket);
if (refreshPrompt) {
lobby.removeStartGameFunctionalityIfPresent();
}
lobby.populateHeader();
lobby.populatePlayers();
LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e));
lobby.setSocketHandlers();
if (currentGameState.client.userType === USER_TYPES.MODERATOR
|| currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR) {
if (refreshPrompt) {
lobby.displayStartGamePromptForModerators();
}
document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL;
} else {
if (refreshPrompt) {
lobby.displayPlayerPrompt();
}
}
break;
case STATUS.IN_PROGRESS:
if (refreshPrompt) {
document.querySelector('#game-control-prompt')?.remove();
document.querySelector('#leave-game-prompt')?.remove();
}
const inProgressGame = new InProgress('game-state-container', stateBucket, socket);
IN_PROGRESS_EVENTS().forEach(e => socket.removeAllListeners(e));
inProgressGame.setSocketHandlers();
inProgressGame.setUserView(currentGameState.client.userType);
break;
case STATUS.ENDED: {
const ended = new Ended('game-state-container', stateBucket, socket);
ended.renderEndOfGame(currentGameState);
break;
}
default:
break;
}
SharedStateUtil.activateRoleInfoButton();
}
function setClientSocketHandlers (stateBucket, socket) {
const commonGameStateAckFn = (gameState, socket) => {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
UserUtility.validateAnonUserSignature(response.content),
window
true,
true
);
};
const startGameStateAckFn = (gameState) => {
commonGameStateAckFn(gameState, socket);
toast('Game started!', 'success');
};
const restartGameStateAckFn = (gameState) => {
commonGameStateAckFn(gameState, socket);
toast('Everyone has returned to the Lobby!', 'success');
};
const fetchGameStateHandler = (ackFn) => {
socket.emit(
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
ackFn
);
};
socket.on(EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); });
socket.on(EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); });
socket.on(EVENT_IDS.SYNC_GAME_STATE, () => {
socket.emit(
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
const oldUserType = stateBucket.currentGameState.client.userType;
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
true,
gameState.client.userType !== oldUserType
);
}
);
});
socket.on('connect_error', (err) => {
toast(err, 'error', true, false);
socket.on(EVENT_IDS.END_GAME, (people) => {
stateBucket.currentGameState.people = people;
stateBucket.currentGameState.status = STATUS.ENDED;
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
true,
true
);
});
socket.on('disconnect', () => {
toast('Disconnected. Attempting reconnect...', 'error', true, false);
});
SharedStateUtil.setClientSocketHandlers(stateBucket, socket);
};
}

View File

@@ -1,17 +1,18 @@
import { globals } from '../../config/globals.js';
import { EVENT_IDS, SOCKET_EVENTS, USER_TYPES, TIMER_EVENTS, PRIMITIVES } from '../../config/globals.js';
import { Confirmation } from '../front_end_components/Confirmation.js';
import { SharedStateUtil } from '../game_state/states/shared/SharedStateUtil.js';
export class GameTimerManager {
constructor (stateBucket, socket) {
this.stateBucket = stateBucket;
this.playListener = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.RESUME_TIMER, this.stateBucket.currentGameState.accessCode);
socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.RESUME_TIMER, this.stateBucket.currentGameState.accessCode);
}
};
this.pauseListener = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.PAUSE_TIMER, this.stateBucket.currentGameState.accessCode);
socket.emit(SOCKET_EVENTS.IN_GAME_MESSAGE, EVENT_IDS.PAUSE_TIMER, this.stateBucket.currentGameState.accessCode);
}
};
}
@@ -19,8 +20,8 @@ export class GameTimerManager {
resumeGameTimer (totalTime, tickRate, soundManager, timerWorker) {
if (window.Worker) {
if (
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
) {
this.swapToPauseButton();
}
@@ -33,8 +34,8 @@ export class GameTimerManager {
timer.classList.add('low');
}
timer.innerText = totalTime < 60000
? returnHumanReadableTime(totalTime, true)
: returnHumanReadableTime(totalTime);
? SharedStateUtil.returnHumanReadableTime(totalTime, true)
: SharedStateUtil.returnHumanReadableTime(totalTime);
timerWorker.onmessage = function (e) {
if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds >= 0) {
if (e.data.timeRemainingInMilliseconds === 0) {
@@ -54,47 +55,31 @@ export class GameTimerManager {
pauseGameTimer (timerWorker, timeRemaining) {
if (window.Worker) {
if (
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
) {
this.swapToPlayButton();
}
timerWorker.postMessage('stop');
const timer = document.getElementById('game-timer');
if (timeRemaining < 60000) {
timer.innerText = returnHumanReadableTime(timeRemaining, true);
timer.classList.add('paused-low');
timer.classList.add('low');
} else {
timer.innerText = returnHumanReadableTime(timeRemaining);
timer.classList.add('paused');
}
populateTimerElement(timeRemaining);
}
}
displayPausedTime (time) {
if (
this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
) {
this.swapToPlayButton();
}
const timer = document.getElementById('game-timer');
if (time < 60000) {
timer.innerText = returnHumanReadableTime(time, true);
timer.classList.add('paused-low');
timer.classList.add('low');
} else {
timer.innerText = returnHumanReadableTime(time);
timer.classList.add('paused');
}
populateTimerElement(time);
}
displayExpiredTime () {
if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
if (this.stateBucket.currentGameState.client.userType === USER_TYPES.TEMPORARY_MODERATOR
|| this.stateBucket.currentGameState.client.userType === USER_TYPES.MODERATOR) {
const currentBtn = document.querySelector('#timer-container-moderator #play-pause img');
if (currentBtn) {
currentBtn.removeEventListener('click', this.pauseListener);
@@ -113,31 +98,31 @@ export class GameTimerManager {
}
const timer = document.getElementById('game-timer');
timer.innerText = returnHumanReadableTime(0, true);
timer.innerText = SharedStateUtil.returnHumanReadableTime(0, true);
}
attachTimerSocketListeners (socket, timerWorker) {
globals.TIMER_EVENTS().forEach(e => socket.removeAllListeners(e));
TIMER_EVENTS().forEach(e => socket.removeAllListeners(e));
socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => {
socket.on(EVENT_IDS.PAUSE_TIMER, (timeRemaining) => {
this.pauseGameTimer(timerWorker, timeRemaining);
});
socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => {
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
socket.on(EVENT_IDS.RESUME_TIMER, (timeRemaining) => {
this.resumeGameTimer(timeRemaining, PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
});
socket.once(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => {
socket.once(EVENT_IDS.GET_TIME_REMAINING, (timeRemaining, paused) => {
if (paused) {
this.displayPausedTime(timeRemaining);
} else if (timeRemaining === 0) {
this.displayExpiredTime();
} else {
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
this.resumeGameTimer(timeRemaining, PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
}
});
socket.on(globals.COMMANDS.END_TIMER, () => {
socket.on(EVENT_IDS.END_TIMER, () => {
Confirmation('The timer has expired!');
});
}
@@ -178,17 +163,14 @@ export class GameTimerManager {
}
}
function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) {
const tenths = Math.floor((milliseconds / 100) % 10);
let seconds = Math.floor((milliseconds / 1000) % 60);
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
return tenthsOfSeconds
? hours + ':' + minutes + ':' + seconds + '.' + tenths
: hours + ':' + minutes + ':' + seconds;
function populateTimerElement (timeRemaining) {
const timer = document.getElementById('game-timer');
if (timeRemaining < 60000) {
timer.innerText = SharedStateUtil.returnHumanReadableTime(timeRemaining, true);
timer.classList.add('paused-low');
timer.classList.add('low');
} else {
timer.innerText = SharedStateUtil.returnHumanReadableTime(timeRemaining);
timer.classList.add('paused');
}
}

View File

@@ -80,6 +80,20 @@ class Timer {
}
}
}
function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) {
const tenths = Math.floor((milliseconds / 100) % 10);
let seconds = Math.floor((milliseconds / 1000) % 60);
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
return tenthsOfSeconds
? hours + ':' + minutes + ':' + seconds + '.' + tenths
: hours + ':' + minutes + ':' + seconds;
}
class Singleton {
constructor (totalTime, tickInterval) {
@@ -105,18 +119,3 @@ class Singleton {
}
}
}
function returnHumanReadableTime (milliseconds, tenthsOfSeconds = false) {
const tenths = Math.floor((milliseconds / 100) % 10);
let seconds = Math.floor((milliseconds / 1000) % 60);
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
return tenthsOfSeconds
? hours + ':' + minutes + ':' + seconds + '.' + tenths
: hours + ':' + minutes + ':' + seconds;
}

View File

@@ -1,4 +1,4 @@
import { globals } from '../../config/globals.js';
import { ENVIRONMENTS, PRIMITIVES } from '../../config/globals.js';
/*
we will use sessionStorage during local development to aid in testing, vs. localStorage for production.
@@ -7,25 +7,25 @@ import { globals } from '../../config/globals.js';
export const UserUtility = {
setAnonymousUserId (id, environment) {
if (environment === globals.ENVIRONMENT.LOCAL) {
sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id);
if (environment === ENVIRONMENTS.LOCAL) {
sessionStorage.setItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY, id);
} else {
localStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id);
localStorage.setItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY, id);
}
},
validateAnonUserSignature (environment) {
let userSig;
if (environment === globals.ENVIRONMENT.LOCAL) {
userSig = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
if (environment === ENVIRONMENTS.LOCAL) {
userSig = sessionStorage.getItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY);
} else {
userSig = localStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
userSig = localStorage.getItem(PRIMITIVES.PLAYER_ID_COOKIE_KEY);
}
return (
userSig
&& typeof userSig === 'string'
&& /^[a-zA-Z0-9]+$/.test(userSig)
&& userSig.length === globals.USER_SIGNATURE_LENGTH
&& userSig.length === PRIMITIVES.USER_SIGNATURE_LENGTH
)
? userSig
: false;

View File

@@ -1,39 +0,0 @@
export const XHRUtility =
{
standardHeaders: [['Content-Type', 'application/json'], ['Accept', 'application/json'], ['X-Requested-With', 'XMLHttpRequest']],
// Easily make XHR calls with a promise wrapper. Defaults to GET and MIME type application/JSON
xhr (url, method = 'GET', headers, body = null) {
if (headers === undefined || headers === null) {
headers = this.standardHeaders;
}
if (typeof url !== 'string' || url.trim().length < 1) {
return Promise.reject('Cannot request with empty URL: ' + url);
}
const req = new XMLHttpRequest();
req.open(method, url.trim());
for (const hdr of headers) {
if (hdr.length !== 2) continue;
req.setRequestHeader(hdr[0], hdr[1]);
}
return new Promise((resolve, reject) => {
req.onload = function () {
const response = {
status: this.status,
statusText: this.statusText,
content: this.responseText
};
if (this.status >= 200 && this.status < 400) {
resolve(response);
} else {
reject(response);
}
};
body ? req.send(body) : req.send();
});
}
};

View File

@@ -1,8 +1,10 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { gameHandler } from '../modules/page_handlers/gameHandler';
import { gameHandler } from '../modules/page_handlers/gameHandler.js';
import { io } from 'socket.io-client';
import { XHRUtility } from '../modules/utility/XHRUtility.js';
import gameTemplate from '../view_templates/GameTemplate.js';
import { toast } from '../modules/front_end_components/Toast.js';
await gameHandler(io('/in-game'), XHRUtility, window, gameTemplate);
gameHandler(io('/in-game'), window, gameTemplate).then(() => {
toast('Connecting...', 'warning', true, false);
}).catch((e) => {
toast(e, 'error', true);
});

View File

@@ -1,4 +1,3 @@
import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { toast } from '../modules/front_end_components/Toast.js';
import { injectNavbar } from '../modules/front_end_components/Navbar.js';
import { Confirmation } from '../modules/front_end_components/Confirmation.js';
@@ -25,32 +24,33 @@ function attemptToJoinGame (event) {
form.removeEventListener('submit', attemptToJoinGame);
form.classList.add('submitted');
document.getElementById('join-button')?.setAttribute('value', 'Joining...');
XHRUtility.xhr(
fetch(
'/api/games/' + userCode.toUpperCase().trim() + '/availability',
'GET',
null,
null
)
.then((res) => {
if (res.status === 200) {
const json = JSON.parse(res.content);
{
method: 'GET',
mode: 'cors'
}
).then((res) => {
if (res.status === 200) {
res.json().then(json => {
window.location = window.location.protocol + '//' + window.location.host +
'/join/' + encodeURIComponent(json.accessCode) +
'?playerCount=' + encodeURIComponent(json.playerCount) +
'&timer=' + encodeURIComponent(getTimeString(json.timerParams));
}
}).catch((res) => {
form.addEventListener('submit', attemptToJoinGame);
form.classList.remove('submitted');
document.getElementById('join-button')?.setAttribute('value', 'Join');
if (res.status === 404) {
toast('Game not found', 'error', true);
} else if (res.status === 400) {
toast(res.content, 'error', true);
} else {
toast('An unknown error occurred. Please try again later.', 'error', true);
}
});
});
}
}).catch((res) => {
form.addEventListener('submit', attemptToJoinGame);
form.classList.remove('submitted');
document.getElementById('join-button')?.setAttribute('value', 'Join');
if (res.status === 404) {
toast('Game not found', 'error', true);
} else if (res.status === 400) {
toast(res.content, 'error', true);
} else {
toast('An unknown error occurred. Please try again later.', 'error', true);
}
});
} else {
toast('Invalid code. Codes are 4 numbers or letters.', 'error', true, true);
}

View File

@@ -1,14 +1,13 @@
import { injectNavbar } from '../modules/front_end_components/Navbar.js';
import { toast } from '../modules/front_end_components/Toast.js';
import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { UserUtility } from '../modules/utility/UserUtility.js';
import { globals } from '../config/globals.js';
import { ENVIRONMENTS, PRIMITIVES } from '../config/globals.js';
const join = () => {
injectNavbar();
const splitUrl = window.location.pathname.split('/join/');
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === PRIMITIVES.ACCESS_CODE_LENGTH) {
document.getElementById('game-code').innerText = accessCode;
document.getElementById('game-time').innerText =
decodeURIComponent((new URL(document.location)).searchParams.get('timer'));
@@ -29,10 +28,11 @@ const joinHandler = (e) => {
if (validateName(name)) {
sendJoinRequest(e, name, accessCode)
.then((res) => {
const json = JSON.parse(res.content);
UserUtility.setAnonymousUserId(json.cookie, json.environment);
resetJoinButtonState(e, res, joinHandler);
window.location = '/game/' + accessCode;
res.json().then(json => {
UserUtility.setAnonymousUserId(json.cookie, json.environment);
resetJoinButtonState(e, res, joinHandler);
window.location = '/game/' + accessCode;
});
}).catch((res) => {
handleJoinError(e, res, joinHandler);
});
@@ -46,17 +46,22 @@ function sendJoinRequest (e, name, accessCode) {
document.getElementById('join-submit').classList.add('submitted');
document.getElementById('join-submit').setAttribute('value', '...');
return XHRUtility.xhr(
return fetch(
'/api/games/' + accessCode + '/players',
'PATCH',
null,
JSON.stringify({
playerName: name,
accessCode: accessCode,
sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL),
localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION),
joinAsSpectator: document.getElementById('join-as-spectator').checked
})
{
method: 'PATCH',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
playerName: name,
accessCode: accessCode,
sessionCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.LOCAL),
localCookie: UserUtility.validateAnonUserSignature(ENVIRONMENTS.PRODUCTION),
joinAsSpectator: document.getElementById('join-as-spectator').checked
})
}
);
}

View File

@@ -18,7 +18,7 @@ const template =
<div></div>
<div></div>
</div>
<p>Connecting to game...</p>
<p>Waiting for connection to Room...</p>
</div>
<div id="mobile-menu-background-overlay"></div>
<div id="navbar"></div>

View File

@@ -3,13 +3,13 @@ const router = express.Router();
const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug');
const logger = require('../modules/Logger')(debugMode);
const eventManager = (require('../modules/singletons/EventManager.js')).instance;
const globals = require('../config/globals.js');
const cors = require('cors');
const { CORS_OPTIONS, CONTENT_TYPE_VALIDATOR } = require('../config/globals');
router.use(cors(globals.CORS));
router.use(cors(CORS_OPTIONS));
router.post('/sockets/broadcast', (req, res, next) => {
globals.CONTENT_TYPE_VALIDATOR(req, res, next);
CONTENT_TYPE_VALIDATOR(req, res, next);
});
// TODO: implement client-side display of this message.

View File

@@ -4,8 +4,8 @@ const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())
const logger = require('../modules/Logger')(debugMode);
const GameManager = require('../modules/singletons/GameManager.js');
const rateLimit = require('express-rate-limit').default;
const globals = require('../config/globals.js');
const cors = require('cors');
const { CORS_OPTIONS, CONTENT_TYPE_VALIDATOR, PRIMITIVES, ERROR_MESSAGES, ENVIRONMENTS } = require('../config/globals');
const gameManager = GameManager.instance;
@@ -20,19 +20,19 @@ const gameEndpointLimiter = rateLimit({
legacyHeaders: false
});
router.use(cors(globals.CORS));
router.options('/:code/players', cors(globals.CORS));
router.options('/create', cors(globals.CORS));
router.options('/restart', cors(globals.CORS));
router.use(cors(CORS_OPTIONS));
router.options('/:code/players', cors(CORS_OPTIONS));
router.options('/create', cors(CORS_OPTIONS));
router.options('/restart', cors(CORS_OPTIONS));
router.post('/create', (req, res, next) => {
globals.CONTENT_TYPE_VALIDATOR(req, res, next);
CONTENT_TYPE_VALIDATOR(req, res, next);
});
router.patch('/players', (req, res, next) => {
globals.CONTENT_TYPE_VALIDATOR(req, res, next);
CONTENT_TYPE_VALIDATOR(req, res, next);
});
router.patch('/restart', (req, res, next) => {
globals.CONTENT_TYPE_VALIDATOR(req, res, next);
CONTENT_TYPE_VALIDATOR(req, res, next);
});
router.post('/create', gameEndpointLimiter, function (req, res) {
@@ -43,8 +43,8 @@ router.post('/create', gameEndpointLimiter, function (req, res) {
res.status(201).send(result); // game was created successfully, and access code was returned
}
}).catch((e) => {
if (e === globals.ERROR_MESSAGE.BAD_CREATE_REQUEST) {
res.status(400).send(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST);
if (e === ERROR_MESSAGES.BAD_CREATE_REQUEST) {
res.status(400).send(ERROR_MESSAGES.BAD_CREATE_REQUEST);
}
});
});
@@ -78,7 +78,7 @@ router.patch('/:code/players', async function (req, res) {
} else {
const game = await gameManager.getActiveGame(req.body.accessCode);
if (game) {
const inUseCookie = gameManager.environment === globals.ENVIRONMENT.PRODUCTION ? req.body.localCookie : req.body.sessionCookie;
const inUseCookie = gameManager.environment === ENVIRONMENTS.PRODUCTION ? req.body.localCookie : req.body.sessionCookie;
gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => {
res.status(200).send({ cookie: data, environment: gameManager.environment });
}).catch((data) => {
@@ -103,7 +103,7 @@ router.patch('/:code/restart', async function (req, res) {
} else {
const game = await gameManager.getActiveGame(req.body.accessCode);
if (game) {
gameManager.restartGame(game, gameManager.namespace, req.query.status).then((data) => {
gameManager.restartGame(game, gameManager.namespace).then((data) => {
res.status(200).send();
}).catch((code) => {
res.status(code).send();
@@ -123,11 +123,11 @@ function validateName (name) {
}
function validateCookie (cookie) {
return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === globals.INSTANCE_ID_LENGTH);
return cookie === null || cookie === false || (typeof cookie === 'string' && cookie.length === PRIMITIVES.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 === PRIMITIVES.ACCESS_CODE_LENGTH;
}
function validateSpectatorFlag (spectatorFlag) {

View File

@@ -1,4 +1,4 @@
const globals = {
const PRIMITIVES = {
ACCESS_CODE_CHAR_POOL: 'BCDFGHJLMNPQRSTVWXYZ23456789',
INSTANCE_ID_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
ACCESS_CODE_LENGTH: 4,
@@ -6,143 +6,169 @@ const globals = {
CLOCK_TICK_INTERVAL_MILLIS: 100,
MAX_CUSTOM_ROLE_NAME_LENGTH: 50,
MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000,
ALIGNMENT: {
GOOD: 'good',
EVIL: 'evil'
},
REDIS_CHANNELS: {
ACTIVE_GAME_STREAM: 'active_game_stream'
},
CORS: process.env.NODE_ENV?.trim() === 'development'
? {
origin: '*',
optionsSuccessStatus: 200
}
: {
origin: 'https://play-werewolf.app',
optionsSuccessStatus: 200
},
CONTENT_TYPE_VALIDATOR: (req, res, next) => {
req.accepts();
if (req.is('application/json')) {
next();
} else {
res.status(400).send('Request has invalid content type.');
}
},
STALE_GAME_SECONDS: 86400,
SOCKET_EVENTS: {
IN_GAME_MESSAGE: 'inGameMessage'
},
EVENT_IDS: {
NEW_GAME: 'newGame',
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
END_TIMER: 'endTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
SOURCE_TIMER_EVENT: 'sourceTimerEvent',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
END_GAME: 'endGame',
RESTART_GAME: 'restartGame',
PLAYER_JOINED: 'playerJoined',
UPDATE_SPECTATORS: 'updateSpectators',
ADD_SPECTATOR: 'addSpectator',
SYNC_GAME_STATE: 'syncGameState',
UPDATE_SOCKET: 'updateSocket',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
TIMER_EVENT: 'timerEvent',
KICK_PERSON: 'kickPerson',
UPDATE_GAME_ROLES: 'updateGameRoles',
LEAVE_ROOM: 'leaveRoom'
},
SYNCABLE_EVENTS: function () {
return [
this.EVENT_IDS.NEW_GAME,
this.EVENT_IDS.START_GAME,
this.EVENT_IDS.KILL_PLAYER,
this.EVENT_IDS.REVEAL_PLAYER,
this.EVENT_IDS.TRANSFER_MODERATOR,
this.EVENT_IDS.END_GAME,
this.EVENT_IDS.RESTART_GAME,
this.EVENT_IDS.PLAYER_JOINED,
this.EVENT_IDS.ADD_SPECTATOR,
this.EVENT_IDS.SYNC_GAME_STATE,
this.EVENT_IDS.UPDATE_SOCKET,
this.EVENT_IDS.FETCH_GAME_STATE,
this.EVENT_IDS.ASSIGN_DEDICATED_MOD,
this.EVENT_IDS.RESUME_TIMER,
this.EVENT_IDS.PAUSE_TIMER,
this.EVENT_IDS.END_TIMER,
this.EVENT_IDS.KICK_PERSON,
this.EVENT_IDS.UPDATE_GAME_ROLES,
this.EVENT_IDS.LEAVE_ROOM
];
},
TIMER_EVENTS: function () {
return [
this.EVENT_IDS.RESUME_TIMER,
this.EVENT_IDS.PAUSE_TIMER,
this.EVENT_IDS.END_TIMER,
this.EVENT_IDS.GET_TIME_REMAINING
];
},
MESSAGES: {
ENTER_NAME: 'Client must enter name.'
},
STATUS: {
LOBBY: 'lobby',
IN_PROGRESS: 'in progress',
ENDED: 'ended'
},
USER_SIGNATURE_LENGTH: 25,
INSTANCE_ID_LENGTH: 75,
USER_TYPES: {
MODERATOR: 'moderator',
PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed',
KILLED_BOT: 'killed bot',
SPECTATOR: 'spectator',
BOT: 'bot'
},
ERROR_MESSAGE: {
GAME_IS_FULL: 'This game is full',
BAD_CREATE_REQUEST: 'Game has invalid options.',
NO_UNIQUE_ACCESS_CODE: 'Could not generate a unique access code.'
},
EVENTS: {
PLAYER_JOINED: 'playerJoined',
PLAYER_LEFT: 'playerLeft',
SYNC_GAME_STATE: 'syncGameState',
UPDATE_SPECTATORS: 'newSpectator',
BROADCAST: 'broadcast'
},
ENVIRONMENT: {
LOCAL: 'local',
PRODUCTION: 'production'
},
LOG_LEVEL: {
INFO: 'info',
DEBUG: 'debug',
ERROR: 'error',
WARN: 'warn',
TRACE: 'trace'
},
GAME_PROCESS_COMMANDS: {
END_TIMER: 'endTimer',
START_GAME: 'startGame',
START_TIMER: 'startTimer',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining'
},
MOCK_AUTH: 'mock_auth',
MAX_SPECTATORS: 25
MAX_SPECTATORS: 25,
MOCK_AUTH: 'mock_auth'
};
module.exports = globals;
const LOG_LEVEL = {
INFO: 'info',
DEBUG: 'debug',
ERROR: 'error',
WARN: 'warn',
TRACE: 'trace'
};
const ALIGNMENT = {
GOOD: 'good',
EVIL: 'evil'
};
const REDIS_CHANNELS = {
ACTIVE_GAME_STREAM: 'active_game_stream'
};
const CORS_OPTIONS = process.env.NODE_ENV?.trim() === 'development'
? {
origin: '*',
optionsSuccessStatus: 200
}
: {
origin: 'https://play-werewolf.app',
optionsSuccessStatus: 200
};
const CONTENT_TYPE_VALIDATOR = (req, res, next) => {
req.accepts();
if (req.is('application/json')) {
next();
} else {
res.status(400).send('Request has invalid content type.');
}
};
const SOCKET_EVENTS = {
IN_GAME_MESSAGE: 'inGameMessage'
};
const EVENT_IDS = {
NEW_GAME: 'newGame',
FETCH_GAME_STATE: 'fetchGameState',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
END_TIMER: 'endTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
SOURCE_TIMER_EVENT: 'sourceTimerEvent',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
END_GAME: 'endGame',
RESTART_GAME: 'restartGame',
PLAYER_JOINED: 'playerJoined',
UPDATE_SPECTATORS: 'updateSpectators',
ADD_SPECTATOR: 'addSpectator',
SYNC_GAME_STATE: 'syncGameState',
UPDATE_SOCKET: 'updateSocket',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
TIMER_EVENT: 'timerEvent',
KICK_PERSON: 'kickPerson',
UPDATE_GAME_ROLES: 'updateGameRoles',
LEAVE_ROOM: 'leaveRoom',
BROADCAST: 'broadcast'
};
const SYNCABLE_EVENTS = function () {
return [
EVENT_IDS.NEW_GAME,
EVENT_IDS.START_GAME,
EVENT_IDS.KILL_PLAYER,
EVENT_IDS.REVEAL_PLAYER,
EVENT_IDS.TRANSFER_MODERATOR,
EVENT_IDS.END_GAME,
EVENT_IDS.RESTART_GAME,
EVENT_IDS.PLAYER_JOINED,
EVENT_IDS.ADD_SPECTATOR,
EVENT_IDS.SYNC_GAME_STATE,
EVENT_IDS.UPDATE_SOCKET,
EVENT_IDS.FETCH_GAME_STATE,
EVENT_IDS.ASSIGN_DEDICATED_MOD,
EVENT_IDS.RESUME_TIMER,
EVENT_IDS.PAUSE_TIMER,
EVENT_IDS.END_TIMER,
EVENT_IDS.KICK_PERSON,
EVENT_IDS.UPDATE_GAME_ROLES,
EVENT_IDS.LEAVE_ROOM
];
};
const TIMER_EVENTS = function () {
return [
EVENT_IDS.RESUME_TIMER,
EVENT_IDS.PAUSE_TIMER,
EVENT_IDS.END_TIMER,
EVENT_IDS.GET_TIME_REMAINING
];
};
const MESSAGES = {
ENTER_NAME: 'Client must enter name.'
};
const STATUS = {
LOBBY: 'lobby',
IN_PROGRESS: 'in progress',
ENDED: 'ended'
};
const USER_TYPES = {
MODERATOR: 'moderator',
PLAYER: 'player',
TEMPORARY_MODERATOR: 'temp mod',
KILLED_PLAYER: 'killed',
KILLED_BOT: 'killed bot',
SPECTATOR: 'spectator',
BOT: 'bot'
};
const ERROR_MESSAGES = {
GAME_IS_FULL: 'This game is full',
BAD_CREATE_REQUEST: 'Game has invalid options.',
NO_UNIQUE_ACCESS_CODE: 'Could not generate a unique access code.'
};
const ENVIRONMENTS = {
LOCAL: 'local',
PRODUCTION: 'production'
};
const GAME_PROCESS_COMMANDS = {
END_TIMER: 'endTimer',
START_GAME: 'startGame',
START_TIMER: 'startTimer',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining'
};
module.exports = {
PRIMITIVES,
LOG_LEVEL,
ALIGNMENT,
REDIS_CHANNELS,
CORS_OPTIONS,
CONTENT_TYPE_VALIDATOR,
SOCKET_EVENTS,
EVENT_IDS,
SYNCABLE_EVENTS,
TIMER_EVENTS,
MESSAGES,
STATUS,
USER_TYPES,
ERROR_MESSAGES,
ENVIRONMENTS,
GAME_PROCESS_COMMANDS
};

View File

@@ -1,4 +1,4 @@
const globals = require('../config/globals');
const { ERROR_MESSAGES, PRIMITIVES, ALIGNMENT } = require('../config/globals');
class GameCreationRequest {
constructor (
@@ -24,7 +24,7 @@ class GameCreationRequest {
|| expectedKeys.some((key) => !Object.keys(gameParams).includes(key))
|| !valid(gameParams)
) {
return Promise.reject(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST);
return Promise.reject(ERROR_MESSAGES.BAD_CREATE_REQUEST);
} else {
return Promise.resolve();
}
@@ -37,12 +37,12 @@ class GameCreationRequest {
&& typeof entry === 'object'
&& typeof entry.role === 'string'
&& entry.role.length > 0
&& entry.role.length <= globals.MAX_CUSTOM_ROLE_NAME_LENGTH
&& entry.role.length <= PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH
&& typeof entry.team === 'string'
&& (entry.team === globals.ALIGNMENT.GOOD || entry.team === globals.ALIGNMENT.EVIL)
&& (entry.team === ALIGNMENT.GOOD || entry.team === ALIGNMENT.EVIL)
&& typeof entry.description === 'string'
&& entry.description.length > 0
&& entry.description.length <= globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
&& entry.description.length <= PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH
&& (!entry.custom || typeof entry.custom === 'boolean')
&& typeof entry.quantity === 'number'
&& entry.quantity >= 0

View File

@@ -1,5 +1,5 @@
// noinspection DuplicatedCode
const globals = require('../config/globals');
const { USER_TYPES } = require('../config/globals');
class Person {
constructor (id, cookie, name, userType, gameRole = null, gameRoleDescription = null, alignment = null, assigned = false) {
@@ -12,7 +12,7 @@ class Person {
this.gameRoleDescription = gameRoleDescription;
this.alignment = alignment;
this.assigned = assigned;
this.out = userType === globals.USER_TYPES.MODERATOR || userType === globals.USER_TYPES.SPECTATOR;
this.out = userType === USER_TYPES.MODERATOR || userType === USER_TYPES.SPECTATOR;
this.killed = false;
this.revealed = false;
this.hasEnteredName = false;

View File

@@ -1,7 +1,6 @@
const globals = require('../config/globals');
const GameStateCurator = require('./GameStateCurator');
const GameCreationRequest = require('../model/GameCreationRequest');
const EVENT_IDS = globals.EVENT_IDS;
const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS } = require('../config/globals');
const Events = [
{
@@ -12,7 +11,7 @@ const Events = [
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode).emit(
globals.EVENTS.PLAYER_JOINED,
EVENT_IDS.PLAYER_JOINED,
GameStateCurator.mapPerson(socketArgs),
game.isStartable
);
@@ -87,7 +86,7 @@ const Events = [
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode).emit(
globals.EVENT_IDS.ADD_SPECTATOR,
EVENT_IDS.ADD_SPECTATOR,
GameStateCurator.mapPerson(socketArgs)
);
}
@@ -117,7 +116,7 @@ const Events = [
communicate: async (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);
vars.gameManager.namespace.to(matchingPerson.socketId).emit(EVENT_IDS.SYNC_GAME_STATE);
}
}
},
@@ -125,7 +124,7 @@ const Events = [
id: EVENT_IDS.START_GAME,
stateChange: async (game, socketArgs, vars) => {
if (game.isStartable) {
game.status = globals.STATUS.IN_PROGRESS;
game.status = STATUS.IN_PROGRESS;
vars.gameManager.deal(game);
if (game.hasTimer) {
game.timerParams.paused = true;
@@ -137,7 +136,7 @@ const Events = [
if (vars.ackFn) {
vars.ackFn();
}
vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME);
vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.START_GAME);
}
},
{
@@ -145,9 +144,9 @@ const Events = [
stateChange: async (game, socketArgs, vars) => {
const person = game.people.find((person) => person.id === socketArgs.personId);
if (person && !person.out) {
person.userType = person.userType === globals.USER_TYPES.BOT
? globals.USER_TYPES.KILLED_BOT
: globals.USER_TYPES.KILLED_PLAYER;
person.userType = person.userType === USER_TYPES.BOT
? USER_TYPES.KILLED_BOT
: USER_TYPES.KILLED_PLAYER;
person.out = true;
person.killed = true;
}
@@ -155,7 +154,7 @@ const Events = [
communicate: async (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);
vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, person);
}
}
},
@@ -171,7 +170,7 @@ const Events = [
const person = game.people.find((person) => person.id === socketArgs.personId);
if (person) {
vars.gameManager.namespace.in(game.accessCode).emit(
globals.EVENT_IDS.REVEAL_PLAYER,
EVENT_IDS.REVEAL_PLAYER,
{
id: person.id,
gameRole: person.gameRole,
@@ -184,7 +183,7 @@ const Events = [
{
id: EVENT_IDS.END_GAME,
stateChange: async (game, socketArgs, vars) => {
game.status = globals.STATUS.ENDED;
game.status = STATUS.ENDED;
if (game.hasTimer && vars.timerManager.timerThreads[game.accessCode]) {
vars.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode);
vars.timerManager.timerThreads[game.accessCode].kill();
@@ -195,7 +194,7 @@ const Events = [
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode)
.emit(globals.EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people));
.emit(EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people));
if (vars.ackFn) {
vars.ackFn();
}
@@ -208,14 +207,14 @@ const Events = [
const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId);
if (currentModerator) {
if (currentModerator.gameRole) {
currentModerator.userType = globals.USER_TYPES.KILLED_PLAYER;
currentModerator.userType = USER_TYPES.KILLED_PLAYER;
} else {
currentModerator.userType = globals.USER_TYPES.SPECTATOR;
currentModerator.userType = USER_TYPES.SPECTATOR;
}
game.previousModeratorId = currentModerator.id;
}
if (toTransferTo) {
toTransferTo.userType = globals.USER_TYPES.MODERATOR;
toTransferTo.userType = USER_TYPES.MODERATOR;
game.currentModeratorId = toTransferTo.id;
}
},
@@ -223,7 +222,7 @@ const Events = [
if (vars.ackFn) {
vars.ackFn();
}
vars.gameManager.namespace.to(game.accessCode).emit(globals.EVENT_IDS.SYNC_GAME_STATE);
vars.gameManager.namespace.to(game.accessCode).emit(EVENT_IDS.SYNC_GAME_STATE);
}
},
{
@@ -233,10 +232,10 @@ const Events = [
const toTransferTo = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId);
if (currentModerator && toTransferTo) {
if (currentModerator.id !== toTransferTo.id) {
currentModerator.userType = globals.USER_TYPES.PLAYER;
currentModerator.userType = USER_TYPES.PLAYER;
}
toTransferTo.userType = globals.USER_TYPES.MODERATOR;
toTransferTo.userType = USER_TYPES.MODERATOR;
toTransferTo.out = true;
toTransferTo.killed = true;
game.previousModeratorId = currentModerator.id;
@@ -247,14 +246,14 @@ const Events = [
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, moderator);
vars.gameManager.namespace.to(moderator.socketId).emit(EVENT_IDS.SYNC_GAME_STATE);
moderatorSocket.to(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, moderator);
} else {
vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.KILL_PLAYER, moderator);
vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.KILL_PLAYER, moderator);
}
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);
vars.gameManager.namespace.to(previousModerator.socketId).emit(EVENT_IDS.SYNC_GAME_STATE);
}
}
},
@@ -274,7 +273,7 @@ const Events = [
if (vars.ackFn) {
vars.ackFn();
}
vars.gameManager.namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME);
vars.gameManager.namespace.in(game.accessCode).emit(EVENT_IDS.RESTART_GAME);
}
},
{
@@ -294,7 +293,7 @@ const Events = [
const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId);
if (socket) {
vars.gameManager.namespace.to(socket.id).emit(
globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
game.timerParams.timeRemaining,
game.timerParams.paused
);
@@ -302,10 +301,10 @@ const Events = [
}
} else { // we need to consult another container for the timer data
await vars.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.SOURCE_TIMER_EVENT,
EVENT_IDS.SOURCE_TIMER_EVENT,
vars.instanceId,
JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype })
)
@@ -330,7 +329,7 @@ const Events = [
});
} else {
await vars.eventManager.publisher.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
socketArgs.timerEventSubtype,
@@ -353,7 +352,7 @@ const Events = [
game.timerParams.timeRemaining = 0;
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_TIMER);
vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.END_TIMER);
}
},
{
@@ -363,7 +362,7 @@ const Events = [
game.timerParams.timeRemaining = socketArgs.timeRemaining;
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, socketArgs.timeRemaining);
vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.PAUSE_TIMER, socketArgs.timeRemaining);
}
},
{
@@ -373,7 +372,7 @@ const Events = [
game.timerParams.timeRemaining = socketArgs.timeRemaining;
},
communicate: async (game, socketArgs, vars) => {
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, socketArgs.timeRemaining);
vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.RESUME_TIMER, socketArgs.timeRemaining);
}
},
{
@@ -382,7 +381,7 @@ const Events = [
communicate: async (game, socketArgs, vars) => {
const socket = vars.gameManager.namespace.sockets.get(socketArgs.socketId);
if (socket) {
vars.gameManager.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, game.timerParams.paused);
vars.gameManager.namespace.to(socket.id).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, game.timerParams.paused);
}
}
}

View File

@@ -1,4 +1,4 @@
const globals = require('../config/globals.js');
const { GAME_PROCESS_COMMANDS, PRIMITIVES } = require('../config/globals');
const ServerTimer = require('./ServerTimer.js');
let timer;
@@ -7,43 +7,43 @@ let timer;
process.on('message', (msg) => {
const logger = require('./Logger')(msg.logLevel);
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.START_TIMER:
case GAME_PROCESS_COMMANDS.START_TIMER:
logger.trace('CHILD PROCESS ' + msg.accessCode + ': START TIMER');
timer = new ServerTimer(
msg.hours,
msg.minutes,
globals.CLOCK_TICK_INTERVAL_MILLIS,
PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS,
logger
);
timer.runTimer().then(() => {
logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER });
process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER });
process.exit(0);
});
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
case GAME_PROCESS_COMMANDS.PAUSE_TIMER:
timer.stopTimer();
logger.trace('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER');
process.send({ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis });
process.send({ command: GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis });
break;
case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
case GAME_PROCESS_COMMANDS.RESUME_TIMER:
timer.resumeTimer().then(() => {
logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER });
process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER });
process.exit(0);
});
logger.trace('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');
process.send({ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis });
process.send({ command: GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis });
break;
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
logger.trace('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING');
process.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
timeRemaining: timer.currentTimeInMillis,
socketId: msg.socketId
});

View File

@@ -1,4 +1,4 @@
const globals = require('../config/globals');
const { USER_TYPES, STATUS } = require('../config/globals');
/* The purpose of this component is to only return the game state information that is necessary. For example, we only
want to return player role information to moderators. This avoids any possibility of a player having access to
@@ -12,7 +12,7 @@ const GameStateCurator = {
mapPeopleForModerator: (people) => {
return people
.filter((person) => {
return person.assigned === true || (person.userType === globals.USER_TYPES.SPECTATOR || person.userType === globals.USER_TYPES.MODERATOR);
return person.assigned === true || (person.userType === USER_TYPES.SPECTATOR || person.userType === USER_TYPES.MODERATOR);
})
.map((person) => ({
name: person.name,
@@ -45,7 +45,7 @@ const GameStateCurator = {
};
function getGameStateBasedOnPermissions (game, person) {
const client = game.status === globals.STATUS.LOBBY // people won't be able to know their role until past the lobby stage.
const client = game.status === STATUS.LOBBY // people won't be able to know their role until past the lobby stage.
? { name: person.name, hasEnteredName: person.hasEnteredName, id: person.id, cookie: person.cookie, userType: person.userType }
: {
name: person.name,
@@ -61,7 +61,7 @@ function getGameStateBasedOnPermissions (game, person) {
killed: person.killed
};
switch (person.userType) {
case globals.USER_TYPES.MODERATOR:
case USER_TYPES.MODERATOR:
return {
accessCode: game.accessCode,
status: game.status,
@@ -73,10 +73,10 @@ function getGameStateBasedOnPermissions (game, person) {
timerParams: game.timerParams,
isStartable: game.isStartable
};
case globals.USER_TYPES.TEMPORARY_MODERATOR:
case globals.USER_TYPES.SPECTATOR:
case globals.USER_TYPES.PLAYER:
case globals.USER_TYPES.KILLED_PLAYER:
case USER_TYPES.TEMPORARY_MODERATOR:
case USER_TYPES.SPECTATOR:
case USER_TYPES.PLAYER:
case USER_TYPES.KILLED_PLAYER:
return {
accessCode: game.accessCode,
status: game.status,
@@ -86,7 +86,7 @@ function getGameStateBasedOnPermissions (game, person) {
gameSize: game.gameSize,
people: game.people
.filter((person) => {
return person.assigned === true || person.userType === globals.USER_TYPES.SPECTATOR;
return person.assigned === true || person.userType === USER_TYPES.SPECTATOR;
})
.map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)),
timerParams: game.timerParams,

View File

@@ -1,6 +1,6 @@
const globals = require('../config/globals');
const { LOG_LEVEL } = require('../config/globals');
module.exports = function (logLevel = globals.LOG_LEVEL.INFO) {
module.exports = function (logLevel = LOG_LEVEL.INFO) {
return {
logLevel: logLevel,
info (message = '') {
@@ -10,7 +10,7 @@ module.exports = function (logLevel = globals.LOG_LEVEL.INFO) {
error (message = '') {
if (
logLevel === globals.LOG_LEVEL.INFO
logLevel === LOG_LEVEL.INFO
) { return; }
const now = new Date();
console.error('ERROR ', now.toGMTString(), ': ', message);
@@ -18,25 +18,25 @@ module.exports = function (logLevel = globals.LOG_LEVEL.INFO) {
warn (message = '') {
if (
logLevel === globals.LOG_LEVEL.INFO
|| logLevel === globals.LOG_LEVEL.ERROR
logLevel === LOG_LEVEL.INFO
|| logLevel === LOG_LEVEL.ERROR
) return;
const now = new Date();
console.error('WARN ', now.toGMTString(), ': ', message);
},
debug (message = '') {
if (logLevel === globals.LOG_LEVEL.INFO || logLevel === globals.LOG_LEVEL.ERROR || logLevel === globals.LOG_LEVEL.WARN) return;
if (logLevel === LOG_LEVEL.INFO || logLevel === LOG_LEVEL.ERROR || logLevel === LOG_LEVEL.WARN) return;
const now = new Date();
console.debug('DEBUG ', now.toGMTString(), ': ', message);
},
trace (message = '') {
if (
logLevel === globals.LOG_LEVEL.INFO
|| logLevel === globals.LOG_LEVEL.WARN
|| logLevel === globals.LOG_LEVEL.DEBUG
|| logLevel === globals.LOG_LEVEL.ERROR
logLevel === LOG_LEVEL.INFO
|| logLevel === LOG_LEVEL.WARN
|| logLevel === LOG_LEVEL.DEBUG
|| logLevel === LOG_LEVEL.ERROR
) return;
const now = new Date();

View File

@@ -1,4 +1,4 @@
const LOG_LEVEL = require('../config/globals').LOG_LEVEL;
const { LOG_LEVEL, ENVIRONMENTS } = require('../config/globals');
const http = require('http');
const https = require('https');
const path = require('path');
@@ -7,7 +7,6 @@ const crypto = require('crypto');
const EventManager = require('./singletons/EventManager.js');
const GameManager = require('./singletons/GameManager.js');
const TimerManager = require('./singletons/TimerManager.js');
const { ENVIRONMENT } = require('../config/globals.js');
const rateLimit = require('express-rate-limit').default;
const ServerBootstrapper = {
@@ -17,8 +16,8 @@ const ServerBootstrapper = {
timerManager: new TimerManager(logger, instanceId),
eventManager: new EventManager(logger, instanceId),
gameManager: process.env.NODE_ENV.trim() === 'development'
? new GameManager(logger, ENVIRONMENT.LOCAL, instanceId)
: new GameManager(logger, ENVIRONMENT.PRODUCTION, instanceId)
? new GameManager(logger, ENVIRONMENTS.LOCAL, instanceId)
: new GameManager(logger, ENVIRONMENTS.PRODUCTION, instanceId)
};
},
@@ -114,7 +113,7 @@ const ServerBootstrapper = {
});
// API endpoints
app.use('/api/games', standardRateLimit, require('../api/GamesAPI'));
app.use('/api/games', standardRateLimit, require('../api/RoomsAPI'));
app.use('/api/admin', (req, res, next) => {
if (isAuthorized(req)) {
next();

View File

@@ -1,7 +1,7 @@
const globals = require('../../config/globals');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const redis = require('redis');
const Events = require('../Events');
const { EVENT_IDS, REDIS_CHANNELS, PRIMITIVES, SOCKET_EVENTS, TIMER_EVENTS, SYNCABLE_EVENTS } = require('../../config/globals');
class EventManager {
constructor (logger, instanceId) {
@@ -20,7 +20,7 @@ class EventManager {
}
broadcast = (message) => {
this.io?.emit(globals.EVENTS.BROADCAST, message);
this.io?.emit(EVENT_IDS.BROADCAST, message);
};
createRedisPublisher = async () => {
@@ -53,7 +53,7 @@ class EventManager {
throw new Error('UNABLE TO CONNECT TO REDIS because: ' + e);
}
await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => {
await this.subscriber.subscribe(REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => {
this.logger.debug('MESSAGE: ' + message);
let messageComponents, args;
try {
@@ -64,7 +64,7 @@ class EventManager {
}
args = JSON.parse(
message.slice(
message.indexOf(messageComponents[messageComponents.length - 1]) + (globals.INSTANCE_ID_LENGTH + 1)
message.indexOf(messageComponents[messageComponents.length - 1]) + (PRIMITIVES.INSTANCE_ID_LENGTH + 1)
)
);
} catch (e) {
@@ -133,12 +133,12 @@ class EventManager {
};
registerSocketHandler = (namespace, socket, gameManager) => {
socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => {
socket.on(SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => {
const game = await gameManager.getActiveGame(accessCode);
if (game) {
if (globals.TIMER_EVENTS().includes(eventId)) {
if (TIMER_EVENTS().includes(eventId)) {
await this.handleEventById(
globals.EVENT_IDS.TIMER_EVENT,
EVENT_IDS.TIMER_EVENT,
null,
game,
socket.id,
@@ -160,10 +160,10 @@ class EventManager {
handleAndSyncSocketEvent = async (eventId, game, socket, socketArgs, ackFn) => {
await this.handleEventById(eventId, null, 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)) {
if (SYNCABLE_EVENTS().includes(eventId)) {
await this.gameManager.refreshGame(game);
await this.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.createMessageToPublish(game.accessCode, eventId, this.instanceId, JSON.stringify(socketArgs))
);
}
@@ -194,7 +194,7 @@ class EventManager {
timerEventSubtype: timerEventSubtype
};
if (event) {
if (!syncOnly || eventId === globals.EVENT_IDS.RESTART_GAME) {
if (!syncOnly || eventId === EVENT_IDS.RESTART_GAME) {
await event.stateChange(game, socketArgs, additionalVars);
}
await event.communicate(game, socketArgs, additionalVars);

View File

@@ -1,4 +1,12 @@
const globals = require('../../config/globals');
const {
STATUS,
PRIMITIVES,
ERROR_MESSAGES,
GAME_PROCESS_COMMANDS,
USER_TYPES,
EVENT_IDS,
REDIS_CHANNELS
} = require('../../config/globals');
const Game = require('../../model/Game');
const Person = require('../../model/Person');
const GameStateCurator = require('../GameStateCurator');
@@ -62,9 +70,9 @@ class GameManager {
gameParams.hasDedicatedModerator,
gameParams.isTestGame
);
const newAccessCode = await this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL);
const newAccessCode = await this.generateAccessCode(PRIMITIVES.ACCESS_CODE_CHAR_POOL);
if (newAccessCode === null) {
return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE);
return Promise.reject(ERROR_MESSAGES.NO_UNIQUE_ACCESS_CODE);
}
const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator);
moderator.assigned = true;
@@ -75,7 +83,7 @@ class GameManager {
}
const newGame = new Game(
newAccessCode,
globals.STATUS.LOBBY,
STATUS.LOBBY,
null,
req.deck,
req.hasTimer,
@@ -88,7 +96,7 @@ class GameManager {
);
newGame.people = initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame, newGame.gameSize);
await this.eventManager.publisher.set(newAccessCode, JSON.stringify(newGame), {
EX: globals.STALE_GAME_SECONDS
EX: PRIMITIVES.STALE_GAME_SECONDS
});
return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment });
}).catch((message) => {
@@ -103,7 +111,7 @@ class GameManager {
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER,
command: GAME_PROCESS_COMMANDS.PAUSE_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
@@ -115,7 +123,7 @@ class GameManager {
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER,
command: GAME_PROCESS_COMMANDS.RESUME_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
@@ -127,14 +135,14 @@ class GameManager {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && (!thread.killed && thread.exitCode === null)) {
thread.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
accessCode: game.accessCode,
socketId: socketId,
logLevel: this.logger.logLevel
});
} else if (thread) {
if (game.timerParams && game.timerParams.timeRemaining === 0) {
this.namespace.to(socketId).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
this.namespace.to(socketId).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
}
}
}
@@ -154,9 +162,9 @@ class GameManager {
let codeDigits, accessCode;
let attempts = 0;
while (!accessCode || ((await this.eventManager.publisher.keys('*')).includes(accessCode)
&& attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) {
&& attempts < PRIMITIVES.ACCESS_CODE_GENERATION_ATTEMPTS)) {
codeDigits = [];
let iterations = globals.ACCESS_CODE_LENGTH;
let iterations = PRIMITIVES.ACCESS_CODE_LENGTH;
while (iterations > 0) {
iterations --;
codeDigits.push(charPool[getRandomInt(charCount)]);
@@ -178,7 +186,7 @@ class GameManager {
return Promise.reject({ status: 400, reason: 'This name is taken.' });
}
if (joinAsSpectator
&& game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS
&& game.people.filter(person => person.userType === USER_TYPES.SPECTATOR).length === PRIMITIVES.MAX_SPECTATORS
) {
return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' });
} else if (joinAsSpectator || this.isGameStartable(game)) {
@@ -196,7 +204,7 @@ class GameManager {
createRandomId(),
createRandomId(),
name,
globals.USER_TYPES.PLAYER,
USER_TYPES.PLAYER,
null,
null,
null,
@@ -208,15 +216,15 @@ class GameManager {
game.isStartable = this.isGameStartable(game);
await this.refreshGame(game);
this.namespace.in(game.accessCode).emit(
globals.EVENTS.PLAYER_JOINED,
EVENT_IDS.PLAYER_JOINED,
GameStateCurator.mapPerson(moderator || newPlayer),
game.isStartable
);
await this.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.PLAYER_JOINED,
EVENT_IDS.PLAYER_JOINED,
this.instanceId,
JSON.stringify(moderator || newPlayer)
)
@@ -237,7 +245,7 @@ class GameManager {
return cards;
}
restartGame = async (game, namespace, status = globals.STATUS.IN_PROGRESS) => {
restartGame = async (game, namespace) => {
// kill any outstanding timer threads
const subProcess = this.timerManager.timerThreads[game.accessCode];
if (subProcess) {
@@ -250,16 +258,16 @@ class GameManager {
}
for (let i = 0; i < game.people.length; i ++) {
if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) {
game.people[i].userType = globals.USER_TYPES.PLAYER;
if (game.people[i].userType === USER_TYPES.KILLED_PLAYER) {
game.people[i].userType = USER_TYPES.PLAYER;
game.people[i].out = false;
}
if (game.people[i].userType === globals.USER_TYPES.KILLED_BOT) {
game.people[i].userType = globals.USER_TYPES.BOT;
if (game.people[i].userType === USER_TYPES.KILLED_BOT) {
game.people[i].userType = USER_TYPES.BOT;
game.people[i].out = false;
}
if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) {
game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === USER_TYPES.MODERATOR) {
game.people[i].userType = USER_TYPES.TEMPORARY_MODERATOR;
game.people[i].out = false;
}
game.people[i].revealed = false;
@@ -270,19 +278,19 @@ class GameManager {
game.people[i].customRole = null;
}
game.status = globals.STATUS.LOBBY;
game.status = STATUS.LOBBY;
await this.refreshGame(game);
await this.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.RESTART_GAME,
EVENT_IDS.RESTART_GAME,
this.instanceId,
'{}'
)
);
namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME);
namespace.in(game.accessCode).emit(EVENT_IDS.RESTART_GAME);
};
/*
@@ -305,9 +313,9 @@ class GameManager {
deal = (game) => {
const cards = this.prepareDeck(game.deck);
let i = 0;
for (const person of game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === globals.USER_TYPES.BOT)
for (const person of game.people.filter(person => person.userType === USER_TYPES.PLAYER
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === USER_TYPES.BOT)
) {
person.gameRole = cards[i].role;
person.customRole = cards[i].custom;
@@ -318,9 +326,9 @@ class GameManager {
}
isGameStartable = (game) => {
return game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === globals.USER_TYPES.BOT).length === game.gameSize;
return game.people.filter(person => person.userType === USER_TYPES.PLAYER
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === USER_TYPES.BOT).length === game.gameSize;
}
findPersonByField = (game, fieldName, value) => {
@@ -334,8 +342,8 @@ function getRandomInt (max) {
function initializeModerator (name, hasDedicatedModerator) {
const userType = hasDedicatedModerator
? globals.USER_TYPES.MODERATOR
: globals.USER_TYPES.TEMPORARY_MODERATOR;
? USER_TYPES.MODERATOR
: USER_TYPES.TEMPORARY_MODERATOR;
return new Person(createRandomId(), createRandomId(), name, userType);
}
@@ -343,7 +351,7 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g
const people = [];
if (isTestGame) {
let j = 0;
const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
const number = moderator.userType === USER_TYPES.TEMPORARY_MODERATOR
? gameSize - 1
: gameSize;
while (j < number) {
@@ -351,7 +359,7 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g
createRandomId(),
createRandomId(),
UsernameGenerator.generate(),
globals.USER_TYPES.BOT,
USER_TYPES.BOT,
null,
null,
null,
@@ -369,8 +377,8 @@ function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, g
function createRandomId () {
let id = '';
for (let i = 0; i < globals.INSTANCE_ID_LENGTH; i ++) {
id += globals.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * globals.INSTANCE_ID_CHAR_POOL.length)];
for (let i = 0; i < PRIMITIVES.INSTANCE_ID_LENGTH; i ++) {
id += PRIMITIVES.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.INSTANCE_ID_CHAR_POOL.length)];
}
return id;
}
@@ -385,21 +393,21 @@ async function addSpectator (game, name, logger, namespace, eventManager, instan
createRandomId(),
createRandomId(),
name,
globals.USER_TYPES.SPECTATOR
USER_TYPES.SPECTATOR
);
spectator.assigned = true;
logger.trace('new spectator: ' + spectator.name);
game.people.push(spectator);
await refreshGame(game);
namespace.in(game.accessCode).emit(
globals.EVENT_IDS.ADD_SPECTATOR,
EVENT_IDS.ADD_SPECTATOR,
GameStateCurator.mapPerson(spectator)
);
await eventManager.publisher.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.ADD_SPECTATOR,
EVENT_IDS.ADD_SPECTATOR,
instanceId,
JSON.stringify(GameStateCurator.mapPerson(spectator))
)

View File

@@ -1,6 +1,6 @@
const { fork } = require('child_process');
const path = require('path');
const globals = require('../../config/globals');
const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS } = require('../../config/globals');
class TimerManager {
constructor (logger, instanceId) {
@@ -25,7 +25,7 @@ class TimerManager {
await eventManager.handleEventById(msg.command, null, game, msg.socketId, game.accessCode, msg, null, false);
await gameManager.refreshGame(game);
await eventManager.publisher.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
eventManager.createMessageToPublish(game.accessCode, msg.command, this.instanceId, JSON.stringify(msg))
);
});
@@ -34,7 +34,7 @@ class TimerManager {
this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode);
});
gameProcess.send({
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
command: GAME_PROCESS_COMMANDS.START_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel,
hours: game.timerParams.hours,

View File

@@ -1,52 +1,51 @@
import { gameHandler } from '../../client/src/modules/page_handlers/gameHandler.js';
import { mockGames } from '../support/MockGames.js';
import gameTemplate from '../../client/src/view_templates/GameTemplate.js';
import { globals } from '../../client/src/config/globals.js';
import {
EVENT_IDS,
SOCKET_EVENTS,
USER_TYPE_ICONS,
USER_TYPES
} from '../../client/src/config/globals.js';
describe('game page', () => {
const XHRUtility = {
xhr (url, method = 'GET', headers, body = null) {
switch (url) {
case '/api/games/environment':
return new Promise((resolve, reject) => {
resolve({ content: 'production' });
});
}
const mockSocket = {
eventHandlers: {},
on: function (message, handler) {
this.eventHandlers[message] = handler;
},
once: function (message, handler) {
this.eventHandlers[message] = handler;
},
timeout: (duration) => {
return mockSocket;
},
removeAllListeners: function (...names) {
},
hasListeners: function (listener) {
return false;
}
};
const window = { location: { href: 'host/game/ABCD' }, fetch: () => {} };
beforeEach(async () => {
document.body.innerHTML = '';
const response = new Response('production', { status: 200, statusText: 'OK' });
spyOn(window, 'fetch').and.resolveTo(response);
});
describe('lobby game - moderator view', () => {
let mockSocket;
beforeEach(async function () {
document.body.innerHTML = '';
mockSocket = {
eventHandlers: {},
on: function (message, handler) {
this.eventHandlers[message] = handler;
},
once: function (message, handler) {
this.eventHandlers[message] = handler;
},
timeout: (duration) => {
return mockSocket;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.gameInLobbyAsModerator)); // copy the game object to prevent leaking of state between specs
}
},
removeAllListeners: function (...names) {
},
hasListeners: function (listener) {
return false;
beforeEach(async () => {
mockSocket.emit = function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.gameInLobbyAsModerator)); // copy the game object to prevent leaking of state between specs
}
};
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
spyOn(mockSocket, 'emit').and.callThrough();
await gameHandler(mockSocket, window, gameTemplate);
mockSocket.eventHandlers.connect();
spyOn(mockSocket, 'emit');
});
it('should display the connected client', () => {
@@ -59,10 +58,10 @@ describe('game page', () => {
});
it('should display a new player when they join', () => {
mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({
mockSocket.eventHandlers[EVENT_IDS.PLAYER_JOINED]({
name: 'Jane',
id: '123',
userType: globals.USER_TYPES.PLAYER,
userType: USER_TYPES.PLAYER,
out: false,
revealed: false
}, false);
@@ -83,8 +82,8 @@ describe('game page', () => {
document.getElementById('save-role-changes-button').click();
expect(mockSocket.emit).toHaveBeenCalledWith(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.UPDATE_GAME_ROLES,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.UPDATE_GAME_ROLES,
mockGames.gameInLobbyAsModerator.accessCode,
jasmine.any(Object),
jasmine.any(Function)
@@ -97,42 +96,21 @@ describe('game page', () => {
});
describe('lobby game - player view', () => {
let mockSocket;
beforeEach(async function () {
document.body.innerHTML = '';
mockSocket = {
eventHandlers: {},
on: function (message, handler) {
this.eventHandlers[message] = handler;
},
once: function (message, handler) {
this.eventHandlers[message] = handler;
},
timeout: (duration) => {
return mockSocket;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.gameInLobbyAsPlayer)); // copy the game object to prevent leaking of state between specs
}
},
removeAllListeners: function (...names) {
},
hasListeners: function (listener) {
return false;
beforeEach(async () => {
mockSocket.emit = function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.gameInLobbyAsPlayer)); // copy the game object to prevent leaking of state between specs
}
};
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
spyOn(mockSocket, 'emit').and.callThrough();
await gameHandler(mockSocket, window, gameTemplate);
mockSocket.eventHandlers.connect();
spyOn(mockSocket, 'emit');
});
it('should display the connected client', () => {
expect(document.getElementById('client-name').innerText).toEqual('Lys');
expect(document.getElementById('client-user-type').innerText).toEqual('player' + globals.USER_TYPE_ICONS.player);
expect(document.getElementById('client-user-type').innerText).toEqual('player' + USER_TYPE_ICONS.player);
});
it('should display the QR Code', () => {
@@ -144,18 +122,18 @@ describe('game page', () => {
document.getElementById('leave-game-button').click();
document.getElementById('confirmation-yes-button').click();
expect(mockSocket.emit).toHaveBeenCalledWith(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.LEAVE_ROOM,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.LEAVE_ROOM,
mockGames.gameInLobbyAsModerator.accessCode,
{ personId: mockGames.gameInLobbyAsPlayer.client.id }
);
});
it('should display a new player when they join', () => {
mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({
mockSocket.eventHandlers[EVENT_IDS.PLAYER_JOINED]({
name: 'Jane',
id: '123',
userType: globals.USER_TYPES.PLAYER,
userType: USER_TYPES.PLAYER,
out: false,
revealed: false
}, false);
@@ -169,40 +147,20 @@ describe('game page', () => {
});
describe('in-progress game - player view', () => {
let mockSocket;
beforeEach(async () => {
document.body.innerHTML = '';
mockSocket = {
eventHandlers: {},
on: function (message, handler) {
this.eventHandlers[message] = handler;
},
timeout: (duration) => {
return mockSocket;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.inProgressGame)); // copy the game object to prevent leaking of state between specs
break;
default:
break;
}
},
hasListeners: function (listener) {
return false;
},
removeAllListeners: function (...names) {
},
once: function (message, handler) {
this.eventHandlers[message] = handler;
mockSocket.emit = function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.inProgressGame)); // copy the game object to prevent leaking of state between specs
break;
default:
break;
}
};
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
spyOn(mockSocket, 'emit').and.callThrough();
await gameHandler(mockSocket, window, gameTemplate);
mockSocket.eventHandlers.connect();
mockSocket.eventHandlers.getTimeRemaining(120000, true);
await mockSocket.eventHandlers.getTimeRemaining(120000, true);
});
it('should display the game role of the client', () => {
@@ -246,44 +204,24 @@ describe('game page', () => {
});
describe('in-progress game - moderator view', () => {
let mockSocket;
beforeEach(async () => {
document.body.innerHTML = '';
mockSocket = {
eventHandlers: {},
on: function (message, handler) {
this.eventHandlers[message] = handler;
},
timeout: (duration) => {
return mockSocket;
},
once: function (message, handler) {
this.eventHandlers[message] = handler;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.moderatorGame)); // copy the game object to prevent leaking of state between specs
break;
case globals.EVENT_IDS.END_GAME:
args[args.length - 1]();
break;
default:
break;
}
},
hasListeners: function (listener) {
return false;
},
removeAllListeners: function (...names) {
mockSocket.emit = function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](deepCopy(mockGames.moderatorGame)); // copy the game object to prevent leaking of state between specs
break;
case EVENT_IDS.END_GAME:
args[args.length - 1]();
break;
default:
break;
}
};
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
spyOn(mockSocket, 'emit').and.callThrough();
await gameHandler(mockSocket, window, gameTemplate);
mockSocket.eventHandlers.connect();
mockSocket.eventHandlers.getTimeRemaining(120000, true);
spyOn(mockSocket, 'emit');
await mockSocket.eventHandlers.getTimeRemaining(120000, true);
});
it('should display the button to play/pause the timer', () => {
@@ -317,14 +255,14 @@ describe('game page', () => {
.querySelector('.kill-player-button').click();
document.getElementById('confirmation-yes-button').click();
expect(mockSocket.emit).toHaveBeenCalledWith(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.KILL_PLAYER,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.KILL_PLAYER,
mockGames.moderatorGame.accessCode,
{ personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' }
);
mockSocket.eventHandlers.killPlayer({
id: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW',
userType: globals.USER_TYPES.KILLED_PLAYER,
userType: USER_TYPES.KILLED_PLAYER,
out: true,
killed: true,
revealed: false,
@@ -339,8 +277,8 @@ describe('game page', () => {
.querySelector('.reveal-role-button').click();
document.getElementById('confirmation-yes-button').click();
expect(mockSocket.emit).toHaveBeenCalledWith(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.REVEAL_PLAYER,
SOCKET_EVENTS.IN_GAME_MESSAGE,
EVENT_IDS.REVEAL_PLAYER,
mockGames.moderatorGame.accessCode,
{ personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' }
);

View File

@@ -1,9 +1,6 @@
// TODO: clean up these deep relative paths? jsconfig.json is not working...
const Game = require('../../../../server/model/Game');
const globals = require('../../../../server/config/globals');
const EVENT_IDS = globals.EVENT_IDS;
const USER_TYPES = globals.USER_TYPES;
const STATUS = globals.STATUS;
const { ENVIRONMENTS, EVENT_IDS, USER_TYPES, STATUS } = require('../../../../server/config/globals.js');
const GameManager = require('../../../../server/modules/singletons/GameManager.js');
const TimerManager = require('../../../../server/modules/singletons/TimerManager.js');
const EventManager = require('../../../../server/modules/singletons/EventManager.js');
@@ -21,7 +18,7 @@ describe('Events', () => {
const toObj = { emit: () => {} };
namespace = { in: () => { return inObj; }, to: () => { return toObj; }, sockets: new Map() };
socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } };
gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test');
gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, ENVIRONMENTS.PRODUCTION, 'test');
timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test');
eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test');
gameManager.setGameSocketNamespace(namespace);
@@ -83,7 +80,7 @@ describe('Events', () => {
.communicate(game, { id: 'd', assigned: true, userType: USER_TYPES.PLAYER }, { gameManager: gameManager });
expect(namespace.in).toHaveBeenCalledWith(game.accessCode);
expect(namespace.in().emit).toHaveBeenCalledWith(
globals.EVENTS.PLAYER_JOINED,
EVENT_IDS.PLAYER_JOINED,
GameStateCurator.mapPerson({ id: 'd', assigned: true, userType: USER_TYPES.PLAYER }),
game.isStartable
);