From 5c869182a2080e8dafef86d2f2e4aaa7ef250cea Mon Sep 17 00:00:00 2001 From: Alec Date: Tue, 30 Nov 2021 02:50:00 -0500 Subject: [PATCH] playing and pausing the timer --- .gitignore | 2 +- client/config/globals.js | 4 +- client/modules/GameStateRenderer.js | 12 ++-- client/modules/Templates.js | 21 ++++++- client/scripts/game.js | 34 +++++++++-- server/config/globals.js | 8 ++- server/model/Game.js | 3 +- server/modules/ActiveGameRunner.js | 19 +++++- server/modules/GameManager.js | 35 ++++++++++++ server/modules/GameProcess.js | 37 ++++++++---- server/modules/GameStateCurator.js | 5 +- server/modules/ServerTimer.js | 89 ++++++++++++++++++----------- 12 files changed, 209 insertions(+), 60 deletions(-) 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; } }