diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index 2ca85cd..2a8ae80 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -18,7 +18,7 @@ export class Lobby { this.startGameHandler = (e) => { e.preventDefault(); - if (!stateBucket.currentGameState.isFull) { + if (!stateBucket.currentGameState.isStartable) { toast('The number of players does not match the number of cards. ' + 'You must either add/remove players or edit roles and their quantities.', 'error'); return; @@ -171,10 +171,10 @@ export class Lobby { } setSocketHandlers () { - this.socket.on(globals.EVENT_IDS.PLAYER_JOINED, (player, gameIsFull) => { + this.socket.on(globals.EVENT_IDS.PLAYER_JOINED, (player, gameisStartable) => { toast(player.name + ' joined!', 'success', true, true, 'short'); this.stateBucket.currentGameState.people.push(player); - this.stateBucket.currentGameState.isFull = gameIsFull; + this.stateBucket.currentGameState.isStartable = gameisStartable; this.populatePlayers(); if (( this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR @@ -193,7 +193,7 @@ export class Lobby { ); }); - this.socket.on(globals.EVENT_IDS.KICK_PERSON, (kickedId, gameIsFull) => { + this.socket.on(globals.EVENT_IDS.KICK_PERSON, (kickedId, gameisStartable) => { if (kickedId === this.stateBucket.currentGameState.client.id) { window.location = '/?message=' + encodeURIComponent('You were kicked by the moderator.'); } else { @@ -202,7 +202,7 @@ export class Lobby { this.stateBucket.currentGameState.people .splice(kickedIndex, 1); } - this.stateBucket.currentGameState.isFull = gameIsFull; + this.stateBucket.currentGameState.isStartable = gameisStartable; SharedStateUtil.setNumberOfSpectators( this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, document.getElementById('spectator-count') @@ -222,7 +222,7 @@ export class Lobby { this.socket.on(globals.EVENT_IDS.UPDATE_GAME_ROLES, (deck, gameSize) => { this.stateBucket.currentGameState.deck = deck; this.stateBucket.currentGameState.gameSize = gameSize; - this.stateBucket.currentGameState.isFull = this.stateBucket.currentGameState.people + this.stateBucket.currentGameState.isStartable = this.stateBucket.currentGameState.people .filter(person => person.userType === globals.USER_TYPES.PLAYER || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === gameSize; this.setLink(getTimeString(this.stateBucket.currentGameState)); diff --git a/server/model/Game.js b/server/model/Game.js index 03b254b..6027645 100644 --- a/server/model/Game.js +++ b/server/model/Game.js @@ -27,7 +27,7 @@ class Game { this.previousModeratorId = null; this.createTime = createTime; this.timerParams = timerParams; - this.isFull = (this.gameSize === 1 && !this.hasDedicatedModerator) || isTestGame; + this.isStartable = (this.gameSize === 1 && !this.hasDedicatedModerator) || isTestGame; this.timeRemaining = null; } } diff --git a/server/modules/Events.js b/server/modules/Events.js index 70f81db..97d521e 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -7,19 +7,14 @@ const Events = [ { id: EVENT_IDS.PLAYER_JOINED, stateChange: async (game, socketArgs, vars) => { - const toBeAssignedIndex = game.people.findIndex( - (person) => person.id === socketArgs.id && person.assigned === false - ); - if (toBeAssignedIndex >= 0) { - game.people[toBeAssignedIndex] = socketArgs; - game.isFull = vars.gameManager.isGameFull(game); - } + game.people.push(socketArgs); + game.isStartable = vars.gameManager.isGameStartable(game); }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, GameStateCurator.mapPerson(socketArgs), - game.isFull + game.isStartable ); } }, @@ -31,14 +26,14 @@ const Events = [ ); if (toBeClearedIndex >= 0) { game.people.splice(toBeClearedIndex, 1); - game.isFull = vars.gameManager.isGameFull(game); + game.isStartable = vars.gameManager.isGameStartable(game); } }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit( EVENT_IDS.KICK_PERSON, socketArgs.personId, - game.isFull + game.isStartable ); } }, @@ -51,7 +46,7 @@ const Events = [ (accumulator, currentValue) => accumulator + currentValue.quantity, 0 ); - game.isFull = vars.gameManager.isGameFull(game); + game.isStartable = vars.gameManager.isGameStartable(game); } }, communicate: async (game, socketArgs, vars) => { @@ -109,7 +104,7 @@ const Events = [ { id: EVENT_IDS.START_GAME, stateChange: async (game, socketArgs, vars) => { - if (game.isFull) { + if (game.isStartable) { game.status = globals.STATUS.IN_PROGRESS; vars.gameManager.deal(game); if (game.hasTimer) { diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index 836b3b5..97c2f25 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -71,7 +71,7 @@ function getGameStateBasedOnPermissions (game, person) { gameSize: game.gameSize, people: GameStateCurator.mapPeopleForModerator(game.people, client), timerParams: game.timerParams, - isFull: game.isFull + isStartable: game.isStartable }; case globals.USER_TYPES.TEMPORARY_MODERATOR: case globals.USER_TYPES.SPECTATOR: @@ -90,7 +90,7 @@ function getGameStateBasedOnPermissions (game, person) { }) .map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)), timerParams: game.timerParams, - isFull: game.isFull + isStartable: game.isStartable }; default: break; diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 33df774..c0fd078 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -181,7 +181,7 @@ class GameManager { && game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS ) { return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); - } else if (joinAsSpectator || this.isGameFull(game)) { + } else if (joinAsSpectator || this.isGameStartable(game)) { console.log('game is full'); return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame); } @@ -206,12 +206,12 @@ class GameManager { newPlayer.assigned = true; game.people.push(newPlayer); } - game.isFull = this.isGameFull(game); + game.isStartable = this.isGameStartable(game); await this.refreshGame(game); this.namespace.in(game.accessCode).emit( globals.EVENTS.PLAYER_JOINED, GameStateCurator.mapPerson(moderator || newPlayer), - game.isFull + game.isStartable ); await this.eventManager.publisher?.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, @@ -318,7 +318,7 @@ class GameManager { } } - isGameFull = (game) => { + isGameStartable = (game) => { return game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === game.gameSize; } diff --git a/spec/e2e/create_spec.js b/spec/e2e/create_spec.js index 68cd6ff..1b2029f 100644 --- a/spec/e2e/create_spec.js +++ b/spec/e2e/create_spec.js @@ -67,7 +67,7 @@ describe('Create page', function () { .toEqual('Test name'); }); - it('should successfully update custom role information after creating it', () => { + it('should successstartabley update custom role information after creating it', () => { document.getElementById('role-category-custom').click(); document.getElementById('custom-role-btn').click(); document.getElementById('role-name').value = 'Test name'; diff --git a/spec/e2e/game_spec.js b/spec/e2e/game_spec.js index a3295de..162ee71 100644 --- a/spec/e2e/game_spec.js +++ b/spec/e2e/game_spec.js @@ -69,18 +69,6 @@ describe('game page', () => { expect(document.getElementById('current-info-message').innerText).toEqual('Jane joined!'); }); - it('should activate the start button for the moderator when the game is full', () => { - expect(document.getElementById('start-game-button').classList.contains('disabled')).toBeTrue(); - mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({ - name: 'Jack', - id: '456', - userType: globals.USER_TYPES.PLAYER, - out: false, - revealed: false - }, true); - expect(document.getElementById('start-game-button').classList.contains('disabled')).toBeFalse(); - }); - afterAll(() => { document.body.innerHTML = ''; }); diff --git a/spec/support/MockGames.js b/spec/support/MockGames.js index c1ed8b5..37680e3 100644 --- a/spec/support/MockGames.js +++ b/spec/support/MockGames.js @@ -46,7 +46,7 @@ export const mockGames = { paused: true, timeRemaining: 600000 }, - isFull: false + isStartable: false }, inProgressGame: { accessCode: 'TVV6', @@ -163,7 +163,7 @@ export const mockGames = { paused: true, timeRemaining: 600000 }, - isFull: true + isStartable: true }, moderatorGame: { @@ -306,6 +306,6 @@ export const mockGames = { paused: true, timeRemaining: 600000 }, - isFull: true + isStartable: true } }; diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index d8e910a..cd7252a 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -34,10 +34,10 @@ describe('Events', () => { STATUS.LOBBY, [ { id: 'a', assigned: true, out: true, killed: false, userType: USER_TYPES.MODERATOR }, - { id: 'b', gameRole: 'Villager', alignment: 'good', assigned: false, out: false, killed: false, userType: USER_TYPES.PLAYER }, + { id: 'b', gameRole: 'Villager', alignment: 'good', assigned: true, out: false, killed: false, userType: USER_TYPES.PLAYER }, { id: 'c', assigned: true, out: true, killed: false, userType: USER_TYPES.SPECTATOR } ], - [{ quantity: 2 }], + [{ quantity: 1 }, { quantity: 1 }], false, 'a', true, @@ -50,7 +50,7 @@ describe('Events', () => { spyOn(socket, 'to').and.callThrough(); spyOn(namespace.in(), 'emit').and.callThrough(); spyOn(namespace.to(), 'emit').and.callThrough(); - spyOn(gameManager, 'isGameFull').and.callThrough(); + spyOn(gameManager, 'isGameStartable').and.callThrough(); spyOn(GameStateCurator, 'mapPerson').and.callThrough(); spyOn(eventManager.publisher, 'publish').and.callThrough(); spyOn(eventManager, 'createMessageToPublish').and.stub(); @@ -60,38 +60,32 @@ describe('Events', () => { describe(EVENT_IDS.PLAYER_JOINED, () => { describe('stateChange', () => { - it('should let a player join and mark the game as full', async () => { + it('should let a player join and mark the game as startable', async () => { await Events.find((e) => e.id === EVENT_IDS.PLAYER_JOINED) - .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); - expect(gameManager.isGameFull).toHaveBeenCalled(); - expect(game.isFull).toEqual(true); + .stateChange(game, { id: 'd', assigned: true, userType: USER_TYPES.PLAYER }, { gameManager: gameManager }); + expect(gameManager.isGameStartable).toHaveBeenCalled(); + expect(game.isStartable).toEqual(true); expect(game.people.find(p => p.id === 'b').assigned).toEqual(true); }); - it('should let a player join and mark the game as NOT full', async () => { - game.people.push({ id: 'd', assigned: false, userType: USER_TYPES.PLAYER }); + it('should let too many players join and mark the game as NOT startable', async () => { + game.people.push({ id: 'e', assigned: true, userType: USER_TYPES.PLAYER }); + game.people.push({ id: 'f', assigned: true, userType: USER_TYPES.PLAYER }); await Events.find((e) => e.id === EVENT_IDS.PLAYER_JOINED) .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); - expect(gameManager.isGameFull).toHaveBeenCalled(); - expect(game.isFull).toEqual(false); + expect(gameManager.isGameStartable).toHaveBeenCalled(); + expect(game.isStartable).toEqual(false); expect(game.people.find(p => p.id === 'b').assigned).toEqual(true); }); - it('should not let the player join if their id does not match some unassigned person', async () => { - await Events.find((e) => e.id === EVENT_IDS.PLAYER_JOINED) - .stateChange(game, { id: 'd', assigned: true }, { gameManager: gameManager }); - expect(gameManager.isGameFull).not.toHaveBeenCalled(); - expect(game.isFull).toEqual(false); - expect(game.people.find(p => p.id === 'd')).not.toBeDefined(); - }); }); describe('communicate', () => { it('should communicate the join event to the rooms sockets, sending the new player', async () => { await Events.find((e) => e.id === EVENT_IDS.PLAYER_JOINED) - .communicate(game, { id: 'b', assigned: true }, { gameManager: gameManager }); + .communicate(game, { id: 'd', assigned: true, userType: USER_TYPES.PLAYER }, { gameManager: gameManager }); expect(namespace.in).toHaveBeenCalledWith(game.accessCode); expect(namespace.in().emit).toHaveBeenCalledWith( globals.EVENTS.PLAYER_JOINED, - GameStateCurator.mapPerson({ id: 'b', assigned: true }), - game.isFull + GameStateCurator.mapPerson({ id: 'd', assigned: true, userType: USER_TYPES.PLAYER }), + game.isStartable ); }); }); @@ -102,8 +96,8 @@ describe('Events', () => { it('should add a spectator', async () => { await Events.find((e) => e.id === EVENT_IDS.ADD_SPECTATOR) .stateChange(game, { id: 'e', name: 'ghost', assigned: true }, { gameManager: gameManager }); - expect(gameManager.isGameFull).not.toHaveBeenCalled(); - expect(game.isFull).toEqual(false); + expect(gameManager.isGameStartable).not.toHaveBeenCalled(); + expect(game.isStartable).toEqual(false); expect(game.people.find(p => p.id === 'e').assigned).toEqual(true); expect(game.people.find(p => p.id === 'e').name).toEqual('ghost'); }); @@ -197,18 +191,18 @@ describe('Events', () => { describe(EVENT_IDS.START_GAME, () => { describe('stateChange', () => { it('should start the game', async () => { - game.isFull = true; + game.isStartable = true; await Events.find((e) => e.id === EVENT_IDS.START_GAME) .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); expect(game.status).toEqual(STATUS.IN_PROGRESS); }); - it('should not start the game if it is not full', async () => { + it('should not start the game if it is not startable', async () => { await Events.find((e) => e.id === EVENT_IDS.START_GAME) .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); expect(game.status).toEqual(STATUS.LOBBY); }); it('should start the game and run the timer if the game has one', async () => { - game.isFull = true; + game.isStartable = true; game.hasTimer = true; game.timerParams = {}; spyOn(timerManager, 'runTimer').and.callFake((a, b) => {}); diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 69c5ef1..6afa3ac 100644 --- a/spec/unit/server/modules/singletons/GameManager_Spec.js +++ b/spec/unit/server/modules/singletons/GameManager_Spec.js @@ -37,8 +37,8 @@ describe('GameManager', () => { 'ABCD', STATUS.LOBBY, [{ id: 'a', name: 'person1', assigned: true, out: true, killed: false, userType: USER_TYPES.MODERATOR }, - { id: 'b', name: 'person2', gameRole: 'Villager', alignment: 'good', assigned: false, out: false, killed: false, userType: USER_TYPES.PLAYER }], - [{ quantity: 2 }], + { id: 'b', name: 'person2', gameRole: 'Villager', alignment: 'good', assigned: true, out: false, killed: false, userType: USER_TYPES.PLAYER }], + [{ quantity: 1 }, { quantity: 1 }], false, 'a', true, @@ -50,30 +50,26 @@ describe('GameManager', () => { }); describe('#joinGame', () => { - it('should mark the game as full when all players have been assigned', async () => { + it('should mark the game as startable when the number of players equals the game size', async () => { await gameManager.joinGame(game, 'Jill', 'x'); - expect(game.isFull).toEqual(true); + expect(game.isStartable).toEqual(true); }); - it('should create a spectator if the game is already full and broadcast it to the room', () => { - game.people.find(p => p.id === 'b').assigned = true; - game.isFull = true; + it('should create a spectator if the game is already startable and broadcast it to the room', async () => { + await gameManager.joinGame(game, 'Jill', 'x'); + game.isStartable = true; spyOn(gameManager.namespace.in(), 'emit'); - gameManager.joinGame(game, 'Jane', 'x'); + await gameManager.joinGame(game, 'Jane', 'x'); - expect(game.isFull).toEqual(true); + expect(game.isStartable).toEqual(true); expect(game.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length).toEqual(1); }); }); describe('#restartGame', () => { - let shuffleSpy; - - beforeEach(() => { - shuffleSpy = spyOn(gameManager, 'shuffle').and.stub(); - }); + beforeEach(() => {}); it('should reset all relevant game parameters', async () => { game.status = STATUS.ENDED; @@ -85,11 +81,10 @@ describe('GameManager', () => { await gameManager.restartGame(game, namespace); - expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.status).toEqual(STATUS.LOBBY); expect(player.userType).toEqual(USER_TYPES.PLAYER); expect(player.out).toBeFalse(); expect(player.killed).toBeFalse(); - expect(shuffleSpy).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME); }); @@ -100,16 +95,12 @@ describe('GameManager', () => { game.status = STATUS.ENDED; const threadKillSpy = spyOn(timerManager.timerThreads.ABCD, 'kill'); - const runTimerSpy = spyOn(timerManager, 'runTimer').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.status).toEqual(STATUS.LOBBY); expect(threadKillSpy).toHaveBeenCalled(); - expect(runTimerSpy).toHaveBeenCalled(); - expect(shuffleSpy).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME); }); @@ -123,10 +114,9 @@ describe('GameManager', () => { await gameManager.restartGame(game, namespace); - expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.status).toEqual(STATUS.LOBBY); expect(game.currentModeratorId).toEqual('b'); expect(game.people.find(p => p.id === 'b').userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR); - expect(shuffleSpy).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME); }); });