playing and pausing the timer

This commit is contained in:
Alec
2021-11-30 02:50:00 -05:00
parent dfe6edeb96
commit 5c869182a2
12 changed files with 209 additions and 60 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.idea .idea
node_modules/* node_modules/*
./client/certs/ client/certs/
.vscode/launch.json .vscode/launch.json
package-lock.json package-lock.json

View File

@@ -7,7 +7,9 @@ export const globals = {
COMMANDS: { COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState', FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment', GET_ENVIRONMENT: 'getEnvironment',
START_GAME: 'startGame' START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer'
}, },
STATUS: { STATUS: {
LOBBY: "lobby", LOBBY: "lobby",

View File

@@ -68,10 +68,10 @@ export class GameStateRenderer {
} }
renderGameHeader() { renderGameHeader() {
let title = document.createElement("h1"); // let title = document.createElement("h1");
title.innerText = "Game"; // title.innerText = "Game";
document.querySelector('#game-title h1')?.remove(); // document.querySelector('#game-title h1')?.remove();
document.getElementById("game-title").appendChild(title); // document.getElementById("game-title").appendChild(title);
} }
renderPlayerRole() { renderPlayerRole() {
@@ -99,6 +99,10 @@ export class GameStateRenderer {
document.getElementById("game-role").style.display = 'none'; document.getElementById("game-role").style.display = 'none';
}); });
} }
renderModeratorView() {
}
} }
function renderClient(client, container) { function renderClient(client, container) {

View File

@@ -45,5 +45,24 @@ export const templates = {
"<div id='game-role-back'>" + "<div id='game-role-back'>" +
"<h4>Click to reveal your role</h4>" + "<h4>Click to reveal your role</h4>" +
"<p>(click again to hide)</p>" + "<p>(click again to hide)</p>" +
"</div>" "</div>",
MODERATOR_GAME_VIEW:
"<div id='person-name'></div>" +
"<h2 class='user-type user-type-moderator'>Moderator</h2>" +
"<div id='game-header'>" +
"<div class='timer-container-moderator'>" +
"<label for='game-timer'>Time Remaining</label>" +
"<div id='game-timer'></div>" +
"</div>" +
"<div id='play-pause'>" +
"<button id='pause-button'>Pause</button>" +
"<button id='play-button'>Play</button>" +
"</div>" +
"<div>" +
"<label for='alive-count'>Players Left</label>" +
"<div id='alive-count'></div>" +
"</div>" +
"</div>" +
"<div id='player-list-moderator'></div>" +
"<button id='end-game-button'>End Game</button>"
} }

View File

@@ -27,13 +27,12 @@ function prepareGamePage(environment, socket, reconnect=false) {
} else { } else {
toast('You are connected.', 'success', true); toast('You are connected.', 'success', true);
console.log(gameState); console.log(gameState);
gameState.accessCode = accessCode;
userId = gameState.client.id; userId = gameState.client.id;
UserUtility.setAnonymousUserId(userId, environment); UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(gameState); let gameStateRenderer = new GameStateRenderer(gameState);
const timerWorker = new Worker('../modules/Timer.js'); const timerWorker = new Worker('../modules/Timer.js');
setClientSocketHandlers(gameStateRenderer, socket, timerWorker); 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 { } else {
@@ -54,15 +53,28 @@ function processGameState (gameState, userId, socket, gameStateRenderer, timerWo
|| gameState.userType === globals.USER_TYPES.TEMPORARY_MODERATOR || gameState.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
) )
) { ) {
displayStartGamePromptForModerators(gameStateRenderer); displayStartGamePromptForModerators(gameStateRenderer, socket);
} }
break; break;
case globals.STATUS.IN_PROGRESS: case globals.STATUS.IN_PROGRESS:
document.querySelector("#start-game-prompt")?.remove(); document.querySelector("#start-game-prompt")?.remove();
gameStateRenderer.gameState = gameState; gameStateRenderer.gameState = gameState;
document.getElementById("game-state-container").innerHTML = templates.GAME;
gameStateRenderer.renderGameHeader(); 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; break;
default: default:
break; 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) { function displayStartGamePromptForModerators(gameStateRenderer, socket) {

View File

@@ -5,7 +5,9 @@ const globals = {
CLIENT_COMMANDS: { CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState', FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment', GET_ENVIRONMENT: 'getEnvironment',
START_GAME: 'startGame' START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer'
}, },
STATUS: { STATUS: {
LOBBY: "lobby", LOBBY: "lobby",
@@ -39,7 +41,9 @@ const globals = {
GAME_PROCESS_COMMANDS: { GAME_PROCESS_COMMANDS: {
END_GAME: "endGame", END_GAME: "endGame",
START_GAME: "startGame", START_GAME: "startGame",
START_TIMER: "startTimer" START_TIMER: "startTimer",
PAUSE_TIMER: "pauseTimer",
RESUME_TIMER: "resumeTimer"
} }
}; };

View File

@@ -1,6 +1,6 @@
class Game { class Game {
constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) { constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) {
this.accessCode = accessCode this.accessCode = accessCode;
this.status = status; this.status = status;
this.moderator = moderator; this.moderator = moderator;
this.people = people; this.people = people;
@@ -8,6 +8,7 @@ class Game {
this.hasTimer = hasTimer; this.hasTimer = hasTimer;
this.timerParams = timerParams; this.timerParams = timerParams;
this.isFull = false; this.isFull = false;
this.timeRemaining = null;
} }
} }

View File

@@ -5,6 +5,7 @@ const globals = require('../config/globals');
class ActiveGameRunner { class ActiveGameRunner {
constructor (logger) { constructor (logger) {
this.activeGames = {}; this.activeGames = {};
this.timerThreads = {};
this.logger = logger; this.logger = logger;
} }
@@ -14,6 +15,7 @@ class ActiveGameRunner {
runGame = (game, namespace) => { runGame = (game, namespace) => {
this.logger.debug('running game ' + game.accessCode); this.logger.debug('running game ' + game.accessCode);
const gameProcess = fork(path.join(__dirname, '/GameProcess.js')); const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
this.timerThreads[game.accessCode] = gameProcess;
gameProcess.on('message', (msg) => { gameProcess.on('message', (msg) => {
switch (msg.command) { switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.END_GAME: case globals.GAME_PROCESS_COMMANDS.END_GAME:
@@ -21,11 +23,26 @@ class ActiveGameRunner {
this.logger.debug('PARENT: END GAME'); this.logger.debug('PARENT: END GAME');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode); namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
break; 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', () => { 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({ gameProcess.send({
command: globals.GAME_PROCESS_COMMANDS.START_TIMER, command: globals.GAME_PROCESS_COMMANDS.START_TIMER,

View File

@@ -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 { } else {
const newAccessCode = this.generateAccessCode(); const newAccessCode = this.generateAccessCode();
let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator); let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator);
if (gameParams.timerParams !== null) {
gameParams.timerParams.paused = false;
}
this.activeGameRunner.activeGames[newAccessCode] = new Game( this.activeGameRunner.activeGames[newAccessCode] = new Game(
newAccessCode, newAccessCode,
globals.STATUS.LOBBY, globals.STATUS.LOBBY,

View File

@@ -1,26 +1,43 @@
const globals = require('../config/globals.js'); const globals = require('../config/globals.js');
const ServerTimer = require('./ServerTimer.js'); const ServerTimer = require('./ServerTimer.js');
let timer;
process.on('message', (msg) => { process.on('message', (msg) => {
const logger = require('./Logger')(msg.logLevel); const logger = require('./Logger')(msg.logLevel);
switch (msg.command) { switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.START_TIMER: case globals.GAME_PROCESS_COMMANDS.START_TIMER:
logger.debug('CHILD PROCESS ' + msg.accessCode + ': 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); logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME }); process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
process.exit(0); 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; break;
} }
}); });
function runGameTimer (hours, minutes, logger) {
const cycleTimer = new ServerTimer(
hours,
minutes,
globals.CLOCK_TICK_INTERVAL_MILLIS,
logger
);
return cycleTimer.runTimer();
}

View File

@@ -19,6 +19,7 @@ function getGameStateBasedOnPermissions(game, person) {
switch (person.userType) { switch (person.userType) {
case globals.USER_TYPES.PLAYER: case globals.USER_TYPES.PLAYER:
return { return {
accessCode: game.accessCode,
status: game.status, status: game.status,
moderator: mapPerson(game.moderator), moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.PLAYER, userType: globals.USER_TYPES.PLAYER,
@@ -30,10 +31,11 @@ function getGameStateBasedOnPermissions(game, person) {
}) })
.map((filteredPerson) => ({ name: filteredPerson.name, userType: filteredPerson.userType })), .map((filteredPerson) => ({ name: filteredPerson.name, userType: filteredPerson.userType })),
timerParams: game.timerParams, timerParams: game.timerParams,
isFull: game.isFull isFull: game.isFull,
} }
case globals.USER_TYPES.MODERATOR: case globals.USER_TYPES.MODERATOR:
return { return {
accessCode: game.accessCode,
status: game.status, status: game.status,
moderator: mapPerson(game.moderator), moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.MODERATOR, userType: globals.USER_TYPES.MODERATOR,
@@ -45,6 +47,7 @@ function getGameStateBasedOnPermissions(game, person) {
} }
case globals.USER_TYPES.TEMPORARY_MODERATOR: case globals.USER_TYPES.TEMPORARY_MODERATOR:
return { return {
accessCode: game.accessCode,
status: game.status, status: game.status,
moderator: mapPerson(game.moderator), moderator: mapPerson(game.moderator),
userType: globals.USER_TYPES.TEMPORARY_MODERATOR, userType: globals.USER_TYPES.TEMPORARY_MODERATOR,

View File

@@ -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(); const now = Date.now();
if (now - start >= totalTime) { serverTimerInstance.currentTimeInMillis = serverTimerInstance.totalTime - (now - serverTimerInstance.start);
clearTimeout(ticking); if (now - serverTimerInstance.start >= serverTimerInstance.totalTime) {
logger.debug('ELAPSED: ' + (now - start) + 'ms (~' + (Math.abs(totalTime - (now - start)) / totalTime).toFixed(3) + '% error).'); clearTimeout(serverTimerInstance.ticking);
timesUpResolver(); // this is a reference to the callback defined in the construction of the promise in runTimer() 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; return;
} }
const delta = now - expected; const delta = now - expected;
expected += interval; expected += serverTimerInstance.interval;
ticking = setTimeout(function () { serverTimerInstance.ticking = setTimeout(function () {
stepFn( stepFn(
expected, serverTimerInstance,
interval, expected
start,
totalTime,
ticking,
timesUpResolver,
logger
); );
}, Math.max(0, interval - delta)); // take into account drift }, Math.max(0, serverTimerInstance.interval - delta)); // take into account drift
} }
class ServerTimer { class ServerTimer {
constructor (hours, minutes, tickInterval, logger) { constructor (hours, minutes, tickInterval, logger) {
this.hours = hours; this.hours = hours;
this.minutes = minutes; this.minutes = minutes;
this.tickInterval = tickInterval; this.tickInterval = tickInterval;
this.logger = logger; this.logger = logger;
this.currentTimeInMillis = null;
this.ticking = null;
this.timesUpPromise = null;
this.timesUpResolver = null;
this.start = null;
this.totalTime = null;
} }
runTimer () { runTimer () {
const interval = this.tickInterval; this.totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes); this.logger.debug('STARTING TIMER FOR ' + this.totalTime + 'ms');
const logger = this.logger; this.start = Date.now();
logger.debug('STARTING TIMER FOR ' + totalTime + 'ms');
const start = Date.now();
const expected = Date.now() + this.tickInterval; const expected = Date.now() + this.tickInterval;
let timesUpResolver; this.timesUpPromise = new Promise((resolve) => {
const timesUpPromise = new Promise((resolve) => { this.timesUpResolver = resolve;
timesUpResolver = resolve;
}); });
const ticking = setTimeout(function () { const instance = this;
this.ticking = setTimeout(function () {
stepFn( stepFn(
expected, instance,
interval, expected
start,
totalTime,
ticking,
timesUpResolver,
logger
); );
}, this.tickInterval); }, 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;
} }
} }