some game joining logic

This commit is contained in:
Alec
2021-11-13 01:21:22 -05:00
parent e3ec7beff0
commit 0c6203dec5
22 changed files with 596 additions and 116 deletions

9
client/config/globals.js Normal file
View File

@@ -0,0 +1,9 @@
export const globals = {
USER_SIGNATURE_LENGTH: 25,
ACCESS_CODE_LENGTH: 6,
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState'
}
};

View File

@@ -1,7 +1,8 @@
export class Game { export class Game {
constructor(deck, hasTimer, timerParams=null) { constructor(deck, hasTimer, moderatorName, timerParams=null) {
this.deck = deck; this.deck = deck;
this.hasTimer = hasTimer; this.hasTimer = hasTimer;
this.moderatorName = moderatorName;
this.timerParams = timerParams; this.timerParams = timerParams;
} }
} }

View File

@@ -0,0 +1,41 @@
import { globals } from '../config/globals.js';
export const UserUtility = {
createNewAnonymousUserId (force = true) {
let newId;
const currentId = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
if (currentId !== null && !force) {
newId = currentId;
} else {
newId = createRandomUserId();
sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, newId);
}
return newId;
},
setAnonymousUserId (id) {
sessionStorage.setItem(globals.PLAYER_ID_COOKIE_KEY, id);
},
validateAnonUserSignature () {
const userSig = sessionStorage.getItem(globals.PLAYER_ID_COOKIE_KEY);
return (
userSig
&& typeof userSig === 'string'
&& /^[a-zA-Z0-9]+$/.test(userSig)
&& userSig.length === globals.USER_SIGNATURE_LENGTH
)
? userSig
: false;
}
};
function createRandomUserId () {
let id = '';
for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i++) {
id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)];
}
return id;
}

View File

@@ -3,54 +3,14 @@ import { ModalManager } from "../modules/ModalManager.js";
import { defaultCards } from "../config/defaultCards.js"; import { defaultCards } from "../config/defaultCards.js";
import { customCards } from "../config/customCards.js"; import { customCards } from "../config/customCards.js";
import { DeckStateManager } from "../modules/DeckStateManager.js"; import { DeckStateManager } from "../modules/DeckStateManager.js";
import {XHRUtility} from "../modules/XHRUtility.js"; import { XHRUtility } from "../modules/XHRUtility.js";
import {Game} from "../model/Game.js"; import { Game } from "../model/Game.js";
export const create = () => { export const create = () => {
let deckManager = new DeckStateManager(); let deckManager = new DeckStateManager();
loadDefaultCards(deckManager); loadDefaultCards(deckManager);
loadCustomCards(deckManager); loadCustomCards(deckManager);
document.getElementById("game-form").onsubmit = (e) => { initializeRemainingEventListeners(deckManager);
e.preventDefault();
let timerBool = hasTimer();
let timerParams = timerBool
? {
hours: document.getElementById("game-hours").value,
minutes: document.getElementById("game-minutes").value
}
: null;
if (deckManager.getDeckSize() >= 5) {
createGameForHosting(
deckManager.getCurrentDeck().filter((card) => card.quantity > 0),
timerBool,
timerParams
);
} else {
toast("You must include enough cards for 5 players.", "error", true);
}
}
document.getElementById("add-role-form").onsubmit = (e) => {
e.preventDefault();
let name = document.getElementById("role-name").value.trim();
let description = document.getElementById("role-description").value.trim();
if (!deckManager.getCustomRoleOption(name)) { // confirm there is no existing custom role with the same name
deckManager.addToCustomRoleOptions({role: name, description: description});
updateCustomRoleOptionsList(deckManager, document.getElementById("deck-select"))
ModalManager.dispelModal("add-role-modal", "add-role-modal-background");
toast("Role Added", "success", true);
} else {
toast("There is already a custom role with this name.", "error", true);
}
}
document.getElementById("custom-role-btn").addEventListener(
"click", () => {
ModalManager.displayModal(
"add-role-modal",
"add-role-modal-background",
"close-modal-button"
)
}
)
} }
// Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state. // Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state.
@@ -83,7 +43,7 @@ function loadCustomCards(deckManager) {
form.appendChild(selectEl); form.appendChild(selectEl);
let submitBtn = document.createElement("input"); let submitBtn = document.createElement("input");
submitBtn.setAttribute("type", "submit"); submitBtn.setAttribute("type", "submit");
submitBtn.setAttribute("value", "Add Role"); submitBtn.setAttribute("value", "Include Role");
submitBtn.addEventListener('click', (e) => { submitBtn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
if (selectEl.value && selectEl.value.length > 0) { if (selectEl.value && selectEl.value.length > 0) {
@@ -100,6 +60,51 @@ function loadCustomCards(deckManager) {
deckManager.customRoleOptions = customCards; deckManager.customRoleOptions = customCards;
} }
function initializeRemainingEventListeners(deckManager) {
document.getElementById("game-form").onsubmit = (e) => {
e.preventDefault();
let timerBool = hasTimer();
let timerParams = timerBool
? {
hours: document.getElementById("game-hours").value,
minutes: document.getElementById("game-minutes").value
}
: null;
if (deckManager.getDeckSize() >= 5) {
createGameForHosting(
deckManager.getCurrentDeck().filter((card) => card.quantity > 0),
timerBool,
document.getElementById("mod-name").value,
timerParams
);
} else {
toast("You must include enough cards for 5 players.", "error", true);
}
}
document.getElementById("add-role-form").onsubmit = (e) => {
e.preventDefault();
let name = document.getElementById("role-name").value.trim();
let description = document.getElementById("role-description").value.trim();
if (!deckManager.getCustomRoleOption(name)) { // confirm there is no existing custom role with the same name
deckManager.addToCustomRoleOptions({role: name, description: description});
updateCustomRoleOptionsList(deckManager, document.getElementById("deck-select"))
ModalManager.dispelModal("add-role-modal", "add-role-modal-background");
toast("Role Added", "success", true);
} else {
toast("There is already a custom role with this name.", "error", true);
}
}
document.getElementById("custom-role-btn").addEventListener(
"click", () => {
ModalManager.displayModal(
"add-role-modal",
"add-role-modal-background",
"close-modal-button"
)
}
)
}
function updateCustomRoleOptionsList(deckManager, selectEl) { function updateCustomRoleOptionsList(deckManager, selectEl) {
document.querySelectorAll('#deck-select option').forEach(e => e.remove()); document.querySelectorAll('#deck-select option').forEach(e => e.remove());
addOptionsToList(deckManager.customRoleOptions, selectEl); addOptionsToList(deckManager.customRoleOptions, selectEl);
@@ -158,13 +163,13 @@ function hasTimer() {
return document.getElementById("game-hours").value.length > 0 || document.getElementById("game-minutes").value.length > 0 return document.getElementById("game-hours").value.length > 0 || document.getElementById("game-minutes").value.length > 0
} }
function createGameForHosting(deck, hasTimer, timerParams) { function createGameForHosting(deck, hasTimer, modName, timerParams) {
XHRUtility.xhr( XHRUtility.xhr(
'/api/games/create', '/api/games/create',
'POST', 'POST',
null, null,
JSON.stringify( JSON.stringify(
new Game(deck, hasTimer, timerParams) new Game(deck, hasTimer, modName, timerParams)
) )
) )
.then((res) => { .then((res) => {
@@ -173,7 +178,7 @@ function createGameForHosting(deck, hasTimer, timerParams) {
&& Object.prototype.hasOwnProperty.call(res, 'content') && Object.prototype.hasOwnProperty.call(res, 'content')
&& typeof res.content === 'string' && typeof res.content === 'string'
) { ) {
window.location = ('/games/' + res.content); window.location = ('/game/' + res.content);
} }
}); });
} }

View File

@@ -0,0 +1,22 @@
import { UserUtility } from "../modules/UserUtility.js";
import { globals } from "../config/globals.js";
export const game = () => {
let userId = UserUtility.validateAnonUserSignature();
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.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) {
if (gameState === null) {
window.location.replace('/not-found');
} else {
console.log(gameState);
userId = gameState.id;
UserUtility.setAnonymousUserId(userId);
// processGameState(gameState, userId, socket);
}
});
} else {
window.location.replace('/not-found');
}
};

View File

@@ -1,2 +1,37 @@
import { XHRUtility } from "../modules/XHRUtility.js";
import { toast } from "../modules/Toast.js";
export const home = () => { export const home = () => {
document.getElementById("join-form").onsubmit = (e) => {
e.preventDefault();
let userCode = document.getElementById("room-code").value;
if (roomCodeIsValid(userCode)) {
attemptToJoinGame(userCode);
}
}
}; };
function roomCodeIsValid(code) {
return typeof code === "string" && /^[a-z0-9]{6}$/.test(code);
}
function attemptToJoinGame(code) {
XHRUtility.xhr(
'/api/games/availability/' + code,
'GET',
null,
null
)
.then((res) => {
if (res.status === 200) {
window.location = '/game/' + res.content;
} else 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);
}
});
}

View File

@@ -32,7 +32,7 @@ body {
align-items: flex-start; align-items: flex-start;
width: 95%; width: 95%;
margin: 0 auto; margin: 0 auto;
max-width: 75em; max-width: 68em;
} }
h1 { h1 {
@@ -58,6 +58,13 @@ label {
font-weight: normal; font-weight: normal;
} }
input, textarea {
background-color: transparent;
border: 1px solid white;
border-radius: 3px;
color: #f7f7f7;
}
textarea, input { textarea, input {
font-family: 'signika-negative', sans-serif; font-family: 'signika-negative', sans-serif;
font-size: 16px; font-size: 16px;
@@ -66,12 +73,24 @@ textarea, input {
button, input[type="submit"] { button, input[type="submit"] {
font-family: 'signika-negative', sans-serif !important; font-family: 'signika-negative', sans-serif !important;
padding: 10px; padding: 10px;
background-color: #1f1f1f; background-color: black;
border: none;
border-radius: 3px; border-radius: 3px;
color: white; color: whitesmoke;
font-size: 18px; font-size: 18px;
cursor: pointer; cursor: pointer;
border: 2px solid transparent;
}
button:active, input[type=submit]:active {
border: 2px solid #21ba45;
}
.container {
padding: 5px;
border-radius: 3px;
display: flex;
flex-direction: column;
justify-content: center;
} }
button:hover, input[type="submit"]:hover { button:hover, input[type="submit"]:hover {
@@ -109,7 +128,6 @@ input {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 5px; padding: 5px;
margin-bottom: -2em;
width: 100%; width: 100%;
} }

View File

@@ -36,6 +36,10 @@
border: 2px solid #0075F2; border: 2px solid #0075F2;
} }
.search {
color: black;
}
.compact-card-right p { .compact-card-right p {
font-size: 40px; font-size: 40px;
margin: 0 10px 0 0; margin: 0 10px 0 0;
@@ -80,6 +84,9 @@
#deck-container, #custom-roles-container { #deck-container, #custom-roles-container {
margin: 0.5em 0; margin: 0.5em 0;
background-color: #1f1f1f;
padding: 10px;
border-radius: 3px;
} }
#deck { #deck {
@@ -91,7 +98,6 @@
form { form {
width: 100%; width: 100%;
margin: 1em 0;
} }
select { select {
@@ -100,19 +106,14 @@ select {
font-family: 'signika-negative', sans-serif; font-family: 'signika-negative', sans-serif;
} }
#deck-container, #custom-roles-container {
border: 1px solid #3d4448;
padding: 10px;
border-radius: 3px;
}
#game-form > div { #game-form > div {
background-color: #1f1f1f;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #3d4448;
padding: 10px; padding: 10px;
border-radius: 3px; border-radius: 3px;
width: fit-content; width: fit-content;
margin: 1em 0;
} }
#game-form > div > label { #game-form > div > label {
@@ -131,6 +132,14 @@ label[for="game-time"], label[for="add-card-to-deck-form"], label[for="deck"] {
font-weight: bold; font-weight: bold;
} }
input[type="number"] {
min-width: 3em;
}
#add-card-to-deck-form {
margin-bottom: 1em;
}
#create-game{ #create-game{
color: #45a445; color: #45a445;
font-size: 30px; font-size: 30px;

0
client/styles/game.css Normal file
View File

View File

@@ -15,6 +15,14 @@ form {
padding: 10px; padding: 10px;
border-radius: 3px; border-radius: 3px;
background-color: #1f1f1f; background-color: #1f1f1f;
justify-content: center;
align-items: center;
}
#join-button {
min-width: 6em;
max-height: 3em;
color: #21ba45;
} }
h3 { h3 {
@@ -33,13 +41,6 @@ form > div {
margin: 1em; margin: 1em;
} }
input[type="text"] {
background-color: transparent;
border: 1px solid white;
border-radius: 3px;
color: #f7f7f7;
}
#join-container > label { #join-container > label {
font-size: 35px; font-size: 35px;
font-family: 'diavlo', sans-serif; font-family: 'diavlo', sans-serif;

File diff suppressed because one or more lines are too long

View File

@@ -26,55 +26,58 @@
</head> </head>
<body> <body>
<div id="navbar"> <div id="navbar">
<a href="/">
<img alt="logo" src="../images/Werewolf_Small.png"/>
</a>
<a href="/">Home</a> <a href="/">Home</a>
</div> </div>
<div id="add-role-modal-background" class="modal-background" style="display: none"></div> <div class="container">
<div id="add-role-modal" class="modal" style="display: none"> <div id="add-role-modal-background" class="modal-background" style="display: none"></div>
<form id="add-role-form"> <div id="add-role-modal" class="modal" style="display: none">
<form id="add-role-form">
<div>
<label for="role-name">Role Name</label>
<input id="role-name" type="text" placeholder="Name your role..." required/>
</div>
<div>
<label for="role-description">Description</label>
<textarea style="resize:none" id="role-description" rows="10" cols="30" placeholder="Describe your role..." required></textarea>
</div>
<div id="modal-button-container">
<button id="close-modal-button">Close</button>
<input type="submit" id="create-role-button" value="Create Role"/>
</div>
</form>
</div>
<h1>Create A Game</h1>
<h3>
Creating a game gives you the moderator role with certain special permissions. You will not be dealt a card.
</h3>
<div id="custom-roles-container">
<label for="add-card-to-deck-form">Custom Roles</label>
<form id="add-card-to-deck-form"></form>
<button id="custom-role-btn">Create Custom Role</button>
</div>
<div id="deck-container">
<label for="deck">Game Deck: 0 Players</label>
<div id="deck"></div>
</div>
<form id="game-form">
<div> <div>
<label for="role-name">Role Name</label> <label for="game-time">Timer (Optional)</label>
<input id="role-name" type="text" placeholder="Name your role..." required/> <div id="game-time">
<label for="game-hours">Hours (max 5)</label>
<input type="number" id="game-hours" name="game-hours"
min="0" max="5" />
<label for="game-hours">Minutes</label>
<input type="number" id="game-minutes" name="game-minutes"
min="1" max="60" />
</div>
</div> </div>
<div> <div>
<label for="role-description">Description</label> <label for="mod-name">Your Name</label>
<textarea style="resize:none" id="role-description" rows="10" cols="30" placeholder="Describe your role..." required></textarea> <input id="mod-name" type="text" maxlength="30" required/>
</div>
<div id="modal-button-container">
<button id="close-modal-button">Close</button>
<input type="submit" id="create-role-button" value="Create Role"/>
</div> </div>
<input id="create-game" type="submit" value="Create"/>
</form> </form>
</div> </div>
<h1>Create A Game</h1>
<h3>
Creating a game gives you the moderator role with certain special permissions. You will not be dealt a card.
</h3>
<div id="custom-roles-container">
<label for="add-card-to-deck-form">Custom Roles</label>
<form id="add-card-to-deck-form"></form>
<button id="custom-role-btn">Create Custom Role</button>
</div>
<div id="deck-container">
<label for="deck">Game Deck: 0 Players</label>
<div id="deck"></div>
</div>
<form id="game-form">
<div>
<label for="game-time">Timer (Optional)</label>
<div id="game-time">
<label for="game-hours">Hours (max 5)</label>
<input type="number" id="game-hours" name="game-hours"
min="0" max="5" />
<label for="game-hours">Minutes</label>
<input type="number" id="game-minutes" name="game-minutes"
min="1" max="60" />
</div>
</div>
<input id="create-game" type="submit" value="Create"/>
</form>
<script type="module"> <script type="module">
import { create } from "../scripts/create.js"; import { create } from "../scripts/create.js";
create(); create();

31
client/views/game.html Normal file
View File

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Active Game</title>
<meta name="description" content="Join or spectate this game of werewolf.">
<meta property="og:title" content="Werewolf Utility - Active Game">
<meta property="og:type" content="website">
<meta property="og:url" content="https://play-werewolf.herokuapp.com/create">
<meta property="og:description" content="Join or spectate this game of werewolf.">
<meta property="og:image" content="image.png">
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="../styles/GLOBAL.css">
<link rel="stylesheet" href="../styles/game.css">
<link rel="stylesheet" href="../styles/modal.css">
</head>
<body>
<script type="module">
import { game } from "../scripts/game.js";
game();
</script>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io('/in-game');
</script>
</body>
</html>

View File

@@ -32,10 +32,7 @@
<label for="room-code">Room Code</label> <label for="room-code">Room Code</label>
<input id="room-code" type="text" placeholder="six-character code..." required/> <input id="room-code" type="text" placeholder="six-character code..." required/>
</div> </div>
<div> <input id="join-button" type="submit" value="Join"/>
<label for="player-name">Your Name</label>
<input id="player-name" type="text" required/>
</div>
</form> </form>
</div> </div>
<script type="module"> <script type="module">

View File

@@ -17,4 +17,20 @@ router.post('/create', function (req, res) {
}); });
}); });
router.get('/availability/:code', function (req, res) {
const joinGamePromise = gameManager.joinGame(req.params.code);
joinGamePromise.then((result) => {
if (result === 404) {
res.status(404).send();
} else if (result instanceof Error) {
res.status(400).send(result.message);
} else if (typeof result === "string") {
logger.debug(result);
res.status(200).send(result);
} else {
res.status(500).send();
}
});
});
module.exports = router; module.exports = router;

View File

@@ -1,6 +1,22 @@
const globals = { const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789', ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6, ACCESS_CODE_LENGTH: 6,
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
TOGGLE_READY: 'toggleReady',
PROCESS_GUESS: 'processGuess'
},
STATUS: {
LOBBY: "lobby"
},
USER_SIGNATURE_LENGTH: 25,
USER_TYPES: {
MODERATOR: "moderator",
PLAYER: "player"
},
ERROR_MESSAGE: {
GAME_IS_FULL: "This game is full"
}
}; };
module.exports = globals; module.exports = globals;

View File

@@ -54,6 +54,9 @@ const io = socketIO(main);
app.set('port', port); app.set('port', port);
const inGame = io.of('/in-game');
/* Instantiate the singleton game manager */ /* Instantiate the singleton game manager */
const gameManager = new GameManager(logger).getInstance(); const gameManager = new GameManager(logger).getInstance();
@@ -86,6 +89,10 @@ app.use(function (req, res) {
res.sendFile(path.join(__dirname, '../client/views/404.html')); res.sendFile(path.join(__dirname, '../client/views/404.html'));
}); });
inGame.on('connection', function (socket) {
gameManager.addGameSocketHandlers(inGame, socket);
});
main.listen(port, function () { main.listen(port, function () {
logger.log(`Starting server on port ${port} http://localhost:${port}` ); logger.log(`Starting server on port ${port} http://localhost:${port}` );
}); });

11
server/model/Game.js Normal file
View File

@@ -0,0 +1,11 @@
class Game {
constructor(status, people, deck, hasTimer, timerParams=null) {
this.status = status;
this.people = people;
this.deck = deck;
this.hasTimer = hasTimer;
this.timerParams = timerParams;
}
}
module.exports = Game;

13
server/model/Person.js Normal file
View File

@@ -0,0 +1,13 @@
class Person {
constructor(id, name, userType, gameRole=null, gameRoleDescription=null, assigned=false) {
this.id = id;
this.socketId = null;
this.name = name;
this.userType = userType;
this.gameRole = gameRole;
this.gameRoleDescription = gameRoleDescription;
this.assigned = assigned;
}
}
module.exports = Person;

View File

@@ -0,0 +1,96 @@
const { fork } = require('child_process');
const path = require('path');
const logger = require('./logger')(false);
class ActiveGameRunner {
constructor () {
this.activeGames = {};
}
// runGame = (game, namespace, gameStateFn) => {
// logger.debug('running game ' + game.accessCode);
// const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
// gameProcess.on('message', (msg) => {
// switch (msg.command) {
// case serverGlobals.COMMAND.END_COUNTDOWN:
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: END COUNTDOWN');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_COUNTDOWN);
// gameProcess.send({
// command: serverGlobals.COMMAND.START_GAME,
// cycleNumber: game.words.length - 1,
// cycleLength: game.timePerWord * 1000,
// accessCode: game.accessCode
// });
// break;
// case serverGlobals.COMMAND.START_GAME:
// game.status = serverGlobals.GAME_STATE.STARTED;
// game.lastCycleTime = new Date().toJSON();
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: START GAME');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_GAME, {
// firstWord: game.words[0].baseword,
// gameLength: game.words.length,
// timePerWord: game.timePerWord * 1000
// });
// break;
// case serverGlobals.COMMAND.CYCLE_WORD:
// game.currentWordIndex += 1;
// game.lastCycleTime = new Date().toJSON();
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: CYCLE WORD');
// if (game.currentWordIndex < game.words.length) {
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.CYCLE_WORD, {
// word: game.words[game.currentWordIndex].baseword,
// index: game.currentWordIndex + 1,
// totalTime: game.timePerWord * 1000,
// gameLength: game.words.length
// });
// }
// gameProcess.send({
// command: serverGlobals.COMMAND.CYCLE_WORD,
// cycleIndex: game.currentWordIndex,
// cycleLength: game.timePerWord * 1000,
// accessCode: game.accessCode,
// gameLength: game.words.length
// });
// break;
// case serverGlobals.COMMAND.END_GAME:
// game.status = serverGlobals.GAME_STATE.ENDED;
// if (!game.posted) {
// logger.debug('GAME PARENT PROCESS: GAME ' + game.accessCode + ' HAS ENDED...BEGINNING POST TO DATABASE');
// this.postGameFn(game).then(() => {
// game.posted = true;
// logger.debug('GAME ' + game.accessCode + ' SUCCESSFULLY POSTED');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_GAME, game.accessCode);
// });
// }
// break;
// }
// });
//
// gameProcess.on('exit', () => {
// if (this.activeGames[game.accessCode]) {
// delete this.activeGames[game.accessCode];
// logger.debug('GAME ' + game.accessCode + ' REMOVED FROM ACTIVE GAMES.');
// }
// });
// gameProcess.send({ command: serverGlobals.COMMAND.START_COUNTDOWN, accessCode: game.accessCode });
// game.status = serverGlobals.GAME_STATE.STARTING;
// game.startCountdownTime = new Date().toJSON();
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_COUNTDOWN);
// }
}
class Singleton {
constructor () {
if (!Singleton.instance) {
logger.log('CREATING SINGLETON ACTIVE GAME RUNNER');
Singleton.instance = new ActiveGameRunner();
}
}
getInstance () {
return Singleton.instance;
}
}
module.exports = Singleton;

View File

@@ -1,23 +1,56 @@
const globals = require('../config/globals'); const globals = require('../config/globals');
const ActiveGameRunner = require('./ActiveGameRunner');
const Game = require('../model/Game');
const Person = require('../model/Person');
class GameManager { class GameManager {
constructor (logger) { constructor (logger) {
this.logger = logger; this.logger = logger;
//this.activeGameRunner = new ActiveGameRunner(this.postGame).getInstance(); this.activeGameRunner = new ActiveGameRunner().getInstance();
this.namespace = null;
//this.gameSocketUtility = GameSocketUtility; //this.gameSocketUtility = GameSocketUtility;
} }
addGameSocketHandlers = (namespace, socket) => {
this.namespace = namespace;
socket.on(globals.CLIENT_COMMANDS.FETCH_GAME_STATE, (accessCode, personId, ackFn) => {
handleRequestForGameState(this.namespace, this.logger, this.activeGameRunner, accessCode, personId, ackFn, socket);
});
}
createGame = (gameParams) => { createGame = (gameParams) => {
const expectedKeys = ['deck', 'hasTimer', 'timerParams']; const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName'];
if (typeof gameParams !== 'object' || expectedKeys.some((key) => !Object.keys(gameParams).includes(key))) { if (typeof gameParams !== 'object' || expectedKeys.some((key) => !Object.keys(gameParams).includes(key))) {
this.logger.error('Tried to create game with invalid options: ' + JSON.stringify(gameParams)); this.logger.error('Tried to create game with invalid options: ' + JSON.stringify(gameParams));
return Promise.reject('Tried to create game with invalid options: ' + gameParams); return Promise.reject('Tried to create game with invalid options: ' + gameParams);
} else { } else {
const newAccessCode = this.generateAccessCode(); const newAccessCode = this.generateAccessCode();
this.activeGameRunner.activeGames[newAccessCode] = new Game(
globals.STATUS.LOBBY,
initializePeopleForGame(gameParams.moderatorName, gameParams.deck),
gameParams.deck,
gameParams.hasTimer,
gameParams.timerParams
);
return Promise.resolve(newAccessCode); return Promise.resolve(newAccessCode);
} }
} }
joinGame = (code) => {
let game = this.activeGameRunner.activeGames[code];
if (game) {
let unassignedPerson = game.people.find((person) => person.assigned === false);
if (!unassignedPerson) {
return Promise.resolve(new Error(globals.ERROR_MESSAGE.GAME_IS_FULL));
} else {
return Promise.resolve(code);
}
} else {
return Promise.resolve(404);
}
}
generateAccessCode = () => { generateAccessCode = () => {
const numLetters = globals.ACCESS_CODE_CHAR_POOL.length; const numLetters = globals.ACCESS_CODE_CHAR_POOL.length;
const codeDigits = []; const codeDigits = [];
@@ -36,6 +69,49 @@ function getRandomInt (max) {
return Math.floor(Math.random() * Math.floor(max)); return Math.floor(Math.random() * Math.floor(max));
} }
function initializeModerator(name) {
return new Person(createRandomUserId(), name, globals.USER_TYPES.MODERATOR)
}
function initializePeopleForGame(modName, uniqueCards) {
let people = [];
let cards = []; // this will contain copies of each card equal to the quantity.
people.push(initializeModerator(modName));
let numberOfRoles = 0;
for (let card of uniqueCards) {
for (let i = 0; i < card.quantity; i ++) {
cards.push(card);
numberOfRoles ++;
}
}
cards = shuffleArray(cards); // The deck should probably be shuffled, ey?.
for(let j = 0; j < numberOfRoles; j ++) {
people.push(new Person(createRandomUserId(), null, globals.USER_TYPES.PLAYER, cards[j].role, cards[j].description))
}
return people;
}
function shuffleArray (array) {
for (let i = 0; i < array.length; i++) {
const randIndex = Math.floor(Math.random() * i);
const temp = array[i];
array[i] = array[randIndex];
array[randIndex] = temp;
}
return array;
}
function createRandomUserId () {
let id = '';
for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i++) {
id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)];
}
return id;
}
class Singleton { class Singleton {
constructor (logger) { constructor (logger) {
if (!Singleton.instance) { if (!Singleton.instance) {
@@ -49,4 +125,72 @@ class Singleton {
} }
} }
/* Since clients are anonymous, we have to rely to some extent on a cookie to identify them. Socket ids
are unique to a client, but they are re-generated if a client disconnects and then reconnects.
Thus, to have the most resilient identification i.e. to let them refresh, navigate away and come back,
get disconnected and reconnect, etc. we should have a combination of the socket id and the cookie. This
will also allow us to reject certain theoretical ways of breaking things, such as copying someone else's
cookie. Though if a client wants to clear their cookie and reset their connection, there's not much we can do.
The best thing in my opinion is to make it hard for clients to _accidentally_ break their experience.
*/
function handleRequestForGameState(namespace, logger, gameRunner, accessCode, personId, ackFn, socket) {
const game = gameRunner.activeGames[accessCode];
if (game) {
let matchingPerson = game.people.find((person) => person.id === personId);
if (matchingPerson) {
if (matchingPerson.socketId === socket.id) {
logger.debug("matching person found with an established connection to the room: " + matchingPerson.name);
ackFn(getGameStateFromPerspectiveOfPerson(game, matchingPerson));
} else {
if (!roomContainsSocketOfMatchingPerson(namespace, matchingPerson, logger, accessCode)) {
logger.debug("matching person found with a new connection to the room: " + matchingPerson.name);
socket.join(accessCode);
matchingPerson.socketId = socket.id;
ackFn(getGameStateFromPerspectiveOfPerson(game, matchingPerson));
} else {
rejectClientRequestForGameState(ackFn);
}
}
} else {
let personWithMatchingSocketId = findPersonWithMatchingSocketId(game.people, socket.id);
if (personWithMatchingSocketId) {
logger.debug("matching person found whose cookie got cleared after establishing a connection to the room: " + personWithMatchingSocketId.name);
ackFn(getGameStateFromPerspectiveOfPerson(game, personWithMatchingSocketId));
} else {
let unassignedPerson = game.people.find((person) => person.assigned === false);
if (unassignedPerson) {
logger.debug("completely new person with a first connection to the room: " + unassignedPerson.name);
socket.join(accessCode);
unassignedPerson.assigned = true;
unassignedPerson.socketId = socket.id;
ackFn(getGameStateFromPerspectiveOfPerson(game, unassignedPerson));
} else {
rejectClientRequestForGameState(ackFn);
}
}
}
} else {
rejectClientRequestForGameState(ackFn);
}
}
function getGameStateFromPerspectiveOfPerson(game, person) {
return person;
}
// in socket.io 2.x , the rooms property is an object. in 3.x and 4.x, it is a javascript Set.
function roomContainsSocketOfMatchingPerson(namespace, matchingPerson, logger, accessCode) {
return namespace.adapter
&& namespace.adapter.rooms[accessCode]
&& namespace.adapter.rooms[accessCode].sockets[matchingPerson.socketId];
}
function rejectClientRequestForGameState(acknowledgementFunction) {
return acknowledgementFunction(null);
}
function findPersonWithMatchingSocketId(people, socketId) {
return people.find((person) => person.socketId === socketId);
}
module.exports = Singleton; module.exports = Singleton;

View File

@@ -10,4 +10,9 @@ router.get('/create', function (request, response) {
response.sendFile(path.join(__dirname, '../../client/views/create.html')); response.sendFile(path.join(__dirname, '../../client/views/create.html'));
}); });
router.get('/game/:code', function (request, response) {
response.sendFile(path.join(__dirname, '../../client/views/game.html'));
});
module.exports = router; module.exports = router;