diff --git a/.gitignore b/.gitignore
index 984aaaa..6af4543 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
.idea
node_modules/*
-./client/certs/
+client/certs/
.vscode/launch.json
package-lock.json
diff --git a/client/config/globals.js b/client/config/globals.js
index 6a16d74..9133683 100644
--- a/client/config/globals.js
+++ b/client/config/globals.js
@@ -7,7 +7,9 @@ export const globals = {
COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment',
- START_GAME: 'startGame'
+ START_GAME: 'startGame',
+ PAUSE_TIMER: 'pauseTimer',
+ RESUME_TIMER: 'resumeTimer'
},
STATUS: {
LOBBY: "lobby",
diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js
index caa2860..95af8c0 100644
--- a/client/modules/GameStateRenderer.js
+++ b/client/modules/GameStateRenderer.js
@@ -68,10 +68,10 @@ export class GameStateRenderer {
}
renderGameHeader() {
- let title = document.createElement("h1");
- title.innerText = "Game";
- document.querySelector('#game-title h1')?.remove();
- document.getElementById("game-title").appendChild(title);
+ // let title = document.createElement("h1");
+ // title.innerText = "Game";
+ // document.querySelector('#game-title h1')?.remove();
+ // document.getElementById("game-title").appendChild(title);
}
renderPlayerRole() {
@@ -99,6 +99,10 @@ export class GameStateRenderer {
document.getElementById("game-role").style.display = 'none';
});
}
+
+ renderModeratorView() {
+
+ }
}
function renderClient(client, container) {
diff --git a/client/modules/Templates.js b/client/modules/Templates.js
index bc9d995..87f44b0 100644
--- a/client/modules/Templates.js
+++ b/client/modules/Templates.js
@@ -45,5 +45,24 @@ export const templates = {
"
" +
"
Click to reveal your role
" +
"
(click again to hide)
" +
- "
"
+ "",
+ MODERATOR_GAME_VIEW:
+ "" +
+ "Moderator
" +
+ "" +
+ "" +
+ ""
}
diff --git a/client/scripts/game.js b/client/scripts/game.js
index 67bd9ad..a739c5b 100644
--- a/client/scripts/game.js
+++ b/client/scripts/game.js
@@ -27,13 +27,12 @@ function prepareGamePage(environment, socket, reconnect=false) {
} else {
toast('You are connected.', 'success', true);
console.log(gameState);
- gameState.accessCode = accessCode;
userId = gameState.client.id;
UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(gameState);
const timerWorker = new Worker('../modules/Timer.js');
setClientSocketHandlers(gameStateRenderer, socket, timerWorker);
- processGameState(gameState, userId, socket, gameStateRenderer, timerWorker, reconnect); // this socket is initialized via a script tag in the game page HTML.
+ processGameState(gameState, userId, socket, gameStateRenderer, timerWorker);
}
});
} else {
@@ -54,15 +53,28 @@ function processGameState (gameState, userId, socket, gameStateRenderer, timerWo
|| gameState.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
) {
- displayStartGamePromptForModerators(gameStateRenderer);
+ displayStartGamePromptForModerators(gameStateRenderer, socket);
}
break;
case globals.STATUS.IN_PROGRESS:
document.querySelector("#start-game-prompt")?.remove();
gameStateRenderer.gameState = gameState;
- document.getElementById("game-state-container").innerHTML = templates.GAME;
gameStateRenderer.renderGameHeader();
- gameStateRenderer.renderPlayerRole();
+ if (gameState.userType === globals.USER_TYPES.PLAYER || gameState.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
+ document.getElementById("game-state-container").innerHTML = templates.GAME;
+ gameStateRenderer.renderPlayerRole();
+ } else if (gameState.userType === globals.USER_TYPES.MODERATOR) {
+ document.getElementById("game-state-container").innerHTML = templates.MODERATOR_GAME_VIEW;
+ gameStateRenderer.renderModeratorView();
+ console.log(gameState);
+ console.log(gameState.accessCode);
+ document.getElementById("pause-button").addEventListener('click', () => {
+ socket.emit(globals.COMMANDS.PAUSE_TIMER, gameState.accessCode);
+ });
+ document.getElementById("play-button").addEventListener('click', () => {
+ socket.emit(globals.COMMANDS.RESUME_TIMER, gameState.accessCode);
+ })
+ }
break;
default:
break;
@@ -110,6 +122,18 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) {
)
});
}
+
+ if(!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) {
+ socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => {
+ console.log(timeRemaining);
+ });
+ }
+
+ if(!socket.hasListeners(globals.COMMANDS.RESUME_TIMER)) {
+ socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => {
+ console.log(timeRemaining);
+ });
+ }
}
function displayStartGamePromptForModerators(gameStateRenderer, socket) {
diff --git a/server/config/globals.js b/server/config/globals.js
index 947f9bb..30d8721 100644
--- a/server/config/globals.js
+++ b/server/config/globals.js
@@ -5,7 +5,9 @@ const globals = {
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment',
- START_GAME: 'startGame'
+ START_GAME: 'startGame',
+ PAUSE_TIMER: 'pauseTimer',
+ RESUME_TIMER: 'resumeTimer'
},
STATUS: {
LOBBY: "lobby",
@@ -39,7 +41,9 @@ const globals = {
GAME_PROCESS_COMMANDS: {
END_GAME: "endGame",
START_GAME: "startGame",
- START_TIMER: "startTimer"
+ START_TIMER: "startTimer",
+ PAUSE_TIMER: "pauseTimer",
+ RESUME_TIMER: "resumeTimer"
}
};
diff --git a/server/model/Game.js b/server/model/Game.js
index 7c71fa7..9768b1f 100644
--- a/server/model/Game.js
+++ b/server/model/Game.js
@@ -1,6 +1,6 @@
class Game {
constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) {
- this.accessCode = accessCode
+ this.accessCode = accessCode;
this.status = status;
this.moderator = moderator;
this.people = people;
@@ -8,6 +8,7 @@ class Game {
this.hasTimer = hasTimer;
this.timerParams = timerParams;
this.isFull = false;
+ this.timeRemaining = null;
}
}
diff --git a/server/modules/ActiveGameRunner.js b/server/modules/ActiveGameRunner.js
index dc5adb1..23c2d91 100644
--- a/server/modules/ActiveGameRunner.js
+++ b/server/modules/ActiveGameRunner.js
@@ -5,6 +5,7 @@ const globals = require('../config/globals');
class ActiveGameRunner {
constructor (logger) {
this.activeGames = {};
+ this.timerThreads = {};
this.logger = logger;
}
@@ -14,6 +15,7 @@ class ActiveGameRunner {
runGame = (game, namespace) => {
this.logger.debug('running game ' + game.accessCode);
const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
+ this.timerThreads[game.accessCode] = gameProcess;
gameProcess.on('message', (msg) => {
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.END_GAME:
@@ -21,11 +23,26 @@ class ActiveGameRunner {
this.logger.debug('PARENT: END GAME');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
break;
+ case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
+ game.timerParams.paused = true;
+ this.logger.trace(msg);
+ game.timeRemaining = msg.timeRemaining;
+ this.logger.debug('PARENT: PAUSE TIMER');
+ namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timeRemaining);
+ break;
+ case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
+ game.timerParams.paused = false;
+ this.logger.trace(msg);
+ game.timeRemaining = msg.timeRemaining;
+ this.logger.debug('PARENT: RESUME TIMER');
+ namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timeRemaining);
+ break;
}
});
gameProcess.on('exit', () => {
- this.logger.debug('Game ' + game.accessCode + ' has ended. Elapsed: ' + (new Date() - game.startTime) + 'ms');
+ this.logger.debug('Game ' + game.accessCode + ' has ended.');
+ delete this.timerThreads[game.accessCode];
});
gameProcess.send({
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js
index bc0b0d5..190b8e3 100644
--- a/server/modules/GameManager.js
+++ b/server/modules/GameManager.js
@@ -43,6 +43,38 @@ class GameManager {
}
}
});
+
+ socket.on(globals.CLIENT_COMMANDS.PAUSE_TIMER, (accessCode) => {
+ this.logger.trace(accessCode);
+ let game = this.activeGameRunner.activeGames[accessCode];
+ if (game) {
+ let thread = this.activeGameRunner.timerThreads[accessCode];
+ if (thread) {
+ this.logger.debug('Timer thread found for game ' + accessCode);
+ thread.send({
+ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER,
+ accessCode: game.accessCode,
+ logLevel: this.logger.logLevel
+ });
+ }
+ }
+ })
+
+ socket.on(globals.CLIENT_COMMANDS.RESUME_TIMER, (accessCode) => {
+ this.logger.trace(accessCode);
+ let game = this.activeGameRunner.activeGames[accessCode];
+ if (game) {
+ let thread = this.activeGameRunner.timerThreads[accessCode];
+ if (thread) {
+ this.logger.debug('Timer thread found for game ' + accessCode);
+ thread.send({
+ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER,
+ accessCode: game.accessCode,
+ logLevel: this.logger.logLevel
+ });
+ }
+ }
+ })
}
@@ -54,6 +86,9 @@ class GameManager {
} else {
const newAccessCode = this.generateAccessCode();
let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator);
+ if (gameParams.timerParams !== null) {
+ gameParams.timerParams.paused = false;
+ }
this.activeGameRunner.activeGames[newAccessCode] = new Game(
newAccessCode,
globals.STATUS.LOBBY,
diff --git a/server/modules/GameProcess.js b/server/modules/GameProcess.js
index c170a72..e4cb8dd 100644
--- a/server/modules/GameProcess.js
+++ b/server/modules/GameProcess.js
@@ -1,26 +1,43 @@
const globals = require('../config/globals.js');
const ServerTimer = require('./ServerTimer.js');
+let timer;
+
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(() => {
+ timer = new ServerTimer(
+ msg.hours,
+ msg.minutes,
+ globals.CLOCK_TICK_INTERVAL_MILLIS,
+ logger
+ );
+ timer.runTimer().then(() => {
logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
process.exit(0);
});
+
+ break;
+ case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
+ timer.stopTimer();
+ logger.debug('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER');
+ process.send({ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis});
+
+ break;
+
+ case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
+ timer.resumeTimer().then(() => {
+ logger.debug('Timer finished for ' + msg.accessCode);
+ process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
+ process.exit(0);
+ });
+ logger.debug('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');
+ process.send({ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis});
+
break;
}
});
-function runGameTimer (hours, minutes, logger) {
- const cycleTimer = new ServerTimer(
- hours,
- minutes,
- globals.CLOCK_TICK_INTERVAL_MILLIS,
- logger
- );
- return cycleTimer.runTimer();
-}
diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js
index 9ed1320..9680be0 100644
--- a/server/modules/GameStateCurator.js
+++ b/server/modules/GameStateCurator.js
@@ -19,6 +19,7 @@ function getGameStateBasedOnPermissions(game, person) {
switch (person.userType) {
case globals.USER_TYPES.PLAYER:
return {
+ accessCode: game.accessCode,
status: game.status,
moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.PLAYER,
@@ -30,10 +31,11 @@ function getGameStateBasedOnPermissions(game, person) {
})
.map((filteredPerson) => ({ name: filteredPerson.name, userType: filteredPerson.userType })),
timerParams: game.timerParams,
- isFull: game.isFull
+ isFull: game.isFull,
}
case globals.USER_TYPES.MODERATOR:
return {
+ accessCode: game.accessCode,
status: game.status,
moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.MODERATOR,
@@ -45,6 +47,7 @@ function getGameStateBasedOnPermissions(game, person) {
}
case globals.USER_TYPES.TEMPORARY_MODERATOR:
return {
+ accessCode: game.accessCode,
status: game.status,
moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.TEMPORARY_MODERATOR,
diff --git a/server/modules/ServerTimer.js b/server/modules/ServerTimer.js
index 8cb439b..48379c7 100644
--- a/server/modules/ServerTimer.js
+++ b/server/modules/ServerTimer.js
@@ -1,60 +1,83 @@
-/* ALL TIMES ARE IN MILLIS */
-function stepFn (expected, interval, start, totalTime, ticking, timesUpResolver, logger) {
+function stepFn (serverTimerInstance, expected) {
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()
+ serverTimerInstance.currentTimeInMillis = serverTimerInstance.totalTime - (now - serverTimerInstance.start);
+ if (now - serverTimerInstance.start >= serverTimerInstance.totalTime) {
+ clearTimeout(serverTimerInstance.ticking);
+ serverTimerInstance.logger.debug(
+ 'ELAPSED: ' + (now - serverTimerInstance.start) + 'ms (~'
+ + (Math.abs(serverTimerInstance.totalTime - (now - serverTimerInstance.start)) / serverTimerInstance.totalTime).toFixed(3) + '% error).'
+ );
+ serverTimerInstance.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 () {
+ expected += serverTimerInstance.interval;
+ serverTimerInstance.ticking = setTimeout(function () {
stepFn(
- expected,
- interval,
- start,
- totalTime,
- ticking,
- timesUpResolver,
- logger
+ serverTimerInstance,
+ expected
);
- }, Math.max(0, interval - delta)); // take into account drift
+ }, Math.max(0, serverTimerInstance.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;
+ this.currentTimeInMillis = null;
+ this.ticking = null;
+ this.timesUpPromise = null;
+ this.timesUpResolver = null;
+ this.start = null;
+ this.totalTime = null;
}
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();
+ this.totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
+ this.logger.debug('STARTING TIMER FOR ' + this.totalTime + 'ms');
+ this.start = Date.now();
const expected = Date.now() + this.tickInterval;
- let timesUpResolver;
- const timesUpPromise = new Promise((resolve) => {
- timesUpResolver = resolve;
+ this.timesUpPromise = new Promise((resolve) => {
+ this.timesUpResolver = resolve;
});
- const ticking = setTimeout(function () {
+ const instance = this;
+ this.ticking = setTimeout(function () {
stepFn(
- expected,
- interval,
- start,
- totalTime,
- ticking,
- timesUpResolver,
- logger
+ instance,
+ expected
);
}, this.tickInterval);
- return timesUpPromise;
+ return this.timesUpPromise;
+ }
+
+ stopTimer() {
+ clearTimeout(this.ticking);
+ let now = Date.now();
+ this.logger.debug(
+ 'ELAPSED (PAUSE): ' + (now - this.start) + 'ms (~'
+ + (Math.abs(this.totalTime - (now - this.start)) / this.totalTime).toFixed(3) + '% error).'
+ );
+ }
+
+ resumeTimer() {
+ this.logger.debug('RESUMING TIMER FOR ' + this.currentTimeInMillis + 'ms');
+ this.start = Date.now();
+ this.totalTime = this.currentTimeInMillis;
+ const expected = Date.now() + this.tickInterval;
+ const instance = this;
+ this.ticking = setTimeout(function () {
+ stepFn(
+ instance,
+ expected
+ );
+ }, this.tickInterval);
+
+ return this.timesUpPromise;
}
}