Files
Werewolf/server/modules/singletons/GameManager.js
2023-08-13 17:51:41 -04:00

427 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
STATUS,
PRIMITIVES,
ERROR_MESSAGES,
GAME_PROCESS_COMMANDS,
USER_TYPES,
EVENT_IDS,
REDIS_CHANNELS
} = require('../../config/globals');
const Game = require('../../model/Game');
const Person = require('../../model/Person');
const GameStateCurator = require('../GameStateCurator');
const UsernameGenerator = require('../UsernameGenerator');
const GameCreationRequest = require('../../model/GameCreationRequest');
class GameManager {
constructor (logger, environment, instanceId) {
if (GameManager.instance) {
throw new Error('The server tried to instantiate more than one GameManager');
}
logger.info('CREATING SINGLETON GAME MANAGER');
this.logger = logger;
this.environment = environment;
this.timerManager = null;
this.eventManager = null;
this.namespace = null;
this.instanceId = instanceId;
GameManager.instance = this;
}
getActiveGame = async (accessCode) => {
const r = await this.eventManager.publisher.get(accessCode);
if (r === null && this.timerManager.timerThreads[accessCode]) {
if (!this.timerManager.timerThreads[accessCode].killed) {
this.timerManager.timerThreads[accessCode].kill();
}
delete this.timerManager.timerThreads[accessCode];
}
let activeGame;
if (r !== null) {
try {
activeGame = JSON.parse(r);
} catch (e) {
this.logger.error('ERROR PARSING ACTIVE GAME: ' + e);
}
}
return r === null ? r : activeGame;
}
setGameSocketNamespace = (namespace) => {
this.namespace = namespace;
};
refreshGame = async (game) => {
this.logger.debug('PUSHING REFRESH OF ' + game.accessCode);
await this.eventManager.publisher.set(game.accessCode, JSON.stringify(game), {
KEEPTTL: true,
XX: true // only set the key if it already exists
});
}
createGame = async (gameParams) => {
this.logger.debug('Received request to create new game.');
return GameCreationRequest.validate(gameParams).then(async () => {
const req = new GameCreationRequest(
gameParams.deck,
gameParams.hasTimer,
gameParams.timerParams,
gameParams.moderatorName,
gameParams.hasDedicatedModerator,
gameParams.isTestGame
);
const newAccessCode = await this.generateAccessCode(PRIMITIVES.ACCESS_CODE_CHAR_POOL);
if (newAccessCode === null) {
return Promise.reject(ERROR_MESSAGES.NO_UNIQUE_ACCESS_CODE);
}
const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator);
moderator.assigned = true;
if (req.timerParams !== null) {
req.timerParams.paused = true;
req.timerParams.timeRemaining = convertFromHoursToMilliseconds(req.timerParams.hours) +
convertFromMinutesToMilliseconds(req.timerParams.minutes);
}
const newGame = new Game(
newAccessCode,
STATUS.LOBBY,
null,
req.deck,
req.hasTimer,
moderator.id,
req.hasDedicatedModerator,
moderator.id,
new Date().toJSON(),
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: PRIMITIVES.STALE_GAME_SECONDS
});
return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment });
}).catch((message) => {
console.error(message);
this.logger.error('Received invalid request to create new game.');
return Promise.reject(message);
});
};
pauseTimer = async (game, logger) => {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: GAME_PROCESS_COMMANDS.PAUSE_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
}
};
resumeTimer = async (game, logger) => {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: GAME_PROCESS_COMMANDS.RESUME_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
}
};
getTimeRemaining = async (game, socketId) => {
if (socketId) {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && (!thread.killed && thread.exitCode === null)) {
thread.send({
command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
accessCode: game.accessCode,
socketId: socketId,
logLevel: this.logger.logLevel
});
} else if (thread) {
if (game.timerParams && game.timerParams.timeRemaining === 0) {
this.namespace.to(socketId).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
}
}
}
};
checkAvailability = async (code) => {
const game = await this.getActiveGame(code.toUpperCase().trim());
if (game) {
return Promise.resolve({ accessCode: code, playerCount: game.gameSize, timerParams: game.timerParams });
} else {
return Promise.resolve(404);
}
};
generateAccessCode = async (charPool) => {
const charCount = charPool.length;
let codeDigits, accessCode;
let attempts = 0;
while (!accessCode || ((await this.eventManager.publisher.keys('*')).includes(accessCode)
&& attempts < PRIMITIVES.ACCESS_CODE_GENERATION_ATTEMPTS)) {
codeDigits = [];
let iterations = PRIMITIVES.ACCESS_CODE_LENGTH;
while (iterations > 0) {
iterations --;
codeDigits.push(charPool[getRandomInt(charCount)]);
}
accessCode = codeDigits.join('');
attempts ++;
}
return (await this.eventManager.publisher.keys('*')).includes(accessCode)
? null
: accessCode;
};
joinGame = async (game, name, cookie, joinAsSpectator) => {
const matchingPerson = this.findPersonByField(game, 'cookie', cookie);
if (matchingPerson) {
return Promise.resolve(matchingPerson.cookie);
}
if (isNameTaken(game, name)) {
return Promise.reject({ status: 400, reason: 'This name is taken.' });
}
if (joinAsSpectator
&& game.people.filter(person => person.userType === USER_TYPES.SPECTATOR).length === PRIMITIVES.MAX_SPECTATORS
) {
return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' });
} else if (joinAsSpectator || this.isGameStartable(game)) {
return await addSpectator(game, name, this.logger, this.namespace, this.eventManager, this.instanceId, this.refreshGame);
}
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 {
newPlayer = new Person(
createRandomId(),
createRandomId(),
name,
USER_TYPES.PLAYER,
null,
null,
null,
game.isTestGame
);
newPlayer.assigned = true;
game.people.push(newPlayer);
}
game.isStartable = this.isGameStartable(game);
await this.refreshGame(game);
this.namespace.in(game.accessCode).emit(
EVENT_IDS.PLAYER_JOINED,
GameStateCurator.mapPerson(moderator || newPlayer),
game.isStartable
);
await this.eventManager.publisher?.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
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) => {
// kill any outstanding timer threads
const subProcess = this.timerManager.timerThreads[game.accessCode];
if (subProcess) {
if (!subProcess.killed) {
this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode);
this.timerManager.timerThreads[game.accessCode].kill();
}
this.logger.debug('Deleting reference to subprocess ' + subProcess.pid);
delete this.timerManager.timerThreads[game.accessCode];
}
for (let i = 0; i < game.people.length; i ++) {
if (game.people[i].userType === USER_TYPES.KILLED_PLAYER) {
game.people[i].userType = USER_TYPES.PLAYER;
game.people[i].out = false;
}
if (game.people[i].userType === USER_TYPES.KILLED_BOT) {
game.people[i].userType = USER_TYPES.BOT;
game.people[i].out = false;
}
if (game.people[i].gameRole && game.people[i].id === game.currentModeratorId && game.people[i].userType === USER_TYPES.MODERATOR) {
game.people[i].userType = USER_TYPES.TEMPORARY_MODERATOR;
game.people[i].out = false;
}
game.people[i].revealed = false;
game.people[i].killed = false;
game.people[i].gameRole = null;
game.people[i].gameRoleDescription = null;
game.people[i].alignment = null;
game.people[i].customRole = null;
}
game.status = STATUS.LOBBY;
await this.refreshGame(game);
await this.eventManager.publisher?.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
this.eventManager.createMessageToPublish(
game.accessCode,
EVENT_IDS.RESTART_GAME,
this.instanceId,
'{}'
)
);
namespace.in(game.accessCode).emit(EVENT_IDS.RESTART_GAME);
};
/*
-- To shuffle an array a of n elements (indices 0..n-1):
for i from n1 downto 1 do
j ← random integer such that 0 ≤ j ≤ i
exchange a[j] and a[i]
*/
shuffle = (array) => {
for (let i = array.length - 1; i > 0; i --) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[j];
array[j] = array[i];
array[i] = temp;
}
return array;
};
deal = (game) => {
const cards = this.prepareDeck(game.deck);
let i = 0;
for (const person of game.people.filter(person => person.userType === USER_TYPES.PLAYER
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === USER_TYPES.BOT)
) {
person.gameRole = cards[i].role;
person.customRole = cards[i].custom;
person.gameRoleDescription = cards[i].description;
person.alignment = cards[i].team;
i ++;
}
}
isGameStartable = (game) => {
return game.people.filter(person => person.userType === USER_TYPES.PLAYER
|| person.userType === USER_TYPES.TEMPORARY_MODERATOR
|| person.userType === USER_TYPES.BOT).length === game.gameSize;
}
findPersonByField = (game, fieldName, value) => {
return game.people.find(person => person[fieldName] === value);
}
}
function getRandomInt (max) {
return Math.floor(Math.random() * Math.floor(max));
}
function initializeModerator (name, hasDedicatedModerator) {
const userType = hasDedicatedModerator
? USER_TYPES.MODERATOR
: USER_TYPES.TEMPORARY_MODERATOR;
return new Person(createRandomId(), createRandomId(), name, userType);
}
function initializePeopleForGame (uniqueRoles, moderator, shuffle, isTestGame, gameSize) {
const people = [];
if (isTestGame) {
let j = 0;
const number = moderator.userType === USER_TYPES.TEMPORARY_MODERATOR
? gameSize - 1
: gameSize;
while (j < number) {
const person = new Person(
createRandomId(),
createRandomId(),
UsernameGenerator.generate(),
USER_TYPES.BOT,
null,
null,
null,
isTestGame
);
people.push(person);
j ++;
}
}
people.push(moderator);
return people;
}
function createRandomId () {
let id = '';
for (let i = 0; i < PRIMITIVES.INSTANCE_ID_LENGTH; i ++) {
id += PRIMITIVES.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * PRIMITIVES.INSTANCE_ID_CHAR_POOL.length)];
}
return id;
}
function isNameTaken (game, name) {
const processedName = name.toLowerCase().trim();
return game.people.find((person) => person.name.toLowerCase().trim() === processedName);
}
async function addSpectator (game, name, logger, namespace, eventManager, instanceId, refreshGame) {
const spectator = new Person(
createRandomId(),
createRandomId(),
name,
USER_TYPES.SPECTATOR
);
spectator.assigned = true;
logger.trace('new spectator: ' + spectator.name);
game.people.push(spectator);
await refreshGame(game);
namespace.in(game.accessCode).emit(
EVENT_IDS.ADD_SPECTATOR,
GameStateCurator.mapPerson(spectator)
);
await eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
eventManager.createMessageToPublish(
game.accessCode,
EVENT_IDS.ADD_SPECTATOR,
instanceId,
JSON.stringify(GameStateCurator.mapPerson(spectator))
)
);
return Promise.resolve(spectator.cookie);
}
function convertFromMinutesToMilliseconds (minutes) {
return minutes * 60 * 1000;
}
function convertFromHoursToMilliseconds (hours) {
return hours * 60 * 60 * 1000;
}
module.exports = GameManager;