From 8fbf77e0c80edcbe1a79f603ee8c2335bd5ce7c6 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Tue, 10 May 2022 15:03:48 -0400 Subject: [PATCH] restart functionality --- client/src/config/globals.js | 2 +- client/src/modules/GameCreationStepManager.js | 32 ++-- client/src/modules/GameStateRenderer.js | 64 +++++--- client/src/modules/GameTimerManager.js | 1 + client/src/modules/HTMLFragments.js | 26 +--- client/src/scripts/game.js | 7 +- client/src/scripts/home.js | 71 +++++---- client/src/styles/GLOBAL.css | 2 +- client/src/styles/create.css | 4 +- client/src/styles/game.css | 11 +- client/src/views/game.html | 3 +- server/config/globals.js | 2 +- server/model/Game.js | 13 +- server/modules/GameManager.js | 87 +++++++---- spec/unit/server/modules/GameManager_Spec.js | 143 ++++++++++++++++++ 15 files changed, 336 insertions(+), 132 deletions(-) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index d22357f..72d5bb6 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -1,6 +1,6 @@ export const globals = { USER_SIGNATURE_LENGTH: 25, - CLOCK_TICK_INTERVAL_MILLIS: 10, + CLOCK_TICK_INTERVAL_MILLIS: 100, MAX_CUSTOM_ROLE_NAME_LENGTH: 30, MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 500, TOAST_DURATION_DEFAULT: 6, diff --git a/client/src/modules/GameCreationStepManager.js b/client/src/modules/GameCreationStepManager.js index 1d12537..1178ae0 100644 --- a/client/src/modules/GameCreationStepManager.js +++ b/client/src/modules/GameCreationStepManager.js @@ -107,13 +107,23 @@ export class GameCreationStepManager { 5: { title: 'Review and submit:', backHandler: this.defaultBackHandler, - forwardHandler: (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams) => { + forwardHandler: () => { + const button = document.getElementById('create-game'); + button.removeEventListener('click', this.steps['5'].forwardHandler); + button.classList.add('submitted'); + button.innerText = 'Creating'; XHRUtility.xhr( '/api/games/create', 'POST', null, JSON.stringify( - new Game(deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams) + new Game( + this.currentGame.deck.filter((card) => card.quantity > 0), + this.currentGame.hasTimer, + this.currentGame.hasDedicatedModerator, + this.currentGame.moderatorName, + this.currentGame.timerParams + ) ) ) .then((res) => { @@ -128,9 +138,10 @@ export class GameCreationStepManager { } }).catch((e) => { const button = document.getElementById('create-game'); - button.innerText = 'Create Game'; + button.innerText = 'Create'; button.classList.remove('submitted'); - button.addEventListener('click', this.steps['4'].forwardHandler); + button.addEventListener('click', this.steps['5'].forwardHandler); + toast(e.content, 'error', true, true, 'medium'); if (e.status === 429) { toast('You\'ve sent this request too many times.', 'error', true, true, 'medium'); } @@ -449,18 +460,7 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu createButton.innerText = 'Create'; createButton.setAttribute('id', 'create-game'); createButton.classList.add('app-button'); - createButton.addEventListener('click', () => { - createButton.removeEventListener('click', forwardHandler); - createButton.classList.add('submitted'); - createButton.innerText = 'Creating...'; - forwardHandler( - builtGame.deck.filter((card) => card.quantity > 0), - builtGame.hasTimer, - builtGame.hasDedicatedModerator, - builtGame.moderatorName, - builtGame.timerParams - ); - }); + createButton.addEventListener('click', forwardHandler); document.getElementById('tracker-container').appendChild(createButton); } } diff --git a/client/src/modules/GameStateRenderer.js b/client/src/modules/GameStateRenderer.js index 55f8916..1ae1bdd 100644 --- a/client/src/modules/GameStateRenderer.js +++ b/client/src/modules/GameStateRenderer.js @@ -2,8 +2,8 @@ import { globals } from '../config/globals.js'; import { toast } from './Toast.js'; import { HTMLFragments } from './HTMLFragments.js'; import { ModalManager } from './ModalManager.js'; -import {XHRUtility} from "./XHRUtility"; -import {UserUtility} from "./UserUtility"; +import { XHRUtility } from './XHRUtility.js'; +import { UserUtility } from './UserUtility.js'; export class GameStateRenderer { constructor (stateBucket, socket) { @@ -12,12 +12,40 @@ export class GameStateRenderer { this.killPlayerHandlers = {}; this.revealRoleHandlers = {}; this.transferModHandlers = {}; - this.startGameHandler = (e) => { + this.startGameHandler = (e) => { // TODO: prevent multiple emissions of this event (recommend converting to XHR) e.preventDefault(); if (confirm('Start the game and deal roles?')) { socket.emit(globals.COMMANDS.START_GAME, this.stateBucket.currentGameState.accessCode); } }; + this.restartGameHandler = (e) => { + e.preventDefault(); + const button = document.getElementById('restart-game'); + button.removeEventListener('click', this.restartGameHandler); + button.classList.add('submitted'); + button.innerText = 'Restarting...'; + XHRUtility.xhr( + '/api/games/' + this.stateBucket.currentGameState.accessCode + '/restart', + 'PATCH', + null, + JSON.stringify({ + playerName: this.stateBucket.currentGameState.client.name, + accessCode: this.stateBucket.currentGameState.accessCode, + sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), + localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION) + }) + ) + .then((res) => { + toast('Game restarted!', 'success', true, true, 'medium'); + }) + .catch((res) => { + const button = document.getElementById('restart-game'); + button.innerText = 'Run it back šŸ”„'; + button.classList.remove('submitted'); + button.addEventListener('click', this.restartGameHandler); + toast(res.content, 'error', true, true, 'medium'); + }); + }; } renderLobbyPlayers () { @@ -263,27 +291,11 @@ export class GameStateRenderer { gameState.client.userType === globals.USER_TYPES.MODERATOR || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR ) { - let div = document.createElement('div'); - div.innerHTML = HTMLFragments.RESTART_GAME_BUTTON; - div.querySelector('#restart-game').addEventListener('click', () => { - XHRUtility.xhr( - '/api/games/' + gameState.accessCode + '/restart', - 'PATCH', - null, - JSON.stringify({ - playerName: gameState.client.name, - accessCode: gameState.accessCode, - sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), - localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION) - }) - ) - .then((res) => { - - }).catch((res) => { - - }); - }) - document.getElementById('end-of-game-buttons').appendChild(div); + const restartGameContainer = document.createElement('div'); + restartGameContainer.innerHTML = HTMLFragments.RESTART_GAME_BUTTON; + const button = restartGameContainer.querySelector('#restart-game'); + button.addEventListener('click', this.restartGameHandler); + document.getElementById('end-of-game-buttons').appendChild(restartGameContainer); } this.renderPlayersWithNoRoleInformationUnlessRevealed(); } @@ -301,6 +313,10 @@ function renderPotentialMods (gameState, group, transferModHandlers, socket) { transferModHandlers[member.id] = (e) => { if (e.type === 'click' || e.code === 'Enter') { if (confirm('Transfer moderator powers to ' + member.name + '?')) { + const transferPrompt = document.getElementById('transfer-mod-prompt'); + if (transferPrompt !== null) { + transferPrompt.innerHTML = ''; + } socket.emit(globals.COMMANDS.TRANSFER_MODERATOR, gameState.accessCode, member.id); } } diff --git a/client/src/modules/GameTimerManager.js b/client/src/modules/GameTimerManager.js index 7df7c04..404cc08 100644 --- a/client/src/modules/GameTimerManager.js +++ b/client/src/modules/GameTimerManager.js @@ -149,6 +149,7 @@ export class GameTimerManager { const pauseBtn = document.createElement('img'); pauseBtn.setAttribute('src', '../images/pause-button.svg'); pauseBtn.addEventListener('click', this.pauseListener); + document.querySelector('#play-pause-placeholder')?.remove(); document.getElementById('play-pause').appendChild(pauseBtn); } } diff --git a/client/src/modules/HTMLFragments.js b/client/src/modules/HTMLFragments.js index bcbda1b..0bb0b8e 100644 --- a/client/src/modules/HTMLFragments.js +++ b/client/src/modules/HTMLFragments.js @@ -80,7 +80,7 @@ export const HTMLFragments = {
`, - MODERATOR_GAME_VIEW: + TRANSFER_MOD_MODAL: ` -
-
+
`, + MODERATOR_GAME_VIEW: + `
+
@@ -118,19 +119,8 @@ export const HTMLFragments = {
`, TEMP_MOD_GAME_VIEW: - ` - -
-
+ `
+
@@ -219,7 +209,7 @@ export const HTMLFragments = {
`, END_OF_GAME_VIEW: `
-

The moderator has ended the game. Roles are revealed.

+

🏁 The moderator has ended the game. Roles are revealed.

diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 9aaee03..22b7f7a 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -75,7 +75,7 @@ function processGameState ( gameTimerManager, timerWorker, refreshPrompt = true, - animateContainer= false + animateContainer = false ) { const containerAnimation = document.getElementById('game-state-container').animate( [ @@ -123,10 +123,12 @@ function processGameState ( gameStateRenderer.renderPlayerView(true); break; case globals.USER_TYPES.MODERATOR: + document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; document.getElementById('game-state-container').innerHTML = HTMLFragments.MODERATOR_GAME_VIEW; gameStateRenderer.renderModeratorView(); break; case globals.USER_TYPES.TEMPORARY_MODERATOR: + document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; document.getElementById('game-state-container').innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW; gameStateRenderer.renderTempModView(); break; @@ -141,6 +143,7 @@ function processGameState ( socket.emit(globals.COMMANDS.GET_TIME_REMAINING, currentGameState.accessCode); } else { document.querySelector('#game-timer')?.remove(); + document.querySelector('#timer-container-moderator')?.remove(); document.querySelector('label[for="game-timer"]')?.remove(); } break; @@ -384,7 +387,7 @@ function activateRoleInfoButton (deck) { }); document.getElementById('role-info-button').addEventListener('click', (e) => { e.preventDefault(); - document.getElementById('prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL; + document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL; const modalContent = document.getElementById('game-role-info-container'); for (const card of deck) { const roleDiv = document.createElement('div'); diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index a78f0a2..17a9a94 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -4,45 +4,50 @@ import { injectNavbar } from '../modules/Navbar.js'; const home = () => { injectNavbar(); - document.getElementById('join-form').onsubmit = (e) => { - e.preventDefault(); - const userCode = document.getElementById('room-code').value; - if (roomCodeIsValid(userCode)) { - attemptToJoinGame(userCode); - } else { - toast('Invalid code. Codes are 4 numbers or letters.', 'error', true, true); - } - }; + document.getElementById('join-form').addEventListener('submit', attemptToJoinGame); }; function roomCodeIsValid (code) { return typeof code === 'string' && /^[A-Z0-9]{4}$/.test(code.toUpperCase().trim()); } -function attemptToJoinGame (code) { - XHRUtility.xhr( - '/api/games/' + code.toUpperCase().trim() + '/availability', - 'GET', - null, - null - ) - .then((res) => { - if (res.status === 200) { - const json = JSON.parse(res.content); - window.location = window.location.protocol + '//' + window.location.host + - '/join/' + encodeURIComponent(json.accessCode) + - '?playerCount=' + encodeURIComponent(json.playerCount) + - '&timer=' + encodeURIComponent(getTimeString(json.timerParams)); - } - }).catch((res) => { - 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); - } - }); +function attemptToJoinGame (event) { + event.preventDefault(); + const userCode = document.getElementById('room-code').value; + if (roomCodeIsValid(userCode)) { + const form = document.getElementById('join-form'); + form.removeEventListener('submit', attemptToJoinGame); + form.classList.add('submitted'); + document.getElementById('join-button')?.setAttribute('value', 'Joining...'); + XHRUtility.xhr( + '/api/games/' + userCode.toUpperCase().trim() + '/availability', + 'GET', + null, + null + ) + .then((res) => { + if (res.status === 200) { + const json = JSON.parse(res.content); + window.location = window.location.protocol + '//' + window.location.host + + '/join/' + encodeURIComponent(json.accessCode) + + '?playerCount=' + encodeURIComponent(json.playerCount) + + '&timer=' + encodeURIComponent(getTimeString(json.timerParams)); + } + }).catch((res) => { + form.addEventListener('submit', attemptToJoinGame); + form.classList.remove('submitted'); + document.getElementById('join-button')?.setAttribute('value', 'Join'); + 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); + } + }); + } else { + toast('Invalid code. Codes are 4 numbers or letters.', 'error', true, true); + } } function getTimeString (timerParams) { diff --git a/client/src/styles/GLOBAL.css b/client/src/styles/GLOBAL.css index 2ff1451..f1cf65e 100644 --- a/client/src/styles/GLOBAL.css +++ b/client/src/styles/GLOBAL.css @@ -607,7 +607,7 @@ input { left: 0; width: 100%; height: calc(100% + 100px); - background-color: rgba(0, 0, 0, 0.75); + background-color: rgba(0, 0, 0, 0.5); z-index: 50; } diff --git a/client/src/styles/create.css b/client/src/styles/create.css index 4bd0ac6..846324e 100644 --- a/client/src/styles/create.css +++ b/client/src/styles/create.css @@ -107,7 +107,7 @@ #deck-status-container { width: 20em; max-width: 95%; - height: 13em; + height: 10em; overflow-y: auto; position: relative; } @@ -356,7 +356,7 @@ input[type="number"] { #deck-select { margin: 0.5em 1em 1.5em 0; overflow-y: auto; - height: 15em; + height: 12em; } .deck-select-role { diff --git a/client/src/styles/game.css b/client/src/styles/game.css index cdb29e9..5644446 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -510,12 +510,19 @@ label[for='moderator'] { align-items: flex-start; } -.timer-container-moderator { +#game-header button { + min-width: 10em; +} + +#timer-container-moderator { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; margin-bottom: 1em; + padding: 0.5em; + border-radius: 3px; + background-color: #333243; } .game-player { @@ -533,7 +540,7 @@ label[for='moderator'] { .game-player-name { position: relative; - width: 10em; + min-width: 6em; overflow: hidden; white-space: nowrap; font-weight: bold; diff --git a/client/src/views/game.html b/client/src/views/game.html index 8db4db3..c0b68e0 100644 --- a/client/src/views/game.html +++ b/client/src/views/game.html @@ -19,7 +19,8 @@ -
+
+
diff --git a/server/config/globals.js b/server/config/globals.js index 0f5a617..17c7c81 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -2,7 +2,7 @@ const globals = { ACCESS_CODE_CHAR_POOL: 'BCDFGHJKLMNPQRSTVWXYZ23456789', ACCESS_CODE_LENGTH: 4, ACCESS_CODE_GENERATION_ATTEMPTS: 50, - CLOCK_TICK_INTERVAL_MILLIS: 10, + CLOCK_TICK_INTERVAL_MILLIS: 100, STALE_GAME_HOURS: 12, CLIENT_COMMANDS: { FETCH_GAME_STATE: 'fetchGameState', diff --git a/server/model/Game.js b/server/model/Game.js index d6a7dcf..3756190 100644 --- a/server/model/Game.js +++ b/server/model/Game.js @@ -1,5 +1,15 @@ class Game { - constructor (accessCode, status, people, deck, hasTimer, moderator, hasDedicatedModerator, timerParams = null) { + constructor ( + accessCode, + status, + people, + deck, + hasTimer, + moderator, + hasDedicatedModerator, + originalModeratorId, + timerParams = null + ) { this.accessCode = accessCode; this.status = status; this.moderator = moderator; @@ -7,6 +17,7 @@ class Game { this.deck = deck; this.hasTimer = hasTimer; this.hasDedicatedModerator = hasDedicatedModerator; + this.originalModeratorId = originalModeratorId; this.timerParams = timerParams; this.isFull = false; this.timeRemaining = null; diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index ce3bcbd..22d4538 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -183,11 +183,12 @@ class GameManager { this.activeGameRunner.activeGames[newAccessCode] = new Game( newAccessCode, globals.STATUS.LOBBY, - initializePeopleForGame(gameParams.deck, moderator), + initializePeopleForGame(gameParams.deck, moderator, this.shuffle), gameParams.deck, gameParams.hasTimer, moderator, gameParams.hasDedicatedModerator, + moderator.id, gameParams.timerParams ); this.activeGameRunner.activeGames[newAccessCode].createTime = new Date().toJSON(); @@ -310,39 +311,65 @@ class GameManager { }; restartGame = async (game, namespace) => { + // kill any outstanding timer threads if (this.activeGameRunner.timerThreads[game.accessCode]) { - this.logger.info('KILLING STALE TIMER PROCESS FOR ' + accessCode); + this.logger.info('KILLING STALE TIMER PROCESS FOR ' + game.accessCode); this.activeGameRunner.timerThreads[game.accessCode].kill(); delete this.activeGameRunner.timerThreads[game.accessCode]; } - game.status = globals.STATUS.IN_PROGRESS; - let cards = []; + + // re-shuffle the deck + const cards = []; for (const card of game.deck) { for (let i = 0; i < card.quantity; i ++) { cards.push(card); } } - shuffle(cards); + + this.shuffle(cards); + + // make sure no players are marked as out or revealed, and give them new cards. for (let i = 0; i < game.people.length; i ++) { if (game.people[i].out) { game.people[i].out = false; } + if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) { + game.people[i].userType = globals.USER_TYPES.PLAYER; + } game.people[i].revealed = false; game.people[i].gameRole = cards[i].role; game.people[i].gameRoleDescription = cards[i].description; game.people[i].alignment = cards[i].team; } + + /* If the game was originally set up with a TEMP mod and the game has gone far enough to establish + a DEDICATED mod, make the current mod a TEMP mod for the restart. */ + if (!game.hasDedicatedModerator && game.moderator.userType === globals.USER_TYPES.MODERATOR) { + game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR; + } + + /* If the game was originally set up with a DEDICATED moderator and the current mod is DIFFERENT from that mod + (i.e. they transferred their powers at some point), check if the current mod was once a player (i.e. they have + a game role). If they were once a player, make them a temp mod for the restart. Otherwise, they were a + spectator, and we want to leave them as a dedicated moderator. + */ + if (game.hasDedicatedModerator && game.moderator.id !== game.originalModeratorId) { + if (game.moderator.gameRole) { + game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR; + } else { + game.moderator.userType = globals.USER_TYPES.MODERATOR; + } + } + + // start the new game + game.status = globals.STATUS.IN_PROGRESS; if (game.hasTimer) { game.timerParams.paused = true; this.activeGameRunner.runGame(game, namespace); } - /* If the game was originally set up with a temporary moderator and the game has gone far enough to establish - a dedicated moderator, make the current moderator a temp mod for the restarting of the same game. */ - if (!game.hasDedicatedModerator && game.moderator.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) { - game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR; - } + namespace.in(game.accessCode).emit(globals.CLIENT_COMMANDS.START_GAME); - } + }; handleRequestForGameState = async (namespace, logger, gameRunner, accessCode, personCookie, ackFn, clientSocket) => { const game = gameRunner.activeGames[accessCode]; @@ -391,6 +418,23 @@ class GameManager { } }); } + + /* + -- To shuffle an array a of n elements (indices 0..n-1): + for i from nāˆ’1 downto 1 do + j ← random integer such that 0 ≤ j ≤ i + exchange a[j] and a[i] + */ + shuffle = (array) => { + for (let i = array.length - 1; i > 0; i --) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = array[j]; + array[j] = array[i]; + array[i] = temp; + } + + return array; + }; } function getRandomInt (max) { @@ -404,9 +448,9 @@ function initializeModerator (name, hasDedicatedModerator) { return new Person(createRandomId(), createRandomId(), name, userType); } -function initializePeopleForGame (uniqueCards, moderator) { +function initializePeopleForGame (uniqueCards, moderator, shuffle) { const people = []; - let cards = []; + const cards = []; let numberOfRoles = 0; for (const card of uniqueCards) { for (let i = 0; i < card.quantity; i ++) { @@ -446,23 +490,6 @@ function initializePeopleForGame (uniqueCards, moderator) { return people; } -/* --- To shuffle an array a of n elements (indices 0..n-1): -for i from nāˆ’1 downto 1 do - j ← random integer such that 0 ≤ j ≤ i - exchange a[j] and a[i] - */ -function shuffle(array) { - for (let i = array.length - 1; i > 0; i --) { - const j = Math.floor(Math.random() * (i + 1)); - const temp = array[j]; - array[j] = array[i]; - array[i] = temp; - } - - return array; -} - function createRandomId () { let id = ''; for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i ++) { diff --git a/spec/unit/server/modules/GameManager_Spec.js b/spec/unit/server/modules/GameManager_Spec.js index aaac3ee..2d172e8 100644 --- a/spec/unit/server/modules/GameManager_Spec.js +++ b/spec/unit/server/modules/GameManager_Spec.js @@ -2,6 +2,7 @@ const Game = require('../../../../server/model/Game'); const globals = require('../../../../server/config/globals'); const USER_TYPES = globals.USER_TYPES; +const STATUS = globals.STATUS; const Person = require('../../../../server/model/Person'); const GameManager = require('../../../../server/modules/GameManager.js'); const GameStateCurator = require('../../../../server/modules/GameStateCurator'); @@ -280,4 +281,146 @@ describe('GameManager', () => { expect(accessCode).toEqual('BBBB'); }); }); + + describe('#restartGame', () => { + let person1, + person2, + person3, + shuffleSpy, + game, + moderator; + + beforeEach(() => { + person1 = new Person('1', '123', 'Placeholder1', USER_TYPES.KILLED_PLAYER); + person2 = new Person('2', '456', 'Placeholder2', USER_TYPES.PLAYER); + person3 = new Person('3', '789', 'Placeholder3', USER_TYPES.PLAYER); + moderator = new Person('4', '000', 'Jack', USER_TYPES.MODERATOR); + person1.out = true; + person2.revealed = true; + moderator.assigned = true; + shuffleSpy = spyOn(gameManager, 'shuffle').and.stub(); + game = new Game( + 'test', + STATUS.ENDED, + [person1, person2, person3], + [ + { role: 'Villager', description: 'test', team: 'good', quantity: 1 }, + { role: 'Seer', description: 'test', team: 'good', quantity: 1 }, + { role: 'Werewolf', description: 'test', team: 'evil', quantity: 1 } + ], + false, + moderator, + true, + '4', + null + ); + }); + + it('should reset all relevant game parameters', async () => { + const emitSpy = spyOn(namespace.in(), 'emit'); + + await gameManager.restartGame(game, namespace); + + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.moderator.id).toEqual('4'); + expect(game.moderator.userType).toEqual(USER_TYPES.MODERATOR); + expect(person1.out).toEqual(false); + expect(person2.revealed).toEqual(false); + for (const person of game.people) { + expect(person.gameRole).toBeDefined(); + } + expect(shuffleSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME); + }); + + it('should reset all relevant game parameters, including when the game has a timer', async () => { + game.timerParams = { hours: 2, minutes: 2, paused: false }; + game.hasTimer = true; + gameManager.activeGameRunner.timerThreads = { test: { kill: () => {} } }; + + const threadKillSpy = spyOn(gameManager.activeGameRunner.timerThreads.test, 'kill'); + const runGameSpy = spyOn(gameManager.activeGameRunner, 'runGame').and.stub(); + const emitSpy = spyOn(namespace.in(), 'emit'); + + await gameManager.restartGame(game, namespace); + + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.timerParams.paused).toBeTrue(); + expect(game.moderator.id).toEqual('4'); + expect(game.moderator.userType).toEqual(USER_TYPES.MODERATOR); + expect(person1.out).toEqual(false); + expect(person2.revealed).toEqual(false); + for (const person of game.people) { + expect(person.gameRole).toBeDefined(); + } + expect(threadKillSpy).toHaveBeenCalled(); + expect(runGameSpy).toHaveBeenCalled(); + expect(Object.keys(gameManager.activeGameRunner.timerThreads).length).toEqual(0); + expect(shuffleSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME); + }); + + it('should reset all relevant game parameters and preserve temporary moderator', async () => { + const emitSpy = spyOn(namespace.in(), 'emit'); + game.moderator = game.people[0]; + game.moderator.userType = USER_TYPES.TEMPORARY_MODERATOR; + game.hasDedicatedModerator = false; + + await gameManager.restartGame(game, namespace); + + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.moderator.id).toEqual('1'); + expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR); + expect(game.moderator.gameRole).toBeDefined(); + expect(person1.out).toEqual(false); + expect(person2.revealed).toEqual(false); + for (const person of game.people) { + expect(person.gameRole).toBeDefined(); + } + expect(shuffleSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME); + }); + + it('should reset all relevant game parameters and restore a temporary moderator from a dedicated moderator', async () => { + const emitSpy = spyOn(namespace.in(), 'emit'); + game.moderator = game.people[0]; + game.moderator.userType = USER_TYPES.MODERATOR; + game.hasDedicatedModerator = false; + + await gameManager.restartGame(game, namespace); + + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.moderator.id).toEqual('1'); + expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR); + expect(game.moderator.gameRole).toBeDefined(); + expect(person1.out).toEqual(false); + expect(person2.revealed).toEqual(false); + for (const person of game.people) { + expect(person.gameRole).toBeDefined(); + } + expect(shuffleSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME); + }); + + it('should reset all relevant game parameters and create a temporary mod if a dedicated mod transferred to a killed player', async () => { + const emitSpy = spyOn(namespace.in(), 'emit'); + game.moderator = game.people[0]; + game.moderator.userType = USER_TYPES.MODERATOR; + game.hasDedicatedModerator = true; + + await gameManager.restartGame(game, namespace); + + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.moderator.id).toEqual('1'); + expect(game.moderator.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR); + expect(game.moderator.gameRole).toBeDefined(); + expect(person1.out).toEqual(false); + expect(person2.revealed).toEqual(false); + for (const person of game.people) { + expect(person.gameRole).toBeDefined(); + } + expect(shuffleSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(globals.CLIENT_COMMANDS.START_GAME); + }); + }); });