restart functionality

This commit is contained in:
AlecM33
2022-05-10 15:03:48 -04:00
parent a5e4009b93
commit 8fbf77e0c8
15 changed files with 336 additions and 132 deletions

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -80,7 +80,7 @@ export const HTMLFragments = {
<label id='players-alive-label'></label>
<div id='game-player-list'></div>
</div>`,
MODERATOR_GAME_VIEW:
TRANSFER_MOD_MODAL:
`<div id='transfer-mod-modal-background' class='modal-background'></div>
<div tabindex='-1' id='transfer-mod-modal' class='modal'>
<h3>Transfer Mod Powers &#128081;</h3>
@@ -88,9 +88,10 @@ export const HTMLFragments = {
<div class='modal-button-container'>
<button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button>
</div>
</div>
<div id='game-header'>
<div class='timer-container-moderator'>
</div>`,
MODERATOR_GAME_VIEW:
`<div id='game-header'>
<div id='timer-container-moderator'>
<div>
<label for='game-timer'>Time Remaining</label>
<div id='game-timer'></div>
@@ -118,19 +119,8 @@ export const HTMLFragments = {
</div>
</div>`,
TEMP_MOD_GAME_VIEW:
`<div id='transfer-mod-modal-background' class='modal-background'></div>
<div id='transfer-mod-modal' class='modal'>
<form id='transfer-mod-form'>
<div id='transfer-mod-form-content'>
<h3>Transfer Mod Powers &#128081;</h3>
</div>
<div class='modal-button-container'>
<button id='close-modal-button' class='cancel app-button'>Cancel</button>
</div>
</form>
</div>
<div id='game-header'>
<div class='timer-container-moderator'>
`<div id='game-header'>
<div id='timer-container-moderator'>
<div>
<label for='game-timer'>Time Remaining</label>
<div id='game-timer'></div>
@@ -219,7 +209,7 @@ export const HTMLFragments = {
</div>`,
END_OF_GAME_VIEW:
`<div id='end-of-game-header'>
<h2>The moderator has ended the game. Roles are revealed.</h2>
<h2>&#x1F3C1; The moderator has ended the game. Roles are revealed.</h2>
<div id="end-of-game-buttons">
<div>
<button id='role-info-button' class='app-button'>View Role Info <img src='/images/info.svg'/></button>

View File

@@ -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');

View File

@@ -4,24 +4,23 @@ 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) {
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/' + code.toUpperCase().trim() + '/availability',
'/api/games/' + userCode.toUpperCase().trim() + '/availability',
'GET',
null,
null
@@ -35,6 +34,9 @@ function attemptToJoinGame (code) {
'&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) {
@@ -43,6 +45,9 @@ function attemptToJoinGame (code) {
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) {

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -19,7 +19,8 @@
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
</head>
<body>
<div id="prompt"></div>
<div id="role-info-prompt"></div>
<div id="transfer-mod-prompt"></div>
<div class="spinner-background"></div>
<div class="spinner-container">
<div class="lds-spinner">

View File

@@ -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',

View File

@@ -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;

View File

@@ -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 n1 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 n1 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 ++) {

View File

@@ -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);
});
});
});