Skip to content

Commit

Permalink
Merge pull request #164 from AlecM33/return-to-lobby
Browse files Browse the repository at this point in the history
Allow editing of the deck + allow players/spectators to leave/be kicked between games.
  • Loading branch information
AlecM33 committed Aug 5, 2023
2 parents 23d563f + f8752b3 commit 3d64c19
Show file tree
Hide file tree
Showing 44 changed files with 1,073 additions and 393 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ Find the latest production deployment at: https://play-werewolf.app/
- [Code Formatting](#code-formatting)

<p align="center">
<img src="https://user-images.githubusercontent.com/24642328/218271143-5df50c7f-3a11-4012-8f1a-f288373458f4.jpg" width="750"/>
<img src="./client/src/images/readme-collage.webp" width="750"/>
</p>

## Overview

An app to create and run games of <a href="https://en.wikipedia.org/wiki/Mafia_(party_game)">Werewolf (Mafia)</a> with your friends. No sign-up, installation, or payment required. A host builds a game and deals a role to everyone's device, and then the app keeps track of the game state (timer, who is killed/revealed, etc).
Since people tend to have their own preferences when it comes to what roles they use or how they run the game, the app tries to take a generalized, flexible, hands-off approach - it won't run day and night for you and won't implement any role abilities. Hosts can use any roles they want, in any configuration, and can create their own roles if the provided ones don't meet their needs.
An app to create and run games of <a href="https://en.wikipedia.org/wiki/Mafia_(party_game)">Werewolf (Mafia)</a> with your friends. No sign-up, installation, or payment required. A moderator creates a room and deals a role to everyone's device, and then the app keeps track of the game state (timer, who is killed/revealed, etc).
Since people tend to have their own preferences when it comes to what roles they use or how they run the game, the app tries to take a generalized, flexible, hands-off approach - it won't run day and night for you and won't implement any role abilities. Moderators can choose whether to be dealt in, and they can use any congifuration of roles they want, creating their own roles if needed.

The app prioritizes responsiveness. A key scenario would be when a group is hanging out with only their phones.

Expand All @@ -30,12 +30,13 @@ Inspired by my time playing <a href="https://boardgamegeek.com/boardgame/152242/

## Features

- hosts can build their own game for any player count using default roles, or custom roles that they create. They can include a timer, shared by everyone, that the moderator can play or pause.
- hosts can build their own game for any player count using default roles or custom roles that they create. They can include a timer, shared by everyone, that the moderator can play or pause.
- party members can join games easily via a shareable link, a QR code, or a 4-character code entered on the homepage.
- when hosts start the game, cards are dealt randomly and automatically.
- Players and spectators can freely leave the lobby, or the moderator can kick them. Roles can also be edited in the lobby. This should allow a room to be re-usable for several games, even if the player count changes
and the moderator needs to change the cards in the game.
- When a moderator starts a game, cards are dealt randomly and automatically.
- players can reference helpful info, such as descriptions of all the roles in the game, the time remaining (if the host has set a timer), and who has been killed or had their role revealed.
- Moderators have the option to be dealt into the game, and will have their moderator powers automatically delegated to whoever they first remove from the game. Moderators can also transfer their powers to players that have been killed or to people that are spectating.
- A specific game can be restarted at any time, which will reset the timer and re-deal roles to all players.
- The app is lightweight and loads fast.

## Tech Stack
Expand Down
10 changes: 8 additions & 2 deletions client/src/config/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ export const globals = {
ADD_SPECTATOR: 'addSpectator',
UPDATE_SPECTATORS: 'updateSpectators',
RESTART_GAME: 'restartGame',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod'
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
KICK_PERSON: 'kickPerson',
UPDATE_GAME_ROLES: 'updateGameRoles',
LEAVE_ROOM: 'leaveRoom'
},
TIMER_EVENTS: function () {
return [
Expand All @@ -66,7 +69,10 @@ export const globals = {
LOBBY_EVENTS: function () {
return [
this.EVENT_IDS.PLAYER_JOINED,
this.EVENT_IDS.ADD_SPECTATOR
this.EVENT_IDS.ADD_SPECTATOR,
this.EVENT_IDS.KICK_PERSON,
this.EVENT_IDS.UPDATE_GAME_ROLES,
this.EVENT_IDS.LEAVE_ROOM
];
},
IN_PROGRESS_EVENTS: function () {
Expand Down
1 change: 1 addition & 0 deletions client/src/images/3-vertical-dots-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/images/readme-collage.webp
Binary file not shown.
4 changes: 4 additions & 0 deletions client/src/images/save-svgrepo-com.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed client/src/images/tutorial/dedicated-mod.webp
Binary file not shown.
Binary file modified client/src/images/tutorial/mod-transfer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file removed client/src/images/tutorial/temporary-mod.webp
Binary file not shown.
41 changes: 26 additions & 15 deletions client/src/modules/front_end_components/HTMLFragments.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@ export const HTMLFragments = {
</select>
</div>`,
START_GAME_PROMPT:
`<div>
<button id='start-game-button'>Start Game</button>
<p>All players must join to start.</p>
</div>`,
`<button id='edit-roles-button'>Edit Roles</button>
<button id='start-game-button'>Start Game</button>`,
LEAVE_GAME_PROMPT:
'<button id=\'leave-game-button\'>Leave Room</button>',
GAME_CONTROL_PROMPT:
`<div id='game-control-prompt'>
<button id='end-game-button'>End Game</button>
</div>`,
ROLE_EDIT_BUTTONS:
`<button class="app-button cancel" id="cancel-role-changes-button">Cancel</button>
<button class="app-button" id="save-role-changes-button">
<p>Save</p><img src="../images/save-svgrepo-com.svg" alt='save'>
</button>`,
PLAYER_GAME_VIEW:
`<div id='game-header'>
<div>
Expand Down Expand Up @@ -139,6 +144,15 @@ export const HTMLFragments = {
<button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button>
</div>
</div>`,
PLAYER_OPTIONS_MODAL:
`<div id='player-options-modal-background' class='modal-background'></div>
<div tabindex='-1' id='player-options-modal' class='modal'>
<h2 id="player-options-modal-title"></h2>
<div id='player-options-modal-content'></div>
<div class='modal-button-container'>
<button id='close-player-options-modal-button' class='app-button cancel'>Close</button>
</div>
</div>`,
MODERATOR_GAME_VIEW:
`<div id='game-header'>
<div id='timer-container-moderator'>
Expand Down Expand Up @@ -296,9 +310,6 @@ export const HTMLFragments = {
<h2>&#x1F3C1; The moderator has ended the game. Roles are revealed.</h2>
<div id="end-of-game-buttons">
<button id='role-info-button' class='app-button'>Roles in This Game <img alt='Info icon' src='/images/info.svg'/></button>
<a href='/'>
<button class='app-button'>Go Home \uD83C\uDFE0</button>
</a>
</div>
</div>
<div id='game-people-container'>
Expand Down Expand Up @@ -355,21 +366,21 @@ export const HTMLFragments = {
</div>
<div class="role-options">
<img tabindex="0" class="role-include" src="images/add.svg" title="add one" alt="add one"/>
<img tabindex="0" class="role-info" src="images/info.svg" title="info" alt="info"/>
<img tabindex="0" class="role-edit" src="images/pencil.svg" title="edit" alt="edit"/>
<img tabindex="0" class="role-remove" src="images/delete.svg" title="remove" alt="remove"/>
<img tabindex="0" class="role-include" src="../images/add.svg" title="add one" alt="add one"/>
<img tabindex="0" class="role-info" src="../images/info.svg" title="info" alt="info"/>
<img tabindex="0" class="role-edit" src="../images/pencil.svg" title="edit" alt="edit"/>
<img tabindex="0" class="role-remove" src="../images/delete.svg" title="remove" alt="remove"/>
</div>`,
DECK_SELECT_ROLE_DEFAULT:
`<div class="role-name"></div>
<div class="role-options">
<img tabindex="0" class="role-include" src="images/add.svg" title="add one" alt="add one"/>
<img tabindex="0" class="role-info" src="images/info.svg" title="info" alt="info"/>
<img tabindex="0" class="role-include" src="../images/add.svg" title="add one" alt="add one"/>
<img tabindex="0" class="role-info" src="../images/info.svg" title="info" alt="info"/>
</div>`,
DECK_SELECT_ROLE_ADDED_TO_DECK:
`<div class="role-name"></div>
<div class="role-options">
<img tabindex="0" class="role-remove" src="images/remove.svg" title="remove one" alt="remove one"/>
<img tabindex="0" class="role-info" src="images/info.svg" title="info" alt="info"/>
<img tabindex="0" class="role-remove" src="../images/remove.svg" title="remove one" alt="remove one"/>
<img tabindex="0" class="role-info" src="../images/info.svg" title="info" alt="info"/>
</div>`
};
14 changes: 7 additions & 7 deletions client/src/modules/game_creation/GameCreationStepManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export class GameCreationStepManager {
showButtons(false, true, this.steps[step].forwardHandler, null);
break;
case 2:
this.renderRoleSelectionStep(this.currentGame, containerId, step, this.deckManager);
this.renderRoleSelectionStep(this.currentGame, containerId, step);
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
break;
case 3:
Expand All @@ -216,7 +216,7 @@ export class GameCreationStepManager {
document.getElementById('step-' + stepNumber)?.remove();
}

renderRoleSelectionStep = (game, containerId, step, deckManager) => {
renderRoleSelectionStep = (game, containerId, step) => {
const stepContainer = document.createElement('div');

setAttributes(stepContainer, { id: 'step-' + step, class: 'flex-row-container-left-align step' });
Expand All @@ -225,13 +225,13 @@ export class GameCreationStepManager {

document.getElementById(containerId).appendChild(stepContainer);

this.roleBox = new RoleBox(stepContainer, deckManager);
deckManager.roleBox = this.roleBox;
this.roleBox = new RoleBox(stepContainer, this.deckManager);
this.deckManager.roleBox = this.roleBox;
this.roleBox.loadDefaultRoles();
this.roleBox.loadCustomRolesFromCookies();
this.roleBox.displayDefaultRoles(document.getElementById('role-select'));

deckManager.loadDeckTemplates(this.roleBox);
this.deckManager.loadDeckTemplates(this.roleBox);

const exportHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
Expand Down Expand Up @@ -290,9 +290,9 @@ export class GameCreationStepManager {

document.getElementById('custom-role-hamburger').addEventListener('click', clickHandler);

deckManager.updateDeckStatus();
this.deckManager.updateDeckStatus();

initializeRemainingEventListeners(deckManager, this.roleBox);
initializeRemainingEventListeners(this.deckManager, this.roleBox);
};
}

Expand Down
28 changes: 26 additions & 2 deletions client/src/modules/game_creation/RoleBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export class RoleBox {
id: createRandomId(),
role: roleObj.role,
team: roleObj.team,
description: roleObj.description
description: roleObj.description,
custom: true
};
}); // we know it is valid JSON from the validate function
}
Expand All @@ -73,7 +74,8 @@ export class RoleBox {
id: createRandomId(),
role: roleObj.role,
team: roleObj.team,
description: roleObj.description
description: roleObj.description,
custom: true
};
}); // we know it is valid JSON from the validate function
const initialLength = this.customRoles.length;
Expand Down Expand Up @@ -107,6 +109,28 @@ export class RoleBox {
reader.readAsText(file);
}

loadSelectedRolesFromCurrentGame = (game) => {
for (const card of game.deck) {
if (card.quantity) {
for (let i = 0; i < card.quantity; i ++) {
if (!this.deckManager.hasRole(card.role)) {
const role = this.getDefaultRole(card.role)
? this.getDefaultRole(card.role)
: this.getCustomRole(card.role);
if (role) {
role.id = card.id;
this.deckManager.addToDeck(role);
}
} else {
this.deckManager.addCopyOfCard(card.role);
}
}
}
}

this.deckManager.updateDeckStatus();
}

// via https://stackoverflow.com/a/18197341
downloadCustomRoles = (filename, text) => {
const element = document.createElement('a');
Expand Down
2 changes: 1 addition & 1 deletion client/src/modules/game_state/states/Ended.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class Ended {
gameState.client.userType === globals.USER_TYPES.MODERATOR
|| gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
) {
document.getElementById('end-of-game-buttons').prepend(SharedStateUtil.createRestartButton(this.stateBucket));
document.getElementById('end-of-game-buttons').prepend(SharedStateUtil.createReturnToLobbyButton(this.stateBucket));
}
SharedStateUtil.displayCurrentModerator(this.stateBucket.currentGameState.people
.find((person) => person.userType === globals.USER_TYPES.MODERATOR
Expand Down
16 changes: 12 additions & 4 deletions client/src/modules/game_state/states/InProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class InProgress {
this.stateBucket.currentGameState.accessCode
);
setTimeout(() => {
if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING)) {
if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) {
document.getElementById('game-timer').innerText = 'Timer not found.';
document.getElementById('game-timer').classList.add('timer-error');
}
Expand All @@ -65,8 +65,16 @@ export class InProgress {
const spectatorCount = this.container.querySelector('#spectator-count');
const spectatorHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') {
Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR)), null, true);
Confirmation(
SharedStateUtil.buildSpectatorList(
this.stateBucket.currentGameState.people
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR),
this.stateBucket.currentGameState.client,
this.socket,
this.stateBucket.currentGameState),
null,
true
);
}
};

Expand Down Expand Up @@ -500,7 +508,7 @@ function createEndGamePromptComponent (socket, stateBucket) {
);
});
});
div.querySelector('#game-control-prompt').prepend(SharedStateUtil.createRestartButton(stateBucket));
div.querySelector('#game-control-prompt').prepend(SharedStateUtil.createReturnToLobbyButton(stateBucket));
document.getElementById('game-content').appendChild(div);
}
}
Expand Down
Loading

0 comments on commit 3d64c19

Please sign in to comment.