diff --git a/client/src/config/globals.js b/client/src/config/globals.js index afc8c12..d4607dd 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -55,6 +55,14 @@ export const globals = { RESTART_GAME: 'restartGame', ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' }, + TIMER_EVENTS: function () { + return [ + this.EVENT_IDS.PAUSE_TIMER, + this.EVENT_IDS.RESUME_TIMER, + this.EVENT_IDS.GET_TIME_REMAINING, + this.EVENT_IDS.END_TIMER + ]; + }, LOBBY_EVENTS: function () { return [ this.EVENT_IDS.PLAYER_JOINED, diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 9d7d928..0f2d613 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -200,7 +200,12 @@ export class InProgress { revealedPerson.gameRole = revealData.gameRole; revealedPerson.alignment = revealData.alignment; if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { - toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium'); + if (revealedPerson.id === this.stateBucket.currentGameState.client.id) { + toast('You revealed your role.', 'success', true, true, 'medium'); + } else { + toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium'); + } + this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); } else { if (revealedPerson.id === this.stateBucket.currentGameState.client.id) { @@ -230,9 +235,13 @@ export class InProgress { }); if (this.stateBucket.currentGameState.timerParams) { - if (!this.stateBucket.timerWorker) { - this.stateBucket.timerWorker = new Worker(new URL('../../timer/Timer.js', import.meta.url)); + if (this.stateBucket.timerWorker) { + this.stateBucket.timerWorker.terminate(); + this.stateBucket.timerWorker = null; } + + this.stateBucket.timerWorker = new Worker(new URL('../../timer/Timer.js', import.meta.url)); + const gameTimerManager = new GameTimerManager(this.stateBucket, this.socket); gameTimerManager.attachTimerSocketListeners(this.socket, this.stateBucket.timerWorker); } diff --git a/client/src/modules/timer/GameTimerManager.js b/client/src/modules/timer/GameTimerManager.js index 0e1b6be..9b76f79 100644 --- a/client/src/modules/timer/GameTimerManager.js +++ b/client/src/modules/timer/GameTimerManager.js @@ -111,17 +111,15 @@ export class GameTimerManager { } attachTimerSocketListeners (socket, timerWorker) { - if (!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) { - socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => { - this.pauseGameTimer(timerWorker, timeRemaining); - }); - } + globals.TIMER_EVENTS().forEach(e => socket.removeAllListeners(e)); - if (!socket.hasListeners(globals.COMMANDS.RESUME_TIMER)) { - socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => { - this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); - }); - } + socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => { + this.pauseGameTimer(timerWorker, timeRemaining); + }); + + socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => { + this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); + }); socket.once(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => { if (paused) { @@ -133,11 +131,9 @@ export class GameTimerManager { } }); - if (!socket.hasListeners(globals.COMMANDS.END_TIMER)) { - socket.on(globals.COMMANDS.END_TIMER, () => { - Confirmation('The timer has expired!'); - }); - } + socket.on(globals.COMMANDS.END_TIMER, () => { + Confirmation('The timer has expired!'); + }); } swapToPlayButton () { diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index 9cca6ef..aa025f6 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -22,9 +22,9 @@ router.post('/sockets/broadcast', function (req, res) { router.get('/games/state', async (req, res) => { const gamesArray = []; - await timerManager.client.keys('*').then(async (r) => { - Object.values(r).forEach((v) => gamesArray.push(JSON.parse(v))); - }); + const keys = await eventManager.client.keys('*'); + const values = await eventManager.client.mGet(keys); + values.forEach((v) => gamesArray.push(JSON.parse(v))); res.status(200).send(gamesArray); }); diff --git a/server/modules/Events.js b/server/modules/Events.js index 32382df..f742fc8 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -124,7 +124,7 @@ const Events = [ id: EVENT_IDS.END_GAME, stateChange: async (game, socketArgs, vars) => { game.status = globals.STATUS.ENDED; - if (vars.timerManager.timerThreads[game.accessCode]) { + if (game.hasTimer && vars.timerManager.timerThreads[game.accessCode]) { vars.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode); vars.timerManager.timerThreads[game.accessCode].kill(); } diff --git a/spec/e2e/game_spec.js b/spec/e2e/game_spec.js index dedaa2f..d02f0b3 100644 --- a/spec/e2e/game_spec.js +++ b/spec/e2e/game_spec.js @@ -25,11 +25,17 @@ describe('game page', () => { on: function (message, handler) { this.eventHandlers[message] = handler; }, + once: function (message, handler) { + this.eventHandlers[message] = handler; + }, emit: function (eventName, ...args) { switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type case globals.EVENT_IDS.FETCH_GAME_STATE: args[args.length - 1](deepCopy(mockGames.gameInLobby)); // copy the game object to prevent leaking of state between specs } + }, + removeAllListeners: function(...names) { + }, hasListeners: function (listener) { return false; @@ -98,7 +104,13 @@ describe('game page', () => { }, hasListeners: function (listener) { return false; - } + }, + removeAllListeners: function(...names) { + + }, + once: function (message, handler) { + this.eventHandlers[message] = handler; + }, }; await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); mockSocket.eventHandlers.connect(); @@ -106,11 +118,11 @@ describe('game page', () => { }); it('should display the game role of the client', () => { - expect(document.getElementById('role-name').innerText).toEqual('Parity Hunter'); - expect(document.getElementById('role-image').getAttribute('src')).toEqual('../images/roles/ParityHunter.png'); + expect(document.getElementById('role-name').innerText).toEqual('Villager'); + expect(document.getElementById('role-image').getAttribute('src')).toContain('../images/roles/Villager'); expect(document.getElementById('game-timer').innerText).toEqual('00:02:00'); expect(document.getElementById('game-timer').classList.contains('paused')).toEqual(true); - expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 4 / 5 Alive'); + expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 6 / 7 Alive'); }); it('should flip the role card of the client', () => { @@ -128,7 +140,7 @@ describe('game page', () => { }); it('should display the number of alive players', () => { - expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 4 / 5 Alive'); + expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 6 / 7 Alive'); }); it('should display the role info modal when the button is clicked', () => { @@ -155,6 +167,9 @@ describe('game page', () => { on: function (message, handler) { this.eventHandlers[message] = handler; }, + once: function (message, handler) { + this.eventHandlers[message] = handler; + }, emit: function (eventName, ...args) { switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type case globals.EVENT_IDS.FETCH_GAME_STATE: @@ -169,7 +184,10 @@ describe('game page', () => { }, hasListeners: function (listener) { return false; - } + }, + removeAllListeners: function(...names) { + + }, }; await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate); mockSocket.eventHandlers.connect(); @@ -188,7 +206,7 @@ describe('game page', () => { it('should display players by their alignment', () => { expect(document.querySelector('.evil-players')).not.toBeNull(); expect(document.querySelector('.good-players')).not.toBeNull(); - expect(document.querySelector('div[data-pointer="FCVSGJFYWLDL5S3Y8B74ZVZLZ"]') + expect(document.querySelector('div[data-pointer="v2eOvaYKusGfiUpuZWTCJ0JUiESC29OuH6fpivwMuwcqizpYTCAzetrPl7fF8F5CoR35pTMIKxh"]') .querySelector('.game-player-role').innerText).toEqual('Werewolf'); }); @@ -198,38 +216,38 @@ describe('game page', () => { it('should display the mod transfer modal, with the single spectator available for selection', () => { document.getElementById('mod-transfer-button').click(); - expect(document.querySelector('div[data-pointer="MGGVR8KQ7V7HGN3QBLJ5339ZL"].potential-moderator') - .innerText).toContain('Matt'); + expect(document.querySelector('div[data-pointer="BKfs1N0cfvwc309eOdwrTeum8NScSX7S8CTCGXgiI6JZufjAgD4WAdkkryn3sqIqKeswCFpIuTc"].potential-moderator') + .innerText).toContain('Stav'); document.getElementById('close-mod-transfer-modal-button').click(); }); it('should emit the appropriate socket event when killing a player, and indicate the result on the UI', () => { - document.querySelector('div[data-pointer="FCVSGJFYWLDL5S3Y8B74ZVZLZ"]') + document.querySelector('div[data-pointer="pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW"]') .querySelector('.kill-player-button').click(); document.getElementById('confirmation-yes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.KILL_PLAYER, mockGames.moderatorGame.accessCode, - { personId: 'FCVSGJFYWLDL5S3Y8B74ZVZLZ' } + { personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' } ); - mockSocket.eventHandlers.killPlayer('FCVSGJFYWLDL5S3Y8B74ZVZLZ'); - expect(document.querySelector('div[data-pointer="FCVSGJFYWLDL5S3Y8B74ZVZLZ"].game-player.killed') + mockSocket.eventHandlers.killPlayer('pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW'); + expect(document.querySelector('div[data-pointer="pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW"].game-player.killed') ).not.toBeNull(); }); it('should emit the appropriate socket event when revealing a player, and indicate the result on the UI', () => { - document.querySelector('div[data-pointer="FCVSGJFYWLDL5S3Y8B74ZVZLZ"]') + document.querySelector('div[data-pointer="pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW"]') .querySelector('.reveal-role-button').click(); document.getElementById('confirmation-yes-button').click(); expect(mockSocket.emit).toHaveBeenCalledWith( globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.REVEAL_PLAYER, mockGames.moderatorGame.accessCode, - { personId: 'FCVSGJFYWLDL5S3Y8B74ZVZLZ' } + { personId: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW' } ); - mockSocket.eventHandlers.revealPlayer({ id: 'FCVSGJFYWLDL5S3Y8B74ZVZLZ', gameRole: 'Werewolf', alignment: 'evil' }); - expect(document.querySelector('div[data-pointer="FCVSGJFYWLDL5S3Y8B74ZVZLZ"]') + mockSocket.eventHandlers.revealPlayer({ id: 'pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW', gameRole: 'Werewolf', alignment: 'evil' }); + expect(document.querySelector('div[data-pointer="pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW"]') .querySelector('.reveal-role-button')).toBeNull(); }); diff --git a/spec/support/MockGames.js b/spec/support/MockGames.js index 5e3501a..4cbc179 100644 --- a/spec/support/MockGames.js +++ b/spec/support/MockGames.js @@ -1,305 +1,311 @@ export const mockGames = { gameInLobby: { - accessCode: 'ZS6M', - status: 'lobby', - moderator: { - name: 'Alec', - id: 'HZM64BVGXCSXS9L5YMGK2WTTQ', - userType: 'moderator', - out: false, - revealed: false + "accessCode": "TVV6", + "status": "lobby", + "currentModeratorId": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "client": { + "name": "Alec", + "hasEnteredName": false, + "id": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "cookie": "28p80dbhY2k1iP1NuEy8UPFmuOctLx3nR0EMONU4MlJFfVrCzNncdNdsav9wEuGEswLQ70DKqa3", + "userType": "moderator" }, - client: { - name: 'Alec', - hasEnteredName: false, - id: 'HZM64BVGXCSXS9L5YMGK2WTTQ', - cookie: 'Q68BYSMM7DB5CH338TNPMF9CK', - userType: 'moderator' - }, - deck: [ + "deck": [ { - role: 'Parity Hunter', - team: 'good', - description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.', - id: 'wli3r2i9zxxmnns5euvtc01v0', - quantity: 1 + "role": "Villager", + "team": "good", + "description": "During the day, find the wolves and kill them.", + "id": "52u5w81ryq5h30qu1gri56xxq", + "quantity": 6 }, { - role: 'Seer', - team: 'good', - description: 'Each night, learn if a chosen person is a Werewolf.', - id: '7q0xxfuflsjetzit1elu5rd2k', - quantity: 1 - }, - { - role: 'Villager', - team: 'good', - description: 'During the day, find the wolves and kill them.', - id: '33pw77odkdt3042yumxtxbrda', - quantity: 1 - }, - { - role: 'Sorceress', - team: 'evil', - description: 'Each night, learn if a chosen person is the Seer.', - id: '6fboglgqwua8n0twgh2f4a0xh', - quantity: 1 - }, - { - role: 'Werewolf', - team: 'evil', - description: "During the night, choose a villager to kill. Don't get killed.", - id: 'ixpmpaouc3oj1llkm6gttxbor', - quantity: 1 + "role": "Werewolf", + "team": "evil", + "description": "During the night, choose a villager to kill. Don't get killed.", + "id": "9uk0jcrm1hkhygzb6iw8xh2a7", + "quantity": 1 } ], - people: [], - timerParams: { - hours: null, - minutes: 15, - paused: false + "gameSize": 7, + "people": [ + { + "name": "Alec", + "id": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "userType": "moderator", + "gameRole": null, + "gameRoleDescription": null, + "alignment": null, + "out": true, + "killed": false, + "revealed": false + } + ], + "timerParams": { + "hours": null, + "minutes": 10, + "paused": true, + "timeRemaining": 600000 }, - isFull: false, - spectators: [] + "isFull": false }, inProgressGame: { - accessCode: 'VVVG', - status: 'in progress', - moderator: { - name: 'Alec', - id: 'H24358C4GQ238LFK66RYMST9P', - userType: 'moderator', - out: false, - revealed: false + "accessCode": "TVV6", + "status": "in progress", + "currentModeratorId": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "client": { + "name": "Andrea", + "hasEnteredName": false, + "id": "pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW", + "cookie": "iIXXtc4BMtMSKOiDnz7sp20AE5QEIEzw7Ro2djXkZax4PQo3jR3VQGEAaD3WvaEBt06ZWgqi8s9", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false }, - client: { - name: 'Andrea', - hasEnteredName: false, - id: 'THCX9K6MCKZXBXYH95FPLP68Y', - cookie: 'ZLPHS946H33W7LVJ28M8XCRVZ', - userType: 'player', - gameRole: 'Parity Hunter', - gameRoleDescription: 'You beat a werewolf in a 1v1 situation, winning the game for the village.', - alignment: 'good', - out: false - }, - deck: [ + "deck": [ { - role: 'Parity Hunter', - team: 'good', - description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.', - id: 'gw82x923gde5pcf3ru8y0w6mr', - quantity: 1 + "role": "Villager", + "team": "good", + "description": "During the day, find the wolves and kill them.", + "id": "52u5w81ryq5h30qu1gri56xxq", + "quantity": 6 }, { - role: 'Seer', - team: 'good', - description: 'Each night, learn if a chosen person is a Werewolf.', - id: '0it2wybz7mdoatqs60b847x5v', - quantity: 1 - }, - { - role: 'Villager', - team: 'good', - description: 'During the day, find the wolves and kill them.', - id: 'v8oeyscxu53bg0a29uxsh4mzc', - quantity: 1 - }, - { - role: 'Sorceress', - team: 'evil', - description: 'Each night, learn if a chosen person is the Seer.', - id: '52ooljj12xpah0dgirxay2lma', - quantity: 1 - }, - { - role: 'Werewolf', - team: 'evil', - description: "During the night, choose a villager to kill. Don't get killed.", - id: '1oomauy0wc9pn5q55d2f4zq64', - quantity: 1 + "role": "Werewolf", + "team": "evil", + "description": "During the night, choose a villager to kill. Don't get killed.", + "id": "9uk0jcrm1hkhygzb6iw8xh2a7", + "quantity": 1 } ], - people: [ + "gameSize": 7, + "people": [ { - name: 'Andrea', - id: 'THCX9K6MCKZXBXYH95FPLP68Y', - userType: 'player', - out: false, - revealed: false + "name": "Andrea", + "id": "pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW", + "userType": "player", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Greg', - id: 'SFVBXJZNF3G3QDML63X34KG5X', - userType: 'player', - out: false, - revealed: false + "name": "Hannah", + "id": "ojCjJqIXfNkOunHD9v2gQSNXRUQPzZCJzzuPTKyQy5TiyZ83ziHMvr2ZzDLWMb4M6dXqa8rN3O3", + "userType": "killed", + "out": true, + "killed": true, + "revealed": true, + "gameRole": "Villager", + "alignment": "good" }, { - name: 'Lys', - id: 'S2496LVXL9CFP5B493XX6XMYL', - userType: 'player', - out: false, - revealed: false + "name": "Greg", + "id": "1o2IntIivHV4pMqV2iMBi3lrMloHcShQiXObuedyc1DqeUho5aD0DI6Vqhja97c1GIMNzfyNC4a", + "userType": "player", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Hannah', - id: 'Y7P2LGDZL6NV283525PL5GZTB', - userType: 'player', - out: true, - revealed: true + "name": "Jerret", + "id": "eaxAqb1nj25jqWHnsWirSdYjLhHQVkQtnIkC4eHvmuleN7FtaG8XYnLJnBv4hhFvXWNbUguTrLJ", + "userType": "player", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Matthew', - id: 'Z9YZ2JBM2GPRXFJB9J6ZFNSP9', - userType: 'player', - out: false, - revealed: false + "name": "Lys", + "id": "v2eOvaYKusGfiUpuZWTCJ0JUiESC29OuH6fpivwMuwcqizpYTCAzetrPl7fF8F5CoR35pTMIKxh", + "userType": "player", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Matt", + "id": "pUDrpiGF1vuMfhztT2KY9bllBoGILVl2vIpRWVFH27SnqGiVP3LunjO0wy0otXToWzwbXBlx7ga", + "userType": "player", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Steve", + "id": "Csz1haKdNa3WLIbqllRwV2e9TgwMlDoQwzbpZkTa0JhioUT5MD1GopUHU90f6cyfQ2Uv7YBTZo1", + "userType": "player", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Alec", + "id": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "userType": "moderator", + "out": true, + "killed": false, + "revealed": false + }, + { + "name": "Stav", + "id": "BKfs1N0cfvwc309eOdwrTeum8NScSX7S8CTCGXgiI6JZufjAgD4WAdkkryn3sqIqKeswCFpIuTc", + "userType": "spectator", + "out": true, + "killed": false, + "revealed": false } ], - timerParams: { - hours: null, - minutes: 2, - paused: true, - timeRemaining: 120000 + "timerParams": { + "hours": null, + "minutes": 10, + "paused": true, + "timeRemaining": 600000 }, - isFull: true, - spectators: [] + "isFull": true }, moderatorGame: { - accessCode: 'LYG5', - status: 'in progress', - moderator: { - name: 'Alec', - id: 'F623SN7JJMV5QW8K9MNQWW4WP', - userType: 'moderator', - out: false, - revealed: false + "accessCode": "TVV6", + "status": "in progress", + "currentModeratorId": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "client": { + "name": "Alec", + "hasEnteredName": false, + "id": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "cookie": "28p80dbhY2k1iP1NuEy8UPFmuOctLx3nR0EMONU4MlJFfVrCzNncdNdsav9wEuGEswLQ70DKqa3", + "userType": "moderator", + "gameRole": null, + "gameRoleDescription": null, + "alignment": null, + "out": true, + "killed": false }, - client: { - name: 'Alec', - hasEnteredName: false, - id: 'F623SN7JJMV5QW8K9MNQWW4WP', - cookie: 'ZJ9RQDF6CNZKZQCSKP4WSHDHQ', - userType: 'moderator', - gameRole: null, - gameRoleDescription: null, - alignment: null, - out: false - }, - deck: [ + "deck": [ { - role: 'Parity Hunter', - team: 'good', - description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.', - id: 'bfs9pwk81yu9k47ho4xyidzy7', - quantity: 1 + "role": "Villager", + "team": "good", + "description": "During the day, find the wolves and kill them.", + "id": "52u5w81ryq5h30qu1gri56xxq", + "quantity": 6 }, { - role: 'Seer', - team: 'good', - description: 'Each night, learn if a chosen person is a Werewolf.', - id: '0rob6qyg3eq7douedxen5pb44', - quantity: 1 - }, - { - role: 'Villager', - team: 'good', - description: 'During the day, find the wolves and kill them.', - id: 'fq9n3u95ka16smbu6zaivnuvv', - quantity: 1 - }, - { - role: 'Sorceress', - team: 'evil', - description: 'Each night, learn if a chosen person is the Seer.', - id: 'bwptvwzg0u1aao48045ht57wx', - quantity: 1 - }, - { - role: 'Werewolf', - team: 'evil', - description: "During the night, choose a villager to kill. Don't get killed.", - id: 'c9gziuv8bon9bhmyuanfvecpd', - quantity: 1 + "role": "Werewolf", + "team": "evil", + "description": "During the night, choose a villager to kill. Don't get killed.", + "id": "9uk0jcrm1hkhygzb6iw8xh2a7", + "quantity": 1 } ], - gameSize: 5, - people: [ + "gameSize": 7, + "people": [ { - name: 'Greg', - id: 'HVB3SK3XPGNSP34W2GVD5G3SP', - userType: 'player', - gameRole: 'Seer', - gameRoleDescription: 'Each night, learn if a chosen person is a Werewolf.', - alignment: 'good', - out: false, - revealed: false + "name": "Andrea", + "id": "pTtVXDJaxtXcrlbG8B43Wom67snoeO24RNEkO6eB2BaIftTdvpnfe1QR65DVj9A6I3VOoKZkYQW", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Lys', - id: 'XJNHYX85HCKYDQLKYN584CRKK', - userType: 'player', - gameRole: 'Sorceress', - gameRoleDescription: 'Each night, learn if a chosen person is the Seer.', - alignment: 'evil', - out: false, - revealed: false + "name": "Hannah", + "id": "ojCjJqIXfNkOunHD9v2gQSNXRUQPzZCJzzuPTKyQy5TiyZ83ziHMvr2ZzDLWMb4M6dXqa8rN3O3", + "userType": "killed", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": true, + "killed": true, + "revealed": true }, { - name: 'Colette', - id: 'MLTP5M76K6NN83VQBDTNC6ZP5', - userType: 'player', - gameRole: 'Parity Hunter', - gameRoleDescription: 'You beat a werewolf in a 1v1 situation, winning the game for the village.', - alignment: 'good', - out: false, - revealed: false + "name": "Greg", + "id": "1o2IntIivHV4pMqV2iMBi3lrMloHcShQiXObuedyc1DqeUho5aD0DI6Vqhja97c1GIMNzfyNC4a", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Hannah', - id: 'FCVSGJFYWLDL5S3Y8B74ZVZLZ', - userType: 'player', - gameRole: 'Werewolf', - gameRoleDescription: "During the night, choose a villager to kill. Don't get killed.", - alignment: 'evil', - out: false, - revealed: false + "name": "Jerret", + "id": "eaxAqb1nj25jqWHnsWirSdYjLhHQVkQtnIkC4eHvmuleN7FtaG8XYnLJnBv4hhFvXWNbUguTrLJ", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false, + "revealed": false }, { - name: 'Andrea', - id: 'VWLJ298FVTZR22R4TNCMRTB5B', - userType: 'player', - gameRole: 'Villager', - gameRoleDescription: 'During the day, find the wolves and kill them.', - alignment: 'good', - out: false, - revealed: false + "name": "Lys", + "id": "v2eOvaYKusGfiUpuZWTCJ0JUiESC29OuH6fpivwMuwcqizpYTCAzetrPl7fF8F5CoR35pTMIKxh", + "userType": "player", + "gameRole": "Werewolf", + "gameRoleDescription": "During the night, choose a villager to kill. Don't get killed.", + "alignment": "evil", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Matt", + "id": "pUDrpiGF1vuMfhztT2KY9bllBoGILVl2vIpRWVFH27SnqGiVP3LunjO0wy0otXToWzwbXBlx7ga", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Steve", + "id": "Csz1haKdNa3WLIbqllRwV2e9TgwMlDoQwzbpZkTa0JhioUT5MD1GopUHU90f6cyfQ2Uv7YBTZo1", + "userType": "player", + "gameRole": "Villager", + "gameRoleDescription": "During the day, find the wolves and kill them.", + "alignment": "good", + "out": false, + "killed": false, + "revealed": false + }, + { + "name": "Alec", + "id": "w8qarnG6FgAZQvRYsAFefldwU2r6KIeOce3nGaLxnfMlKIBOLj0DhUSC951bQ7yLwbRjDAS72r4", + "userType": "moderator", + "gameRole": null, + "gameRoleDescription": null, + "alignment": null, + "out": true, + "killed": false, + "revealed": false + }, + { + "name": "Stav", + "id": "BKfs1N0cfvwc309eOdwrTeum8NScSX7S8CTCGXgiI6JZufjAgD4WAdkkryn3sqIqKeswCFpIuTc", + "userType": "spectator", + "gameRole": null, + "gameRoleDescription": null, + "alignment": null, + "out": true, + "killed": false, + "revealed": false } ], - timerParams: { - hours: null, - minutes: 30, - paused: true, - timeRemaining: 1800000 + "timerParams": { + "hours": null, + "minutes": 10, + "paused": true, + "timeRemaining": 600000 }, - isFull: true, - spectators: [ - { - id: 'MGGVR8KQ7V7HGN3QBLJ5339ZL', - cookie: '9M2F677JBGWKCJBMXR54GBWWZ', - socketId: '3RdkA19luMvUfVh2AAAP', - name: 'Matt', - userType: 'spectator', - gameRole: null, - gameRoleDescription: null, - alignment: null, - assigned: false, - out: false, - revealed: false, - hasEnteredName: false - } - ] + "isFull": true } }; diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js new file mode 100644 index 0000000..639b6fa --- /dev/null +++ b/spec/unit/server/modules/Events_Spec.js @@ -0,0 +1,323 @@ +// TODO: clean up these deep relative paths? jsconfig.json is not working... +const Game = require('../../../../server/model/Game'); +const globals = require('../../../../server/config/globals'); +const EVENT_IDS = globals.EVENT_IDS; +const USER_TYPES = globals.USER_TYPES; +const STATUS = globals.STATUS; +const GameManager = require('../../../../server/modules/singletons/GameManager.js'); +const TimerManager = require('../../../../server/modules/singletons/TimerManager.js'); +const Events = require('../../../../server/modules/Events.js'); +const GameStateCurator = require('../../../../server/modules/GameStateCurator.js'); +const logger = require('../../../../server/modules/Logger.js')(false); + +describe('Events', () => { + let gameManager, namespace, socket, game, timerManager; + + beforeAll(() => { + spyOn(logger, 'debug'); + spyOn(logger, 'error'); + + const inObj = { emit: () => {} }; + namespace = { in: () => { return inObj; }, to: () => { return inObj; }, sockets: new Map() }; + socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; + gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test'); + timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); + gameManager.setGameSocketNamespace(namespace); + }); + + beforeEach(() => { + game = new Game( + 'ABCD', + 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 }], + [{ quantity: 2 }], + false, + 'a', + true, + 'a', + new Date().toJSON(), + null + ); + spyOn(namespace, 'to').and.callThrough(); + spyOn(namespace, 'in').and.callThrough(); + spyOn(socket, 'to').and.callThrough(); + spyOn(namespace.in(), 'emit').and.callThrough(); + spyOn(gameManager, 'isGameFull').and.callThrough(); + spyOn(GameStateCurator, 'mapPerson').and.callThrough(); + namespace.sockets = new Map(); + timerManager.timerThreads = {}; + }); + + describe(EVENT_IDS.PLAYER_JOINED, () => { + describe('stateChange', () => { + it('should let a player join and mark the game as full', 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); + 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: 'c', assigned: false, 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(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 }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith( + globals.EVENTS.PLAYER_JOINED, + GameStateCurator.mapPerson({ id: 'b', assigned: true }), + game.isFull + ); + }); + }); + }); + + describe(EVENT_IDS.ADD_SPECTATOR, () => { + describe('stateChange', () => { + 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(game.people.find(p => p.id === 'e').assigned).toEqual(true); + expect(game.people.find(p => p.id === 'e').name).toEqual('ghost'); + }); + }); + describe('communicate', () => { + it('should communicate the add spectator event to the rooms sockets, sending the new spectator', async () => { + await Events.find((e) => e.id === EVENT_IDS.ADD_SPECTATOR) + .communicate(game, { id: 'e', name: 'ghost', assigned: true }, { gameManager: gameManager }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith( + EVENT_IDS.ADD_SPECTATOR, + GameStateCurator.mapPerson({ id: 'e', name: 'ghost', assigned: true }) + ); + }); + }); + }); + + describe(EVENT_IDS.FETCH_GAME_STATE, () => { + describe('stateChange', () => { + it('should find the matching person and update their associated socket id if it is different', async () => { + const mockSocket = { join: () => {} }; + spyOn(mockSocket, 'join').and.callThrough(); + namespace.sockets.set('123', mockSocket); + game.people.push({ cookie: 'cookie', socketId: '456' }); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .stateChange(game, { personId: 'cookie' }, { gameManager: gameManager, requestingSocketId: '123' }); + expect(mockSocket.join).toHaveBeenCalledWith(game.accessCode); + expect(game.people.find(p => p.socketId === '123')).not.toBeNull(); + }); + it('should find the matching person and should NOT update their socketId if it is NOT different', async () => { + const mockSocket = { join: () => {} }; + spyOn(mockSocket, 'join').and.callThrough(); + namespace.sockets.set('123', mockSocket); + game.people.push({ cookie: 'cookie', socketId: '123' }); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .stateChange(game, { personId: 'cookie' }, { gameManager: gameManager, requestingSocketId: '123' }); + expect(mockSocket.join).not.toHaveBeenCalled(); + expect(game.people.find(p => p.socketId === '123')).not.toBeNull(); + }); + }); + describe('communicate', () => { + it('should do nothing if the client is not expecting an acknowledgement', async () => { + game.people.push({ cookie: 'cookie', socketId: '456' }); + const mockSocket = { join: () => {} }; + namespace.sockets.set('456', mockSocket); + spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .communicate(game, { personId: 'cookie' }, { ackFn: null }); + expect(GameStateCurator.getGameStateFromPerspectiveOfPerson).not.toHaveBeenCalled(); + }); + it('should acknowledge the client with null if a matching person was not found', async () => { + game.people.push({ cookie: 'differentCookie', socketId: '456' }); + const mockSocket = { join: () => {} }; + namespace.sockets.set('456', mockSocket); + const vars = { ackFn: () => {}, gameManager: gameManager }; + spyOn(vars, 'ackFn').and.callThrough(); + spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .communicate(game, { personId: 'cookie' }, vars); + expect(GameStateCurator.getGameStateFromPerspectiveOfPerson).not.toHaveBeenCalled(); + expect(vars.ackFn).toHaveBeenCalledWith(null); + }); + it('should acknowledge the client with null if a matching person was found, but the socket is not connected' + + ' to the namespace', async () => { + game.people.push({ cookie: 'cookie', socketId: '456' }); + const mockSocket = { join: () => {} }; + namespace.sockets.set('123', mockSocket); + const vars = { ackFn: () => {}, gameManager: gameManager }; + spyOn(vars, 'ackFn').and.callThrough(); + spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .communicate(game, { personId: 'cookie' }, vars); + expect(GameStateCurator.getGameStateFromPerspectiveOfPerson).not.toHaveBeenCalled(); + expect(vars.ackFn).toHaveBeenCalledWith(null); + }); + it('should acknowledge the client with the game state if they are found and their socket is connected', async () => { + game.people.push({ cookie: 'cookie', socketId: '456' }); + const mockSocket = { join: () => {} }; + namespace.sockets.set('456', mockSocket); + const vars = { ackFn: () => {}, gameManager: gameManager }; + spyOn(vars, 'ackFn').and.callThrough(); + spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.FETCH_GAME_STATE) + .communicate(game, { personId: 'cookie' }, vars); + expect(GameStateCurator.getGameStateFromPerspectiveOfPerson).toHaveBeenCalled(); + expect(vars.ackFn).toHaveBeenCalled(); + }); + }); + }); + + describe(EVENT_IDS.START_GAME, () => { + describe('stateChange', () => { + it('should start the game', async () => { + game.isFull = 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 () => { + 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.hasTimer = true; + game.timerParams = {}; + spyOn(timerManager, 'runTimer').and.callFake((a, b) => {}); + await Events.find((e) => e.id === EVENT_IDS.START_GAME) + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager }); + expect(game.status).toEqual(STATUS.IN_PROGRESS); + expect(game.timerParams.paused).toEqual(true); + expect(timerManager.runTimer).toHaveBeenCalled(); + }); + }); + describe('communicate', () => { + it('should communicate the start event to the room', async () => { + await Events.find((e) => e.id === EVENT_IDS.START_GAME) + .communicate(game, {}, { gameManager: gameManager }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith(EVENT_IDS.START_GAME); + }); + it('should communicate the start event to the room and acknowledge the client', async () => { + const vars = { ackFn: () => {}, gameManager: gameManager }; + spyOn(vars, 'ackFn').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.START_GAME) + .communicate(game, {}, vars); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith(EVENT_IDS.START_GAME); + expect(vars.ackFn).toHaveBeenCalled(); + }); + }); + }); + describe(EVENT_IDS.KILL_PLAYER, () => { + describe('stateChange', () => { + it('should kill the indicated player', async () => { + await Events.find((e) => e.id === EVENT_IDS.KILL_PLAYER) + .stateChange(game, { personId: 'b' }, { gameManager: gameManager }); + const person = game.people.find(p => p.id === 'b'); + expect(person.userType).toEqual(USER_TYPES.KILLED_PLAYER); + expect(person.out).toBeTrue(); + expect(person.killed).toBeTrue(); + }); + it('should not kill the player if they are already out', async () => { + await Events.find((e) => e.id === EVENT_IDS.KILL_PLAYER) + .stateChange(game, { personId: 'a' }, { gameManager: gameManager }); + const person = game.people.find(p => p.id === 'a'); + expect(person.userType).toEqual(USER_TYPES.MODERATOR); + expect(person.out).toBeTrue(); + expect(person.killed).toBeFalse(); + }); + }); + describe('communicate', () => { + it('should communicate the killed player to the room', async () => { + await Events.find((e) => e.id === EVENT_IDS.KILL_PLAYER) + .communicate(game, { personId: 'b' }, { gameManager: gameManager }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith( + EVENT_IDS.KILL_PLAYER, + 'b' + ); + }); + }); + }); + describe(EVENT_IDS.REVEAL_PLAYER, () => { + describe('stateChange', () => { + it('should reveal the indicated player', async () => { + await Events.find((e) => e.id === EVENT_IDS.REVEAL_PLAYER) + .stateChange(game, { personId: 'b' }, { gameManager: gameManager }); + const person = game.people.find(p => p.id === 'b'); + expect(person.userType).toEqual(USER_TYPES.PLAYER); + expect(person.out).toBeFalse(); + expect(person.killed).toBeFalse(); + expect(person.revealed).toBeTrue(); + }); + }); + describe('communicate', () => { + it('should communicate the killed player to the room', async () => { + await Events.find((e) => e.id === EVENT_IDS.REVEAL_PLAYER) + .communicate(game, { personId: 'b' }, { gameManager: gameManager }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith( + EVENT_IDS.REVEAL_PLAYER, + { id: 'b', gameRole: 'Villager', alignment: 'good' } + ); + }); + }); + }); + describe(EVENT_IDS.END_GAME, () => { + describe('stateChange', () => { + it('should end the game and reveal all players', async () => { + await Events.find((e) => e.id === EVENT_IDS.END_GAME) + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); + expect(game.status).toEqual(STATUS.ENDED); + expect(game.people.find(p => p.id === 'b').revealed).toBeTrue(); + }); + it('should end the game and kill the associated timer thread', async () => { + game.hasTimer = true; + timerManager.timerThreads = { ABCD: { kill: () => {} } }; + spyOn(timerManager.timerThreads.ABCD, 'kill').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.END_GAME) + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager, logger: { trace: () => {} } }); + expect(game.status).toEqual(STATUS.ENDED); + expect(game.people.find(p => p.id === 'b').revealed).toBeTrue(); + expect(timerManager.timerThreads.ABCD.kill).toHaveBeenCalled(); + }); + }); + describe('communicate', () => { + it('should communicate the end event to the room', async () => { + await Events.find((e) => e.id === EVENT_IDS.END_GAME) + .communicate(game, {}, { gameManager: gameManager }); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith(EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); + }); + it('should communicate the end event to the room and acknowledge the client', async () => { + const vars = { ackFn: () => {}, gameManager: gameManager }; + spyOn(vars, 'ackFn').and.callThrough(); + await Events.find((e) => e.id === EVENT_IDS.END_GAME) + .communicate(game, {}, vars); + expect(namespace.in).toHaveBeenCalledWith(game.accessCode); + expect(namespace.in().emit).toHaveBeenCalledWith(EVENT_IDS.END_GAME, GameStateCurator.mapPeopleForModerator(game.people)); + expect(vars.ackFn).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/unit/server/modules/GameManager_Spec.js b/spec/unit/server/modules/GameManager_Spec.js index 205a011..4ee9c92 100644 --- a/spec/unit/server/modules/GameManager_Spec.js +++ b/spec/unit/server/modules/GameManager_Spec.js @@ -5,12 +5,13 @@ const USER_TYPES = globals.USER_TYPES; const STATUS = globals.STATUS; const Person = require('../../../../server/model/Person'); const GameManager = require('../../../../server/modules/singletons/GameManager.js'); +const TimerManager = require('../../../../server/modules/singletons/TimerManager.js'); +const EventManager = require('../../../../server/modules/singletons/EventManager.js'); const GameStateCurator = require('../../../../server/modules/GameStateCurator.js'); -const ActiveGameRunner = require('../../../../server/modules/singletons/TimerManager.js'); const logger = require('../../../../server/modules/Logger.js')(false); describe('GameManager', () => { - let gameManager, namespace, socket; + let gameManager, timerManager, eventManager, namespace, socket, game; beforeAll(() => { spyOn(logger, 'debug'); @@ -19,346 +20,79 @@ describe('GameManager', () => { const inObj = { emit: () => {} }; namespace = { in: () => { return inObj; }, to: () => { return inObj; } }; socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; - gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, new ActiveGameRunner(logger)); + gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test'); + timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); + eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test'); + eventManager.publisher = { publish: async (...a) => {} } + gameManager.eventManager = eventManager; + gameManager.timerManager = timerManager; gameManager.setGameSocketNamespace(namespace); + spyOn(gameManager, 'refreshGame').and.callFake(async () => {}); + spyOn(eventManager.publisher, 'publish').and.callFake(async () => {}); }); beforeEach(() => { spyOn(namespace, 'to').and.callThrough(); spyOn(socket, 'to').and.callThrough(); - }); - - describe('#transferModerator', () => { - it('Should transfer successfully from a dedicated moderator to a killed player', () => { - const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.KILLED_PLAYER); - personToTransferTo.socketId = 'socket1'; - personToTransferTo.out = true; - const moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR); - moderator.socketId = 'socket2'; - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [personToTransferTo, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)], - [], - false, - moderator, - true, - moderator.id, - new Date().toJSON() - ); - gameManager.transferModeratorPowers(socket, game, personToTransferTo, namespace, logger); - - expect(game.moderator).toEqual(personToTransferTo); - expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR); - expect(moderator.userType).toEqual(USER_TYPES.SPECTATOR); - expect(namespace.to).toHaveBeenCalledWith(personToTransferTo.socketId); - expect(namespace.to).toHaveBeenCalledWith(game.moderator.socketId); - }); - - it('Should transfer successfully from a dedicated moderator to a spectator', () => { - const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.SPECTATOR); - personToTransferTo.socketId = 'socket1'; - const moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR); - moderator.socketId = 'socket2'; - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [new Person('2', '456', 'Jane', USER_TYPES.PLAYER)], - [], - false, - moderator, - true, - moderator.id, - new Date().toJSON() - ); - game.spectators.push(personToTransferTo); - gameManager.transferModeratorPowers(socket, game, personToTransferTo, namespace, logger); - - expect(game.moderator).toEqual(personToTransferTo); - expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR); - expect(moderator.userType).toEqual(USER_TYPES.SPECTATOR); - expect(namespace.to).toHaveBeenCalledWith(personToTransferTo.socketId); - expect(namespace.to).toHaveBeenCalledWith(game.moderator.socketId); - }); - - it('Should transfer successfully from a temporary moderator to a killed player', () => { - const personToTransferTo = new Person('1', '123', 'Joe', USER_TYPES.KILLED_PLAYER); - personToTransferTo.out = true; - personToTransferTo.socketId = 'socket1'; - const tempMod = new Person('3', '789', 'Jack', USER_TYPES.TEMPORARY_MODERATOR); - tempMod.socketId = 'socket2'; - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [personToTransferTo, tempMod, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)], - [], - false, - tempMod, - false, - tempMod.id, - new Date().toJSON() - ); - gameManager.transferModeratorPowers(socket, game, personToTransferTo, namespace, logger); - - expect(game.moderator).toEqual(personToTransferTo); - expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR); - expect(tempMod.userType).toEqual(USER_TYPES.PLAYER); - expect(namespace.to).toHaveBeenCalledWith(personToTransferTo.socketId); - expect(namespace.to).toHaveBeenCalledWith(game.moderator.socketId); - }); - - it('Should make the temporary moderator a dedicated moderator when they take themselves out of the game', () => { - const tempMod = new Person('3', '789', 'Jack', USER_TYPES.TEMPORARY_MODERATOR); - tempMod.socketId = 'socket1'; - const personToTransferTo = tempMod; - tempMod.out = true; - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [personToTransferTo, tempMod, new Person('2', '456', 'Jane', USER_TYPES.PLAYER)], - [], - false, - tempMod, - true, - tempMod.id, - new Date().toJSON() - ); - gameManager.transferModeratorPowers(socket, game, personToTransferTo, namespace, logger); - - expect(game.moderator).toEqual(personToTransferTo); - expect(personToTransferTo.userType).toEqual(USER_TYPES.MODERATOR); - expect(tempMod.userType).toEqual(USER_TYPES.MODERATOR); - expect(namespace.to).toHaveBeenCalledOnceWith(personToTransferTo.socketId); - expect(socket.to).toHaveBeenCalledWith(game.accessCode); - }); - }); - - describe('#killPlayer', () => { - it('Should mark a player as out and broadcast it, and should not transfer moderators if the moderator is a dedicated mod.', () => { - spyOn(namespace.in(), 'emit'); - spyOn(gameManager, 'transferModeratorPowers'); - const player = new Person('1', '123', 'Joe', USER_TYPES.PLAYER); - const mod = new Person('2', '456', 'Jane', USER_TYPES.MODERATOR); - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [player], - [], - false, - mod, - true, - mod.id, - new Date().toJSON() - ); - gameManager.killPlayer(socket, game, player, namespace, logger); - - expect(player.out).toEqual(true); - expect(player.userType).toEqual(USER_TYPES.KILLED_PLAYER); - expect(namespace.in().emit).toHaveBeenCalled(); - expect(gameManager.transferModeratorPowers).not.toHaveBeenCalled(); - }); - - it('Should mark a temporary moderator as out but preserve their user type, and call the transfer mod function', () => { - spyOn(namespace.in(), 'emit'); - spyOn(gameManager, 'transferModeratorPowers'); - const tempMod = new Person('1', '123', 'Joe', USER_TYPES.TEMPORARY_MODERATOR); - const game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [tempMod], - [], - false, - tempMod, - true, - tempMod.id, - new Date().toJSON() - ); - gameManager.killPlayer(socket, game, tempMod, namespace, logger); - - expect(tempMod.out).toEqual(true); - expect(tempMod.userType).toEqual(USER_TYPES.TEMPORARY_MODERATOR); - expect(namespace.in().emit).not.toHaveBeenCalled(); - expect(gameManager.transferModeratorPowers).toHaveBeenCalled(); - }); - }); - - describe('#handleRequestForGameState', () => { - let gameRunner, mod, player; - - beforeEach(() => { - mod = new Person('2', '456', 'Jane', USER_TYPES.MODERATOR); - player = new Person('1', '123', 'Joe', USER_TYPES.PLAYER); - gameRunner = { - activeGames: new Map([ - ['abc', new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [player], - [], - false, - mod, - true, - mod.id, - new Date().toJSON()) - ] - ]) - }; - }); - - it('should send the game state to a matching person with an active connection to the room', () => { - const socket = { id: 'socket1' }; - spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson'); - player.socketId = 'socket1'; - spyOn(namespace.in(), 'emit'); - gameManager.handleRequestForGameState( - gameRunner.activeGames.get('abc'), - namespace, - logger, - gameRunner, - 'abc', - '123', - (arg) => {}, - socket - ); - - expect(GameStateCurator.getGameStateFromPerspectiveOfPerson) - .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player); - }); - - it('should send the game state to a matching person who reset their connection', () => { - const socket = { id: 'socket_222222', join: () => {} }; - spyOn(socket, 'join'); - spyOn(GameStateCurator, 'getGameStateFromPerspectiveOfPerson'); - player.socketId = 'socket_111111'; - spyOn(namespace.in(), 'emit'); - gameManager.handleRequestForGameState( - gameRunner.activeGames.get('abc'), - namespace, - logger, - gameRunner, - 'abc', - '123', - (arg) => {}, - socket - ); - - expect(GameStateCurator.getGameStateFromPerspectiveOfPerson) - .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player); - expect(player.socketId).toEqual(socket.id); - expect(socket.join).toHaveBeenCalled(); - }); + timerManager.timerThreads = {}; + game = new Game( + '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 }], + false, + 'a', + true, + 'a', + new Date().toJSON(), + null + ); + game.currentModeratorId = 'a'; }); describe('#joinGame', () => { - let game, person, moderator; - beforeEach(() => { - person = new Person('1', '123', 'Placeholder', USER_TYPES.KILLED_PLAYER); - moderator = new Person('3', '789', 'Jack', USER_TYPES.MODERATOR); - game = new Game( - 'abc', - globals.STATUS.IN_PROGRESS, - [person], - [], - false, - moderator, - true, - moderator.id, - new Date().toJSON() - ); - }); - - it('should mark the game as full when all players have been assigned', () => { - moderator.assigned = true; - - gameManager.joinGame(game, 'Jill', 'x'); + it('should mark the game as full when all players have been assigned', async () => { + await gameManager.joinGame(game, 'Jill', 'x'); expect(game.isFull).toEqual(true); - expect(game.people[0].name).toEqual('Jill'); - expect(game.people[0].assigned).toEqual(true); }); it('should create a spectator if the game is already full and broadcast it to the room', () => { - moderator.assigned = true; - person.assigned = true; + game.people.find(p => p.id === 'b').assigned = true; game.isFull = true; spyOn(gameManager.namespace.in(), 'emit'); gameManager.joinGame(game, 'Jane', 'x'); expect(game.isFull).toEqual(true); - expect(game.people[0].name).toEqual('Placeholder'); - expect(game.moderator.name).toEqual('Jack'); - expect(game.spectators.length).toEqual(1); - expect(game.spectators[0].name).toEqual('Jane'); - expect(game.spectators[0].userType).toEqual(USER_TYPES.SPECTATOR); - expect(gameManager.namespace.in().emit).toHaveBeenCalledWith(globals.EVENTS.UPDATE_SPECTATORS, jasmine.anything()); - }); - }); - - describe('#generateAccessCode', () => { - it('should continue to generate access codes up to the max attempts when the generated code is already in use by another game', () => { - gameManager.activeGameRunner.activeGames = new Map([['AAAA', {}]]); - - const accessCode = gameManager.generateAccessCode(['A']); - expect(accessCode).toEqual(null); // we might the max generation attempts of 50. - }); - - it('should generate and return a unique access code', () => { - gameManager.activeGameRunner.activeGames = new Map([['AAAA', {}]]); - - const accessCode = gameManager.generateAccessCode(['B']); - expect(accessCode).toEqual('BBBB'); + expect(game.people.filter(p => p.userType === USER_TYPES.SPECTATOR).length).toEqual(1); }); }); describe('#restartGame', () => { - let person1, - person2, - person3, - shuffleSpy, - game, - moderator; + let shuffleSpy; + 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 () => { + game.status = STATUS.ENDED; + let player = game.people.find(p => p.id === 'b'); + player.userType = USER_TYPES.KILLED_PLAYER; + player.killed = true; + player.out = true; 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(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); }); @@ -366,114 +100,38 @@ describe('GameManager', () => { 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: () => {} } }; + timerManager.timerThreads = { 'ABCD': { kill: () => {} } }; + game.status = STATUS.ENDED; - const threadKillSpy = spyOn(gameManager.activeGameRunner.timerThreads.test, 'kill'); - const runGameSpy = spyOn(gameManager.activeGameRunner, 'runGame').and.stub(); + 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.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(runTimerSpy).toHaveBeenCalled(); expect(shuffleSpy).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME); }); - it('should reset all relevant game parameters and preserve temporary moderator', async () => { + it('should reset all relevant game parameters and create a temporary moderator', async () => { const emitSpy = spyOn(namespace.in(), 'emit'); + game.currentModeratorId = 'b'; + game.people.find(p => p.id === 'a').userType = USER_TYPES.SPECTATOR; game.moderator = game.people[0]; - game.moderator.userType = USER_TYPES.TEMPORARY_MODERATOR; + game.people.find(p => p.id === 'b').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(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); }); - - 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.EVENT_IDS.RESTART_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.EVENT_IDS.RESTART_GAME); - }); - }); - - describe('#pruneStaleGames', () => { - it('delete a game if it was created more than 24 hours ago', () => { - const moreThan24HoursAgo = new Date(); - moreThan24HoursAgo.setDate(moreThan24HoursAgo.getDate() - 1); - moreThan24HoursAgo.setHours(moreThan24HoursAgo.getHours() - 1); - gameManager.activeGameRunner.activeGames = new Map([['AAAA', { createTime: moreThan24HoursAgo.toJSON() }]]); - - gameManager.pruneStaleGames(); - - expect(gameManager.activeGameRunner.activeGames.size).toEqual(0); - }); - - it('should not delete a game if it was not created more than 24 hours ago', () => { - const lessThan24HoursAgo = new Date(); - lessThan24HoursAgo.setHours(lessThan24HoursAgo.getHours() - 23); - gameManager.activeGameRunner.activeGames = new Map([['AAAA', { createTime: lessThan24HoursAgo.toJSON() }]]); - - gameManager.pruneStaleGames(); - - expect(gameManager.activeGameRunner.activeGames.size).toEqual(1); - }); }); });