some timer logic
@@ -1,5 +1,6 @@
|
||||
export const globals = {
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 100,
|
||||
ACCESS_CODE_LENGTH: 6,
|
||||
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
|
||||
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
@@ -18,7 +19,8 @@ export const globals = {
|
||||
},
|
||||
EVENTS: {
|
||||
PLAYER_JOINED: "playerJoined",
|
||||
SYNC_GAME_STATE: "syncGameState"
|
||||
SYNC_GAME_STATE: "syncGameState",
|
||||
START_TIMER: "startTimer"
|
||||
},
|
||||
USER_TYPES: {
|
||||
MODERATOR: "moderator",
|
||||
|
||||
BIN
client/images/roles/Double-BlindMinion.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/images/roles/DreamWolf.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
client/images/roles/Hunter.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/images/roles/KnowingMinion.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client/images/roles/Mason.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
client/images/roles/Seer.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
client/images/roles/Shadow.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
client/images/roles/Sorcerer.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
client/images/roles/Villager.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
client/images/roles/Werewolf.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
@@ -485,6 +485,9 @@ function updateCustomRoleOptionsList(deckManager, selectEl) {
|
||||
}
|
||||
|
||||
function addOptionsToList(options, selectEl) {
|
||||
options.sort((a, b) => {
|
||||
return a.role.localeCompare(b.role);
|
||||
});
|
||||
for (let i = 0; i < options.length; i ++) {
|
||||
let optionEl = document.createElement("option");
|
||||
let alignmentClass = customCards[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL
|
||||
|
||||
@@ -4,6 +4,7 @@ import { toast } from "./Toast.js";
|
||||
export class GameStateRenderer {
|
||||
constructor(gameState) {
|
||||
this.gameState = gameState;
|
||||
this.cardFlipped = false;
|
||||
}
|
||||
|
||||
renderLobbyPlayers() {
|
||||
@@ -79,6 +80,20 @@ export class GameStateRenderer {
|
||||
}
|
||||
name.setAttribute("title", this.gameState.client.gameRole);
|
||||
document.querySelector('#role-description').innerText = this.gameState.client.gameRoleDescription;
|
||||
document.getElementById("role-image").setAttribute(
|
||||
'src',
|
||||
'../images/roles/' + this.gameState.client.gameRole.replaceAll(' ', '') + '.png'
|
||||
);
|
||||
|
||||
document.getElementById("game-role-back").addEventListener('click', () => {
|
||||
document.getElementById("game-role").style.display = 'flex';
|
||||
document.getElementById("game-role-back").style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById("game-role").addEventListener('click', () => {
|
||||
document.getElementById("game-role-back").style.display = 'flex';
|
||||
document.getElementById("game-role").style.display = 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const templates = {
|
||||
"<div id='person-name'></div>" +
|
||||
"<div id='game-header'>" +
|
||||
"<div>" +
|
||||
"<label for='game-timer'>Timer</label>" +
|
||||
"<label for='game-timer'>Time Remaining</label>" +
|
||||
"<div id='game-timer'></div>" +
|
||||
"</div>" +
|
||||
"<div>" +
|
||||
@@ -37,9 +37,13 @@ export const templates = {
|
||||
"<div id='alive-count'></div>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"<div id='game-role'>" +
|
||||
"<div id='game-role' style='display:none'>" +
|
||||
"<h4 id='role-name'></h4>" +
|
||||
"<img alt='role' id='role-image'/>" +
|
||||
"<p id='role-description'></p>" +
|
||||
"</div>" +
|
||||
"<div id='game-role-back'>" +
|
||||
"<h4>Click to reveal your role</h4>" +
|
||||
"<p>(click again to hide)</p>" +
|
||||
"</div>"
|
||||
}
|
||||
|
||||
@@ -9,16 +9,18 @@ See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
|
||||
const messageParameters = {
|
||||
STOP: 'stop',
|
||||
TOTAL_TIME: 'totalTime',
|
||||
TICK_INTERVAL: 'tickInterval'
|
||||
TICK_INTERVAL: 'tickInterval',
|
||||
HOURS: 'hours',
|
||||
MINUTES: 'minutes'
|
||||
};
|
||||
|
||||
onmessage = function (e) {
|
||||
if (typeof e.data === 'object'
|
||||
&& e.data.hasOwnProperty(messageParameters.TOTAL_TIME)
|
||||
&& e.data.hasOwnProperty(messageParameters.HOURS)
|
||||
&& e.data.hasOwnProperty(messageParameters.MINUTES)
|
||||
&& e.data.hasOwnProperty(messageParameters.TICK_INTERVAL)
|
||||
) {
|
||||
const timer = new Singleton(e.data.totalTime, e.data.tickInterval);
|
||||
const timer = new Singleton(e.data.hours, e.data.minutes, e.data.tickInterval);
|
||||
timer.startTimer();
|
||||
}
|
||||
};
|
||||
@@ -30,7 +32,10 @@ function stepFn (expected, interval, start, totalTime) {
|
||||
}
|
||||
const delta = now - expected;
|
||||
expected += interval;
|
||||
postMessage({ timeRemaining: (totalTime - (expected - start)) / 1000 });
|
||||
postMessage({
|
||||
timeRemainingInMilliseconds: totalTime - (expected - start),
|
||||
displayTime: returnHumanReadableTime(totalTime - (expected - start))
|
||||
});
|
||||
Singleton.setNewTimeoutReference(setTimeout(() => {
|
||||
stepFn(expected, interval, start, totalTime);
|
||||
}, Math.max(0, interval - delta)
|
||||
@@ -38,16 +43,17 @@ function stepFn (expected, interval, start, totalTime) {
|
||||
}
|
||||
|
||||
class Timer {
|
||||
constructor (totalTime, tickInterval) {
|
||||
constructor (hours, minutes, tickInterval) {
|
||||
this.timeoutId = undefined;
|
||||
this.totalTime = totalTime;
|
||||
this.hours = hours;
|
||||
this.minutes = minutes;
|
||||
this.tickInterval = tickInterval;
|
||||
}
|
||||
|
||||
startTimer () {
|
||||
if (!isNaN(this.totalTime) && !isNaN(this.tickInterval)) {
|
||||
if (!isNaN(this.hours) && !isNaN(this.minutes) && !isNaN(this.tickInterval)) {
|
||||
const interval = this.tickInterval;
|
||||
const totalTime = this.totalTime;
|
||||
const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
|
||||
const start = Date.now();
|
||||
const expected = Date.now() + this.tickInterval;
|
||||
if (this.timeoutId) {
|
||||
@@ -61,19 +67,20 @@ class Timer {
|
||||
}
|
||||
|
||||
class Singleton {
|
||||
constructor (totalTime, tickInterval) {
|
||||
constructor (hours, minutes, tickInterval) {
|
||||
if (!Singleton.instance) {
|
||||
Singleton.instance = new Timer(totalTime, tickInterval);
|
||||
Singleton.instance = new Timer(hours, minutes, tickInterval);
|
||||
} else {
|
||||
// This allows the same timer to be configured to run for different intervals / at a different granularity.
|
||||
Singleton.setNewTimerParameters(totalTime, tickInterval);
|
||||
Singleton.setNewTimerParameters(hours, minutes, tickInterval);
|
||||
}
|
||||
return Singleton.instance;
|
||||
}
|
||||
|
||||
static setNewTimerParameters (totalTime, tickInterval) {
|
||||
static setNewTimerParameters (hours, minutes, tickInterval) {
|
||||
if (Singleton.instance) {
|
||||
Singleton.instance.totalTime = totalTime;
|
||||
Singleton.instance.hours = hours;
|
||||
Singleton.instance.minutes = minutes;
|
||||
Singleton.instance.tickInterval = tickInterval;
|
||||
}
|
||||
}
|
||||
@@ -84,3 +91,24 @@ class Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertFromMinutesToMilliseconds(minutes) {
|
||||
return minutes * 60 * 1000;
|
||||
}
|
||||
|
||||
function convertFromHoursToMilliseconds(hours) {
|
||||
return hours * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
function returnHumanReadableTime(milliseconds) {
|
||||
|
||||
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 hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ export const game = () => {
|
||||
userId = gameState.client.id;
|
||||
UserUtility.setAnonymousUserId(userId, environment);
|
||||
let gameStateRenderer = new GameStateRenderer(gameState);
|
||||
processGameState(gameState, userId, socket, gameStateRenderer); // this socket is initialized via a script tag in the game page HTML.
|
||||
setClientSocketHandlers(gameStateRenderer, socket);
|
||||
const timerWorker = new Worker('../modules/Timer.js');
|
||||
processGameState(gameState, userId, socket, gameStateRenderer, timerWorker); // this socket is initialized via a script tag in the game page HTML.
|
||||
setClientSocketHandlers(gameStateRenderer, socket, timerWorker);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -29,7 +30,7 @@ export const game = () => {
|
||||
});
|
||||
};
|
||||
|
||||
function processGameState (gameState, userId, socket, gameStateRenderer) {
|
||||
function processGameState (gameState, userId, socket, gameStateRenderer, timerWorker) {
|
||||
cancelCurrentToast();
|
||||
switch (gameState.status) {
|
||||
case globals.STATUS.LOBBY:
|
||||
@@ -58,7 +59,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
|
||||
}
|
||||
}
|
||||
|
||||
function setClientSocketHandlers(gameStateRenderer, socket) {
|
||||
function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) {
|
||||
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
|
||||
toast(player.name + " joined!", "success", false);
|
||||
gameStateRenderer.gameState.people.push(player);
|
||||
@@ -84,6 +85,16 @@ function setClientSocketHandlers(gameStateRenderer, socket) {
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
socket.on(globals.EVENTS.START_TIMER, () => {
|
||||
runGameTimer(
|
||||
gameStateRenderer.gameState.timerParams.hours,
|
||||
gameStateRenderer.gameState.timerParams.minutes,
|
||||
globals.CLOCK_TICK_INTERVAL_MILLIS,
|
||||
null,
|
||||
timerWorker
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function displayStartGamePromptForModerators(gameStateRenderer) {
|
||||
@@ -99,3 +110,14 @@ function displayStartGamePromptForModerators(gameStateRenderer) {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function runGameTimer (hours, minutes, tickRate, soundManager, timerWorker) {
|
||||
if (window.Worker) {
|
||||
timerWorker.onmessage = function (e) {
|
||||
if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) {
|
||||
document.getElementById('game-timer').innerText = e.data.displayTime;
|
||||
}
|
||||
};
|
||||
timerWorker.postMessage({ hours: hours, minutes: minutes, tickInterval: tickRate });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ h1 {
|
||||
|
||||
#game-role {
|
||||
position: relative;
|
||||
border-bottom: 2px solid gray;
|
||||
border: 5px solid transparent;
|
||||
background-color: #e7e7e7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -85,6 +85,56 @@ h1 {
|
||||
/*transform-style: preserve-3d;*/
|
||||
}
|
||||
|
||||
#game-role-back {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #333243;
|
||||
border: 5px solid #61606a;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
max-width: 17em;
|
||||
border-radius: 3px;
|
||||
height: 23em;
|
||||
margin: 0 auto 2em auto;
|
||||
width: 100%;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,0.11),
|
||||
0 2px 2px rgba(0,0,0,0.11),
|
||||
0 4px 4px rgba(0,0,0,0.11),
|
||||
0 8px 8px rgba(0,0,0,0.11),
|
||||
0 16px 16px rgba(0,0,0,0.11),
|
||||
0 32px 32px rgba(0,0,0,0.11);
|
||||
/*perspective: 1000px;*/
|
||||
/*transform-style: preserve-3d;*/
|
||||
}
|
||||
|
||||
#game-role-back h4 {
|
||||
font-size: 24px;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
color: #e7e7e7;
|
||||
}
|
||||
|
||||
#game-role-back p {
|
||||
color: #c3c3c3;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#game-timer {
|
||||
padding: 1px;
|
||||
background-color: #3c3c3c;
|
||||
color: whitesmoke;
|
||||
border-radius: 3px;
|
||||
font-size: 35px;
|
||||
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
|
||||
border: 1px solid #747474;
|
||||
}
|
||||
|
||||
#role-name {
|
||||
position: absolute;
|
||||
top: 6%;
|
||||
@@ -100,10 +150,15 @@ h1 {
|
||||
}
|
||||
|
||||
#role-image {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
position: absolute;
|
||||
top: 34%;
|
||||
top: 37%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 78%;
|
||||
}
|
||||
|
||||
#role-description {
|
||||
@@ -114,7 +169,7 @@ h1 {
|
||||
transform: translate(-50%, 0);
|
||||
font-size: 16px;
|
||||
width: 78%;
|
||||
max-height: 7em;
|
||||
max-height: 6em;
|
||||
}
|
||||
|
||||
#game-link img {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const globals = {
|
||||
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
ACCESS_CODE_LENGTH: 6,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 100,
|
||||
CLIENT_COMMANDS: {
|
||||
FETCH_GAME_STATE: 'fetchGameState',
|
||||
GET_ENVIRONMENT: 'getEnvironment',
|
||||
@@ -8,7 +9,8 @@ const globals = {
|
||||
},
|
||||
STATUS: {
|
||||
LOBBY: "lobby",
|
||||
IN_PROGRESS: "in progress"
|
||||
IN_PROGRESS: "in progress",
|
||||
ENDED: "ended"
|
||||
},
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
USER_TYPES: {
|
||||
@@ -33,6 +35,11 @@ const globals = {
|
||||
ERROR: "error",
|
||||
WARN: "warn",
|
||||
TRACE: "trace"
|
||||
},
|
||||
GAME_PROCESS_COMMANDS: {
|
||||
END_GAME: "endGame",
|
||||
START_GAME: "startGame",
|
||||
START_TIMER: "startTimer"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Game {
|
||||
constructor(status, people, deck, hasTimer, moderator, timerParams=null) {
|
||||
constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) {
|
||||
this.accessCode = accessCode
|
||||
this.status = status;
|
||||
this.moderator = moderator;
|
||||
this.people = people;
|
||||
|
||||
@@ -1,90 +1,49 @@
|
||||
const { fork } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const logger = require('./logger')(false);
|
||||
const globals = require('../config/globals');
|
||||
|
||||
class ActiveGameRunner {
|
||||
constructor () {
|
||||
constructor (logger) {
|
||||
this.activeGames = {};
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// }
|
||||
/* We're only going to fork a child process for games with a timer. They will report back to the parent process whenever
|
||||
the timer is up.
|
||||
*/
|
||||
runGame = (game, namespace) => {
|
||||
this.logger.debug('running game ' + game.accessCode);
|
||||
const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
|
||||
gameProcess.on('message', (msg) => {
|
||||
switch (msg.command) {
|
||||
case globals.GAME_PROCESS_COMMANDS.END_GAME:
|
||||
game.status = globals.STATUS.ENDED;
|
||||
this.logger.debug('PARENT: END GAME');
|
||||
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
gameProcess.on('exit', () => {
|
||||
this.logger.debug('Game ' + game.accessCode + ' has ended. Elapsed: ' + (new Date() - game.startTime) + 'ms');
|
||||
});
|
||||
gameProcess.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
|
||||
accessCode: game.accessCode,
|
||||
logLevel: this.logger.logLevel,
|
||||
hours: game.timerParams.hours,
|
||||
minutes: game.timerParams.minutes
|
||||
});
|
||||
game.startTime = new Date().toJSON();
|
||||
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.START_TIMER);
|
||||
}
|
||||
}
|
||||
|
||||
class Singleton {
|
||||
constructor () {
|
||||
constructor (logger) {
|
||||
if (!Singleton.instance) {
|
||||
logger.log('CREATING SINGLETON ACTIVE GAME RUNNER');
|
||||
Singleton.instance = new ActiveGameRunner();
|
||||
Singleton.instance = new ActiveGameRunner(logger);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class GameManager {
|
||||
constructor (logger, environment) {
|
||||
this.logger = logger;
|
||||
this.environment = environment;
|
||||
this.activeGameRunner = new ActiveGameRunner().getInstance();
|
||||
this.activeGameRunner = new ActiveGameRunner(logger).getInstance();
|
||||
this.namespace = null;
|
||||
//this.gameSocketUtility = GameSocketUtility;
|
||||
}
|
||||
@@ -37,6 +37,9 @@ class GameManager {
|
||||
if (game) {
|
||||
game.status = globals.STATUS.IN_PROGRESS;
|
||||
namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
|
||||
if (game.hasTimer) {
|
||||
this.activeGameRunner.runGame(game, namespace);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -51,6 +54,7 @@ class GameManager {
|
||||
const newAccessCode = this.generateAccessCode();
|
||||
let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator);
|
||||
this.activeGameRunner.activeGames[newAccessCode] = new Game(
|
||||
newAccessCode,
|
||||
globals.STATUS.LOBBY,
|
||||
initializePeopleForGame(gameParams.deck, moderator),
|
||||
gameParams.deck,
|
||||
|
||||
26
server/modules/GameProcess.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const globals = require('../config/globals.js');
|
||||
const ServerTimer = require('./ServerTimer.js');
|
||||
|
||||
process.on('message', (msg) => {
|
||||
const logger = require('./Logger')(msg.logLevel);
|
||||
switch (msg.command) {
|
||||
case globals.GAME_PROCESS_COMMANDS.START_TIMER:
|
||||
logger.debug('CHILD PROCESS ' + msg.accessCode + ': START TIMER');
|
||||
runGameTimer(msg.hours, msg.minutes, logger).then(() => {
|
||||
logger.debug('Timer finished for ' + msg.accessCode);
|
||||
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
|
||||
process.exit(0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function runGameTimer (hours, minutes, logger) {
|
||||
const cycleTimer = new ServerTimer(
|
||||
hours,
|
||||
minutes,
|
||||
globals.CLOCK_TICK_INTERVAL_MILLIS,
|
||||
logger
|
||||
);
|
||||
return cycleTimer.runTimer();
|
||||
}
|
||||
@@ -2,6 +2,7 @@ const globals = require('../config/globals');
|
||||
|
||||
module.exports = function (logLevel = globals.LOG_LEVEL.INFO) {
|
||||
return {
|
||||
logLevel: logLevel,
|
||||
log (message = '') {
|
||||
const now = new Date();
|
||||
console.log('LOG ', now.toGMTString(), ': ', message);
|
||||
|
||||
69
server/modules/ServerTimer.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/* ALL TIMES ARE IN MILLIS */
|
||||
|
||||
function stepFn (expected, interval, start, totalTime, ticking, timesUpResolver, logger) {
|
||||
const now = Date.now();
|
||||
if (now - start >= totalTime) {
|
||||
clearTimeout(ticking);
|
||||
logger.debug('ELAPSED: ' + (now - start) + 'ms (~' + (Math.abs(totalTime - (now - start)) / totalTime).toFixed(3) + '% error).');
|
||||
timesUpResolver(); // this is a reference to the callback defined in the construction of the promise in runTimer()
|
||||
return;
|
||||
}
|
||||
const delta = now - expected;
|
||||
expected += interval;
|
||||
ticking = setTimeout(function () {
|
||||
stepFn(
|
||||
expected,
|
||||
interval,
|
||||
start,
|
||||
totalTime,
|
||||
ticking,
|
||||
timesUpResolver,
|
||||
logger
|
||||
);
|
||||
}, Math.max(0, interval - delta)); // take into account drift
|
||||
}
|
||||
|
||||
class ServerTimer {
|
||||
constructor (hours, minutes, tickInterval, logger) {
|
||||
this.hours = hours;
|
||||
this.minutes = minutes;
|
||||
this.tickInterval = tickInterval;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
runTimer () {
|
||||
const interval = this.tickInterval;
|
||||
const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
|
||||
const logger = this.logger;
|
||||
logger.debug('STARTING TIMER FOR ' + totalTime + 'ms');
|
||||
const start = Date.now();
|
||||
const expected = Date.now() + this.tickInterval;
|
||||
let timesUpResolver;
|
||||
const timesUpPromise = new Promise((resolve) => {
|
||||
timesUpResolver = resolve;
|
||||
});
|
||||
const ticking = setTimeout(function () {
|
||||
stepFn(
|
||||
expected,
|
||||
interval,
|
||||
start,
|
||||
totalTime,
|
||||
ticking,
|
||||
timesUpResolver,
|
||||
logger
|
||||
);
|
||||
}, this.tickInterval);
|
||||
|
||||
return timesUpPromise;
|
||||
}
|
||||
}
|
||||
|
||||
function convertFromMinutesToMilliseconds(minutes) {
|
||||
return minutes * 60 * 1000;
|
||||
}
|
||||
|
||||
function convertFromHoursToMilliseconds(hours) {
|
||||
return hours * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
module.exports = ServerTimer;
|
||||