some game joining logic

This commit is contained in:
Alec
2021-11-13 01:21:22 -05:00
parent e3ec7beff0
commit 0c6203dec5
22 changed files with 596 additions and 116 deletions

View File

@@ -17,4 +17,20 @@ router.post('/create', function (req, res) {
});
});
router.get('/availability/:code', function (req, res) {
const joinGamePromise = gameManager.joinGame(req.params.code);
joinGamePromise.then((result) => {
if (result === 404) {
res.status(404).send();
} else if (result instanceof Error) {
res.status(400).send(result.message);
} else if (typeof result === "string") {
logger.debug(result);
res.status(200).send(result);
} else {
res.status(500).send();
}
});
});
module.exports = router;

View File

@@ -1,6 +1,22 @@
const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6,
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
TOGGLE_READY: 'toggleReady',
PROCESS_GUESS: 'processGuess'
},
STATUS: {
LOBBY: "lobby"
},
USER_SIGNATURE_LENGTH: 25,
USER_TYPES: {
MODERATOR: "moderator",
PLAYER: "player"
},
ERROR_MESSAGE: {
GAME_IS_FULL: "This game is full"
}
};
module.exports = globals;

View File

@@ -54,6 +54,9 @@ const io = socketIO(main);
app.set('port', port);
const inGame = io.of('/in-game');
/* Instantiate the singleton game manager */
const gameManager = new GameManager(logger).getInstance();
@@ -86,6 +89,10 @@ app.use(function (req, res) {
res.sendFile(path.join(__dirname, '../client/views/404.html'));
});
inGame.on('connection', function (socket) {
gameManager.addGameSocketHandlers(inGame, socket);
});
main.listen(port, function () {
logger.log(`Starting server on port ${port} http://localhost:${port}` );
});

11
server/model/Game.js Normal file
View File

@@ -0,0 +1,11 @@
class Game {
constructor(status, people, deck, hasTimer, timerParams=null) {
this.status = status;
this.people = people;
this.deck = deck;
this.hasTimer = hasTimer;
this.timerParams = timerParams;
}
}
module.exports = Game;

13
server/model/Person.js Normal file
View File

@@ -0,0 +1,13 @@
class Person {
constructor(id, name, userType, gameRole=null, gameRoleDescription=null, assigned=false) {
this.id = id;
this.socketId = null;
this.name = name;
this.userType = userType;
this.gameRole = gameRole;
this.gameRoleDescription = gameRoleDescription;
this.assigned = assigned;
}
}
module.exports = Person;

View File

@@ -0,0 +1,96 @@
const { fork } = require('child_process');
const path = require('path');
const logger = require('./logger')(false);
class ActiveGameRunner {
constructor () {
this.activeGames = {};
}
// runGame = (game, namespace, gameStateFn) => {
// logger.debug('running game ' + game.accessCode);
// const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
// gameProcess.on('message', (msg) => {
// switch (msg.command) {
// case serverGlobals.COMMAND.END_COUNTDOWN:
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: END COUNTDOWN');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_COUNTDOWN);
// gameProcess.send({
// command: serverGlobals.COMMAND.START_GAME,
// cycleNumber: game.words.length - 1,
// cycleLength: game.timePerWord * 1000,
// accessCode: game.accessCode
// });
// break;
// case serverGlobals.COMMAND.START_GAME:
// game.status = serverGlobals.GAME_STATE.STARTED;
// game.lastCycleTime = new Date().toJSON();
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: START GAME');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_GAME, {
// firstWord: game.words[0].baseword,
// gameLength: game.words.length,
// timePerWord: game.timePerWord * 1000
// });
// break;
// case serverGlobals.COMMAND.CYCLE_WORD:
// game.currentWordIndex += 1;
// game.lastCycleTime = new Date().toJSON();
// logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: CYCLE WORD');
// if (game.currentWordIndex < game.words.length) {
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.CYCLE_WORD, {
// word: game.words[game.currentWordIndex].baseword,
// index: game.currentWordIndex + 1,
// totalTime: game.timePerWord * 1000,
// gameLength: game.words.length
// });
// }
// gameProcess.send({
// command: serverGlobals.COMMAND.CYCLE_WORD,
// cycleIndex: game.currentWordIndex,
// cycleLength: game.timePerWord * 1000,
// accessCode: game.accessCode,
// gameLength: game.words.length
// });
// break;
// case serverGlobals.COMMAND.END_GAME:
// game.status = serverGlobals.GAME_STATE.ENDED;
// if (!game.posted) {
// logger.debug('GAME PARENT PROCESS: GAME ' + game.accessCode + ' HAS ENDED...BEGINNING POST TO DATABASE');
// this.postGameFn(game).then(() => {
// game.posted = true;
// logger.debug('GAME ' + game.accessCode + ' SUCCESSFULLY POSTED');
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_GAME, game.accessCode);
// });
// }
// break;
// }
// });
//
// gameProcess.on('exit', () => {
// if (this.activeGames[game.accessCode]) {
// delete this.activeGames[game.accessCode];
// logger.debug('GAME ' + game.accessCode + ' REMOVED FROM ACTIVE GAMES.');
// }
// });
// gameProcess.send({ command: serverGlobals.COMMAND.START_COUNTDOWN, accessCode: game.accessCode });
// game.status = serverGlobals.GAME_STATE.STARTING;
// game.startCountdownTime = new Date().toJSON();
// namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_COUNTDOWN);
// }
}
class Singleton {
constructor () {
if (!Singleton.instance) {
logger.log('CREATING SINGLETON ACTIVE GAME RUNNER');
Singleton.instance = new ActiveGameRunner();
}
}
getInstance () {
return Singleton.instance;
}
}
module.exports = Singleton;

View File

@@ -1,23 +1,56 @@
const globals = require('../config/globals');
const ActiveGameRunner = require('./ActiveGameRunner');
const Game = require('../model/Game');
const Person = require('../model/Person');
class GameManager {
constructor (logger) {
this.logger = logger;
//this.activeGameRunner = new ActiveGameRunner(this.postGame).getInstance();
this.activeGameRunner = new ActiveGameRunner().getInstance();
this.namespace = null;
//this.gameSocketUtility = GameSocketUtility;
}
addGameSocketHandlers = (namespace, socket) => {
this.namespace = namespace;
socket.on(globals.CLIENT_COMMANDS.FETCH_GAME_STATE, (accessCode, personId, ackFn) => {
handleRequestForGameState(this.namespace, this.logger, this.activeGameRunner, accessCode, personId, ackFn, socket);
});
}
createGame = (gameParams) => {
const expectedKeys = ['deck', 'hasTimer', 'timerParams'];
const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName'];
if (typeof gameParams !== 'object' || expectedKeys.some((key) => !Object.keys(gameParams).includes(key))) {
this.logger.error('Tried to create game with invalid options: ' + JSON.stringify(gameParams));
return Promise.reject('Tried to create game with invalid options: ' + gameParams);
} else {
const newAccessCode = this.generateAccessCode();
this.activeGameRunner.activeGames[newAccessCode] = new Game(
globals.STATUS.LOBBY,
initializePeopleForGame(gameParams.moderatorName, gameParams.deck),
gameParams.deck,
gameParams.hasTimer,
gameParams.timerParams
);
return Promise.resolve(newAccessCode);
}
}
joinGame = (code) => {
let game = this.activeGameRunner.activeGames[code];
if (game) {
let unassignedPerson = game.people.find((person) => person.assigned === false);
if (!unassignedPerson) {
return Promise.resolve(new Error(globals.ERROR_MESSAGE.GAME_IS_FULL));
} else {
return Promise.resolve(code);
}
} else {
return Promise.resolve(404);
}
}
generateAccessCode = () => {
const numLetters = globals.ACCESS_CODE_CHAR_POOL.length;
const codeDigits = [];
@@ -36,6 +69,49 @@ function getRandomInt (max) {
return Math.floor(Math.random() * Math.floor(max));
}
function initializeModerator(name) {
return new Person(createRandomUserId(), name, globals.USER_TYPES.MODERATOR)
}
function initializePeopleForGame(modName, uniqueCards) {
let people = [];
let cards = []; // this will contain copies of each card equal to the quantity.
people.push(initializeModerator(modName));
let numberOfRoles = 0;
for (let card of uniqueCards) {
for (let i = 0; i < card.quantity; i ++) {
cards.push(card);
numberOfRoles ++;
}
}
cards = shuffleArray(cards); // The deck should probably be shuffled, ey?.
for(let j = 0; j < numberOfRoles; j ++) {
people.push(new Person(createRandomUserId(), null, globals.USER_TYPES.PLAYER, cards[j].role, cards[j].description))
}
return people;
}
function shuffleArray (array) {
for (let i = 0; i < array.length; i++) {
const randIndex = Math.floor(Math.random() * i);
const temp = array[i];
array[i] = array[randIndex];
array[randIndex] = temp;
}
return array;
}
function createRandomUserId () {
let id = '';
for (let i = 0; i < globals.USER_SIGNATURE_LENGTH; i++) {
id += globals.ACCESS_CODE_CHAR_POOL[Math.floor(Math.random() * globals.ACCESS_CODE_CHAR_POOL.length)];
}
return id;
}
class Singleton {
constructor (logger) {
if (!Singleton.instance) {
@@ -49,4 +125,72 @@ class Singleton {
}
}
/* Since clients are anonymous, we have to rely to some extent on a cookie to identify them. Socket ids
are unique to a client, but they are re-generated if a client disconnects and then reconnects.
Thus, to have the most resilient identification i.e. to let them refresh, navigate away and come back,
get disconnected and reconnect, etc. we should have a combination of the socket id and the cookie. This
will also allow us to reject certain theoretical ways of breaking things, such as copying someone else's
cookie. Though if a client wants to clear their cookie and reset their connection, there's not much we can do.
The best thing in my opinion is to make it hard for clients to _accidentally_ break their experience.
*/
function handleRequestForGameState(namespace, logger, gameRunner, accessCode, personId, ackFn, socket) {
const game = gameRunner.activeGames[accessCode];
if (game) {
let matchingPerson = game.people.find((person) => person.id === personId);
if (matchingPerson) {
if (matchingPerson.socketId === socket.id) {
logger.debug("matching person found with an established connection to the room: " + matchingPerson.name);
ackFn(getGameStateFromPerspectiveOfPerson(game, matchingPerson));
} else {
if (!roomContainsSocketOfMatchingPerson(namespace, matchingPerson, logger, accessCode)) {
logger.debug("matching person found with a new connection to the room: " + matchingPerson.name);
socket.join(accessCode);
matchingPerson.socketId = socket.id;
ackFn(getGameStateFromPerspectiveOfPerson(game, matchingPerson));
} else {
rejectClientRequestForGameState(ackFn);
}
}
} else {
let personWithMatchingSocketId = findPersonWithMatchingSocketId(game.people, socket.id);
if (personWithMatchingSocketId) {
logger.debug("matching person found whose cookie got cleared after establishing a connection to the room: " + personWithMatchingSocketId.name);
ackFn(getGameStateFromPerspectiveOfPerson(game, personWithMatchingSocketId));
} else {
let unassignedPerson = game.people.find((person) => person.assigned === false);
if (unassignedPerson) {
logger.debug("completely new person with a first connection to the room: " + unassignedPerson.name);
socket.join(accessCode);
unassignedPerson.assigned = true;
unassignedPerson.socketId = socket.id;
ackFn(getGameStateFromPerspectiveOfPerson(game, unassignedPerson));
} else {
rejectClientRequestForGameState(ackFn);
}
}
}
} else {
rejectClientRequestForGameState(ackFn);
}
}
function getGameStateFromPerspectiveOfPerson(game, person) {
return person;
}
// in socket.io 2.x , the rooms property is an object. in 3.x and 4.x, it is a javascript Set.
function roomContainsSocketOfMatchingPerson(namespace, matchingPerson, logger, accessCode) {
return namespace.adapter
&& namespace.adapter.rooms[accessCode]
&& namespace.adapter.rooms[accessCode].sockets[matchingPerson.socketId];
}
function rejectClientRequestForGameState(acknowledgementFunction) {
return acknowledgementFunction(null);
}
function findPersonWithMatchingSocketId(people, socketId) {
return people.find((person) => person.socketId === socketId);
}
module.exports = Singleton;

View File

@@ -10,4 +10,9 @@ router.get('/create', function (request, response) {
response.sendFile(path.join(__dirname, '../../client/views/create.html'));
});
router.get('/game/:code', function (request, response) {
response.sendFile(path.join(__dirname, '../../client/views/game.html'));
});
module.exports = router;