redesign how players are added and how cards are dealt

This commit is contained in:
AlecM33
2023-08-03 18:08:33 -04:00
parent 24ae53209f
commit bcfb0afdd1
6 changed files with 118 additions and 143 deletions

View File

@@ -45,8 +45,8 @@ export const HTMLFragments = {
</select>
</div>`,
START_GAME_PROMPT:
`<button id='start-game-button'>Start Game</button>
<button id='edit-roles-button'>Edit Roles</button>`,
`<button id='edit-roles-button'>Edit Roles</button>
<button id='start-game-button'>Start Game</button>`,
GAME_CONTROL_PROMPT:
`<div id='game-control-prompt'>
<button id='end-game-button'>End Game</button>

View File

@@ -18,6 +18,11 @@ export class Lobby {
this.startGameHandler = (e) => {
e.preventDefault();
if (!stateBucket.currentGameState.isFull) {
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;
}
Confirmation('Start game and deal roles?', () => {
socket.timeout(5000).emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
@@ -217,6 +222,9 @@ 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
.filter(person => person.userType === globals.USER_TYPES.PLAYER
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === gameSize;
this.setLink(getTimeString(this.stateBucket.currentGameState));
this.setPlayerCount();
});
@@ -225,7 +233,7 @@ export class Lobby {
displayStartGamePromptForModerators () {
const existingPrompt = document.getElementById('start-game-prompt');
if (existingPrompt) {
enableOrDisableStartButton(this.stateBucket.currentGameState, existingPrompt, this.startGameHandler);
enableStartButton(existingPrompt, this.startGameHandler);
document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler);
} else {
const newPrompt = document.createElement('div');
@@ -233,7 +241,7 @@ export class Lobby {
newPrompt.innerHTML = HTMLFragments.START_GAME_PROMPT;
document.body.appendChild(newPrompt);
enableOrDisableStartButton(this.stateBucket.currentGameState, newPrompt, this.startGameHandler);
enableStartButton(newPrompt, this.startGameHandler);
document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler);
}
}
@@ -244,14 +252,9 @@ export class Lobby {
}
}
function enableOrDisableStartButton (gameState, buttonContainer, handler) {
if (gameState.isFull) {
buttonContainer.querySelector('#start-game-button').addEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.remove('disabled');
} else {
buttonContainer.querySelector('#start-game-button').removeEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.add('disabled');
}
function enableStartButton (buttonContainer, handler) {
buttonContainer.querySelector('#start-game-button').addEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.remove('disabled');
}
function activateLink (linkContainer, link) {

View File

@@ -641,7 +641,6 @@ label[for='moderator'] {
#start-game-button {
background-color: #1c8a36;
animation: shadow-pulse 1.5s infinite ease-out;
}
#end-game-button {

View File

@@ -82,7 +82,8 @@ router.patch('/:code/players', async function (req, res) {
gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => {
res.status(200).send({ cookie: data, environment: gameManager.environment });
}).catch((data) => {
res.status(data.status).send(data.reason);
console.error(data);
res.status(data.status || 500).send(data.reason);
});
} else {
res.status(404).send();

View File

@@ -1,6 +1,5 @@
const globals = require('../config/globals');
const GameStateCurator = require('./GameStateCurator');
const UsernameGenerator = require('./UsernameGenerator');
const GameCreationRequest = require('../model/GameCreationRequest');
const EVENT_IDS = globals.EVENT_IDS;
@@ -31,23 +30,8 @@ const Events = [
(person) => person.id === socketArgs.personId && person.assigned === true
);
if (toBeClearedIndex >= 0) {
const toBeCleared = game.people[toBeClearedIndex];
if (toBeCleared.userType === globals.USER_TYPES.SPECTATOR) {
game.people.splice(toBeClearedIndex, 1);
} else {
toBeCleared.assigned = false;
toBeCleared.socketId = null;
toBeCleared.cookie = (() => {
let id = '';
for (let i = 0; i < globals.INSTANCE_ID_LENGTH; i ++) {
id += globals.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * globals.INSTANCE_ID_CHAR_POOL.length)];
}
return id;
})();
toBeCleared.hasEnteredName = false;
toBeCleared.name = UsernameGenerator.generate();
game.isFull = vars.gameManager.isGameFull(game);
}
game.people.splice(toBeClearedIndex, 1);
game.isFull = vars.gameManager.isGameFull(game);
}
},
communicate: async (game, socketArgs, vars) => {
@@ -126,6 +110,7 @@ const Events = [
stateChange: async (game, socketArgs, vars) => {
if (game.isFull) {
game.status = globals.STATUS.IN_PROGRESS;
vars.gameManager.deal(game);
if (game.hasTimer) {
game.timerParams.paused = true;
await vars.timerManager.runTimer(game, vars.gameManager.namespace, vars.eventManager, vars.gameManager);

View File

@@ -76,7 +76,7 @@ class GameManager {
const newGame = new Game(
newAccessCode,
globals.STATUS.LOBBY,
initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame),
null,
req.deck,
req.hasTimer,
moderator.id,
@@ -86,6 +86,7 @@ class GameManager {
req.timerParams,
req.isTestGame
);
newGame.people = initializePeopleForGame(req.deck, moderator, this.shuffle, req.isTestGame, newGame.gameSize);
await this.eventManager.publisher.set(newAccessCode, JSON.stringify(newGame), {
EX: globals.STALE_GAME_SECONDS
});
@@ -142,7 +143,7 @@ class GameManager {
checkAvailability = async (code) => {
const game = await this.getActiveGame(code.toUpperCase().trim());
if (game) {
return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams });
return Promise.resolve({ accessCode: code, playerCount: game.gameSize, timerParams: game.timerParams });
} else {
return Promise.resolve(404);
}
@@ -180,40 +181,62 @@ 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) {
} else if (joinAsSpectator || this.isGameFull(game)) {
console.log('game is full');
return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame);
}
const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false
? this.findPersonByField(game, 'id', game.currentModeratorId)
: game.people.find((person) => person.assigned === false && person.userType === globals.USER_TYPES.PLAYER);
if (unassignedPerson) {
this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name);
unassignedPerson.assigned = true;
unassignedPerson.name = name;
game.isFull = this.isGameFull(game);
await this.refreshGame(game);
this.namespace.in(game.accessCode).emit(
globals.EVENTS.PLAYER_JOINED,
GameStateCurator.mapPerson(unassignedPerson),
game.isFull
);
await this.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.PLAYER_JOINED,
this.instanceId,
JSON.stringify(unassignedPerson)
)
);
return Promise.resolve(unassignedPerson.cookie);
let moderator, newPlayer;
const isModeratorJoining = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false;
if (isModeratorJoining) {
moderator = this.findPersonByField(game, 'id', game.currentModeratorId);
this.logger.trace('Moderator joining. Assigning: ' + name);
moderator.assigned = true;
moderator.name = name;
} else {
if (game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS) {
return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' });
}
return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame);
newPlayer = new Person(
createRandomId(),
createRandomId(),
name,
globals.USER_TYPES.PLAYER,
null,
null,
null,
game.isTestGame
);
newPlayer.assigned = true;
game.people.push(newPlayer);
}
};
game.isFull = this.isGameFull(game);
await this.refreshGame(game);
this.namespace.in(game.accessCode).emit(
globals.EVENTS.PLAYER_JOINED,
GameStateCurator.mapPerson(moderator || newPlayer),
game.isFull
);
await this.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
globals.EVENT_IDS.PLAYER_JOINED,
this.instanceId,
JSON.stringify(moderator || newPlayer)
)
);
return Promise.resolve(moderator?.cookie || newPlayer?.cookie);
}
prepareDeck (deck) {
const cards = [];
for (const card of deck) {
for (let i = 0; i < card.quantity; i ++) {
cards.push(card);
}
}
this.shuffle(cards);
return cards;
}
restartGame = async (game, namespace, status = globals.STATUS.IN_PROGRESS) => {
// kill any outstanding timer threads
@@ -227,17 +250,6 @@ class GameManager {
delete this.timerManager.timerThreads[game.accessCode];
}
// re-shuffle the deck
const cards = [];
for (const card of game.deck) {
for (let i = 0; i < card.quantity; i ++) {
cards.push(card);
}
}
this.shuffle(cards);
// make sure no players are marked as out or revealed, and give them new cards.
for (let i = 0; i < game.people.length; i ++) {
if (game.people[i].userType === globals.USER_TYPES.KILLED_PLAYER) {
game.people[i].userType = globals.USER_TYPES.PLAYER;
@@ -247,30 +259,19 @@ class GameManager {
game.people[i].userType = globals.USER_TYPES.BOT;
game.people[i].out = false;
}
if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) {
game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
game.people[i].out = false;
}
game.people[i].revealed = false;
game.people[i].killed = false;
if (game.people[i].gameRole) {
game.people[i].gameRole = cards[i].role;
game.people[i].gameRoleDescription = cards[i].description;
game.people[i].alignment = cards[i].team;
if (game.people[i].id === game.currentModeratorId && game.people[i].userType === globals.USER_TYPES.MODERATOR) {
game.people[i].userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
game.people[i].out = false;
}
}
game.people[i].gameRole = null;
game.people[i].gameRoleDescription = null;
game.people[i].alignment = null;
game.people[i].customRole = null;
}
if (status === globals.STATUS.IN_PROGRESS) {
game.status = globals.STATUS.IN_PROGRESS;
if (game.hasTimer) {
game.timerParams.paused = true;
game.timerParams.timeRemaining = convertFromHoursToMilliseconds(game.timerParams.hours) +
convertFromMinutesToMilliseconds(game.timerParams.minutes);
await this.timerManager.runTimer(game, namespace, this.eventManager, this);
}
} else {
game.status = globals.STATUS.LOBBY;
}
game.status = globals.STATUS.LOBBY;
await this.refreshGame(game);
await this.eventManager.publisher?.publish(
@@ -302,12 +303,24 @@ class GameManager {
return array;
};
deal = () => {
deal = (game) => {
const cards = this.prepareDeck(game.deck);
let i = 0;
for (const person of game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === globals.USER_TYPES.BOT)
) {
person.gameRole = cards[i].role;
person.customRole = cards[i].custom;
person.gameRoleDescription = cards[i].description;
person.alignment = cards[i].team;
i ++;
}
}
isGameFull = (game) => {
return !game.people.find((person) => person.userType === globals.USER_TYPES.PLAYER && person.assigned === false);
return game.people.filter(person => person.userType === globals.USER_TYPES.PLAYER
|| person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR).length === game.gameSize;
}
findPersonByField = (game, fieldName, value) => {
@@ -326,46 +339,29 @@ function initializeModerator (name, hasDedicatedModerator) {
return new Person(createRandomId(), createRandomId(), name, userType);
}
function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame) {
function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, gameSize) {
const people = [];
const cards = [];
for (const role of uniqueRoles) {
for (let i = 0; i < role.quantity; i ++) {
cards.push(role);
if (isTestGame) {
let j = 0;
const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
? gameSize - 1
: gameSize;
while (j < number) {
const person = new Person(
createRandomId(),
createRandomId(),
UsernameGenerator.generate(),
globals.USER_TYPES.BOT,
null,
null,
null,
isTestGame
);
people.push(person);
j ++;
}
}
shuffle(cards); // this shuffles in-place.
let j = 0;
const number = moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
? cards.length - 1
: cards.length;
while (j < number) {
const person = new Person(
createRandomId(),
createRandomId(),
UsernameGenerator.generate(),
isTestGame ? globals.USER_TYPES.BOT : globals.USER_TYPES.PLAYER,
cards[j].role,
cards[j].description,
cards[j].team,
isTestGame
);
person.customRole = cards[j].custom;
person.hasEnteredName = false;
people.push(person);
j ++;
}
if (moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
moderator.gameRole = cards[cards.length - 1].role;
moderator.customRole = cards[cards.length - 1].custom;
moderator.gameRoleDescription = cards[cards.length - 1].description;
moderator.alignment = cards[cards.length - 1].team;
}
people.push(moderator);
return people;
@@ -384,15 +380,6 @@ function isNameTaken (game, name) {
return game.people.find((person) => person.name.toLowerCase().trim() === processedName);
}
function getGameSize (cards) {
let quantity = 0;
for (const card of cards) {
quantity += card.quantity;
}
return quantity;
}
async function addSpectator (game, name, logger, namespace, eventManager, instanceId, refreshGame) {
const spectator = new Person(
createRandomId(),