diff --git a/.gitignore b/.gitignore index 5a4c89c..2e1c742 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules server/server.toml server/built/* server/dist/* +server/resources/games #=============================================================================# # The files that were auto-generated into a .gitignore by Vue-cli diff --git a/server/src/events/CreateGame.ts b/server/src/events/CreateGame.ts index c0b309c..bd52466 100644 --- a/server/src/events/CreateGame.ts +++ b/server/src/events/CreateGame.ts @@ -1,20 +1,20 @@ +import { games, log } from '../main'; import { Game } from '../objects/Game'; import { Player } from '../objects/Player'; import { Server, Socket } from 'socket.io'; -import { conf, games, log } from '../main'; +import { routineCheck } from '../utils/cleanup'; export default (io: Server, socket: Socket, data: CreateGame) => { try { let host = new Player(data.name, socket, true); // Create the game object to save - let game = new Game(conf, host); + let game = new Game(host); games[game.id] = game; - game.players.push(host); game.log = log.getChildLogger({ displayLoggerName: true, name: game.id, - }) + }); game.log.info(`New game created (host=${host.name})`); socket.join(game.id); @@ -23,8 +23,12 @@ export default (io: Server, socket: Socket, data: CreateGame) => { game_code: game.id, players: game.playerData, }); + + // Check for any inactive games that are still marked as active + routineCheck(); } catch (err) { + log.prettyError(err); socket.emit(`GameCreated`, { status: 500, message: err.message, diff --git a/server/src/events/DeleteGame.ts b/server/src/events/DeleteGame.ts index 846195a..00b1624 100644 --- a/server/src/events/DeleteGame.ts +++ b/server/src/events/DeleteGame.ts @@ -33,9 +33,13 @@ export default (io: Server, socket: Socket, data: DeleteGame) => { // Delete game game.log.debug(`Game deleted.`) delete games[data.game_code]; - io.to(game.id).emit(`GameDeleted`, { status: 200 }); + io.to(game.id).emit(`GameDeleted`, { + status: 200, + message: `Game deleted by the host.` + }); } catch (err) { + log.prettyError(err); socket.emit(`GameDeleted`, { status: 500, message: err.message, diff --git a/server/src/events/GetHand.ts b/server/src/events/GetHand.ts index 4eb1ca7..9e39a8e 100644 --- a/server/src/events/GetHand.ts +++ b/server/src/events/GetHand.ts @@ -23,6 +23,7 @@ export default (io: Server, socket: Socket, data: GetHand) => { }); } catch (err) { + log.prettyError(err); socket.emit(`QuestionList`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/GetPastQuestions.ts b/server/src/events/GetPastQuestions.ts index 8a7100d..c9d7f60 100644 --- a/server/src/events/GetPastQuestions.ts +++ b/server/src/events/GetPastQuestions.ts @@ -24,6 +24,7 @@ export default (io: Server, socket: Socket, data: GetPastQuestions) => { }); } catch (err) { + log.prettyError(err); socket.emit(`PastQuestions`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/JoinGame.ts b/server/src/events/JoinGame.ts index a04e9ea..27a76be 100644 --- a/server/src/events/JoinGame.ts +++ b/server/src/events/JoinGame.ts @@ -1,9 +1,83 @@ -import { games, log } from '../main'; +import { readFileSync } from 'fs'; +import { Game } from '../objects/Game'; import { Player } from '../objects/Player'; import { Server, Socket } from 'socket.io'; +import { games, hibernatedGames, log, conf } from '../main'; export default (io: Server, socket: Socket, data: JoinGame) => { try { + // Check if the game is hibernated so that we can re-instantiate the + // Game object and bring it back to being alive + let hibernatedIndex = hibernatedGames.indexOf(data.game_code) + if (hibernatedIndex >= 0) { + log.info(`Recreating game from datastore.`); + + let datastore = JSON.parse(readFileSync( + `${conf.datastores.directory}/${data.game_code}.${conf.datastores.filetype}`, + `utf-8` + )) as datastoreGame; + let host = new Player(data.name, socket, true); + let game = Game.fromJSON(host, datastore); + game.log = log.getChildLogger({ + displayLoggerName: true, + name: game.id, + }); + + game.ingame = datastore.ingame; + + // Get the specific information for team + let playerData = datastore.players.find(p => p.name === data.name); + if (playerData) { + host.role = playerData.role; + host.team = playerData.team; + }; + + let hand: string[] = []; + if (host.team) { + let team = game.teams[host.team - 1]; + switch (host.role) { + case "guesser": + game.log.silly(`${host.name} is one of the team's guessers`); + hand = team.hand; + team.guessers.push(host); + socket.join([ + `${game.id}:*:guesser`, + `${game.id}:${team.id}:guesser` + ]); + break; + case "writer": + game.log.silly(`${host.name} is the team's writer`); + team.writer = host; + socket.join([ + `${game.id}:*:writer`, + `${game.id}:${team.id}:writer` + ]); + break; + }; + game.log.debug(`Host assigned to team`); + }; + + hibernatedGames.splice(hibernatedIndex, 1); + games[game.id] = game; + + game.log.info(`Successfully unhibernated`); + socket.join(game.id); + socket.emit(`GameRejoined`, { + status: 200, + ingame: game.ingame, + role: host.role, + team: host.team, + is_host: true, + players: game.playerData, + chosen_object: game.object, + hand: hand, + answers: { + team_1: game.teams[0].answers, + team_2: game.teams[1].answers, + }, + }); + return; + }; // Assert game exists if (!games[data.game_code]) { @@ -18,30 +92,45 @@ export default (io: Server, socket: Socket, data: JoinGame) => { let game = games[data.game_code]; - // Ensure no one has the same name as the player that is joining + /* + Ensure that if the socket is attempting to reconnect to the game, that + the player they are connecting to does not have actively connected + socket. This will also function as the main game joining for hibernated + games that were reloaded from disk. + */ let sameName = game.players.find(x => x.name == data.name); if (sameName != null) { - if (!game.ingame) { - game.log.info(`Client attempted to connect using name already in use.`); - socket.emit(`GameJoined`, { - status: 400, - message: `A player already has that name in the game.`, - source: `JoinGame` + + if (!sameName.socket?.connected) { + sameName.socket = socket; + game.log.info(`Player Reconnected to the game (name=${data.name})`); + + // Get the hand of the player's team + let hand: string[] = []; + if (sameName.team && sameName.role == `guesser`) { + hand = game.teams[sameName.team - 1].hand; + }; + + socket.emit(`GameRejoined`, { + status: 200, + ingame: game.ingame, + role: sameName.role, + team: sameName.team, + is_host: sameName.isHost, + players: game.playerData, + chosen_object: game.object, + answers: { + team_1: game.teams[0].answers, + team_2: game.teams[1].answers, + }, + hand: hand, }); return; - }; - - // Player has the same name but is allowed to rejoin if they - // disconnect in the middle of the game - if (!sameName.socket.connected) { - game.log.info(`Player Reconnected to the game (name=${data.name})`); - socket.emit(`GameRejoined`, { status: 200 }); - return; } else { - game.log.debug(`${socket.id} attempted to claim ${sameName.socket.id}'s game spot.`); + game.log.debug(`${socket.id} attempted to join with a name already in use ${data.name}`); socket.emit(`GameJoined`, { status: 403, - message: `Can't connect to an already connected client`, + message: `A player already has that name in the game.`, source: `JoinGame` }); return; @@ -77,6 +166,7 @@ export default (io: Server, socket: Socket, data: JoinGame) => { }); } catch (err) { + log.prettyError(err); socket.emit(`GameJoined`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/LeaveGame.ts b/server/src/events/LeaveGame.ts index 43725e6..e01a0f2 100644 --- a/server/src/events/LeaveGame.ts +++ b/server/src/events/LeaveGame.ts @@ -64,6 +64,7 @@ export default (io: Server, socket: Socket, data: LeaveGame) => { }; } catch (err) { + log.prettyError(err); socket.emit(`GameLeft`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/NewHand.ts b/server/src/events/NewHand.ts index 710abf4..7acb880 100644 --- a/server/src/events/NewHand.ts +++ b/server/src/events/NewHand.ts @@ -1,4 +1,4 @@ -import { conf, games, log } from '../main'; +import { games, log } from '../main'; import { Server, Socket } from 'socket.io'; export default (io: Server, socket: Socket, data: NewHand) => { @@ -18,6 +18,13 @@ export default (io: Server, socket: Socket, data: NewHand) => { let team = game.teams[data.team - 1]; let deck = game.questions; + /** + * The amount of cards that the team has in their hand prior to + * discarding all of their hand, this is used to make sure that they + * get back the same number of cards that they had in their hand. + */ + let handSize = team.hand.length; + // Empty the medium's hand to the discard pile so we know where the // cards are. for (var card of team.hand) { @@ -27,7 +34,7 @@ export default (io: Server, socket: Socket, data: NewHand) => { }; // Add the questions and then alert the game clients about the changes - team.addCardsToHand(deck.draw(conf.game.hand_size)); + team.addCardsToHand(deck.draw(handSize)); game.log.silly(`Drew a new hand of cards for team ${data.team}.`); io.to(game.id).emit(`UpdateHand`, { status: 200, @@ -36,6 +43,7 @@ export default (io: Server, socket: Socket, data: NewHand) => { }); } catch (err) { + log.prettyError(err); socket.emit(`UpdateHand`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/ObjectList.ts b/server/src/events/ObjectList.ts index 04f6cfe..11a08e6 100644 --- a/server/src/events/ObjectList.ts +++ b/server/src/events/ObjectList.ts @@ -7,7 +7,7 @@ export default (io: Server, socket: Socket, data: ObjectList) => { // Assert game exists if (!games[data.game_code]) { log.debug(`Can't get objects for game that doesn't exist: ${data.game_code}`); - socket.emit(`Error`, { + socket.emit(`ObjectList`, { status: 404, message: `Game with code ${data.game_code} could not be found`, source: `ObjectList` @@ -22,7 +22,8 @@ export default (io: Server, socket: Socket, data: ObjectList) => { }); } catch (err) { - socket.emit(`Error`, { + log.prettyError(err); + socket.emit(`ObjectList`, { status: 500, message: `${err.name}: ${err.message}`, source: `ObjectList`, diff --git a/server/src/events/ResetGame.ts b/server/src/events/ResetGame.ts new file mode 100644 index 0000000..8c2b093 --- /dev/null +++ b/server/src/events/ResetGame.ts @@ -0,0 +1,29 @@ +import { games, log } from '../main'; +import { Server, Socket } from 'socket.io'; + +export default (io: Server, socket: Socket, data: ResetGame) => { + try { + if (!games[data.game_code]) { + log.debug(`Can't find game with code: ${data.game_code}`); + socket.emit(`GameReset`, { + status: 404, + message: `Can't find game with code: ${data.game_code}`, + source: `ResetGame` + }); + return; + }; + let game = games[data.game_code]; + + game.questions.reset(); + game.resetObject(); + + io.to(game.id).emit(`GameReset`, { status: 200 }); + } catch (err) { + log.prettyError(err); + socket.emit(`GameReset`, { + status: 500, + message: `${err.name}: ${err.message}`, + source: `ResetGame`, + }); + } +}; \ No newline at end of file diff --git a/server/src/events/SelectObject.ts b/server/src/events/SelectObject.ts index 521522d..d50faf2 100644 --- a/server/src/events/SelectObject.ts +++ b/server/src/events/SelectObject.ts @@ -35,6 +35,7 @@ export default (io: Server, socket: Socket, data: SelectObject) => { }); } catch (err) { + log.prettyError(err); socket.emit(`ChosenObject`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/SendCard.ts b/server/src/events/SendCard.ts index 6947d5d..67aae24 100644 --- a/server/src/events/SendCard.ts +++ b/server/src/events/SendCard.ts @@ -20,8 +20,11 @@ export default (io: Server, socket: Socket, data: SendCard) => { // The writer is answering if (data.from === "writer") { - game.log.debug(` Writer selected question to answer.`); + game.log.debug(`Writer selected question to answer.`); + + // Draw new cards for team deck.discard(data.text); + team.addCardsToHand(game.questions.draw(conf.game.hand_size - team.hand.length)); team.selectQuestion(data.text); socket.emit(`UpdateHand`, { @@ -29,16 +32,20 @@ export default (io: Server, socket: Socket, data: SendCard) => { mode: "replace", questions: [] }); + io.to(`${game.id}:${team.id}:guesser`).emit(`UpdateHand`, { + status: 200, + mode: "replace", + questions: team.hand + }); return; } // The writer is sending the card to the writer else if (data.from === "guesser") { - game.log.debug(`Guesser is sending the card to the writer.`); + game.log.debug(`Guesser is sending a card to the writer.`); // Update the team's hand team.removeCard(data.text); - team.addCardsToHand(game.questions.draw(conf.game.hand_size - team.hand.length)); // send the question text to the writer player io.to(`${game.id}:${team.id}:writer`).emit(`UpdateHand`, { @@ -67,9 +74,10 @@ export default (io: Server, socket: Socket, data: SendCard) => { }; } catch (err) { + log.prettyError(err); socket.emit(`UpdateHand`, { status: 500, - message: `${err.name}: ${err.message}`, + message: err.message, source: `SendCard`, }); } diff --git a/server/src/events/StartGame.ts b/server/src/events/StartGame.ts index 719037b..14580e9 100644 --- a/server/src/events/StartGame.ts +++ b/server/src/events/StartGame.ts @@ -7,7 +7,7 @@ export default (io: Server, socket: Socket, data: StartGame) => { // Assert game exists if (!games[data.game_code]) { log.debug(`Could not find a game with ID ${data.game_code} to start`); - socket.emit(`GameJoined`, { + socket.emit(`GameStarted`, { status: 404, message: `Game with code "${data.game_code}" could not be found`, source: `StartGame`, @@ -65,6 +65,7 @@ export default (io: Server, socket: Socket, data: StartGame) => { io.to(game.id).emit(`GameStarted`, { status: 200 }); } catch (err) { + log.prettyError(err); socket.emit(`GameStarted`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/UpdateAnswer.ts b/server/src/events/UpdateAnswer.ts index 0821170..ee7c2c8 100644 --- a/server/src/events/UpdateAnswer.ts +++ b/server/src/events/UpdateAnswer.ts @@ -26,6 +26,7 @@ export default (io: Server, socket: Socket, data: UpdateAnswer) => { }); } catch (err) { + log.prettyError(err); socket.emit(`UpdateAnswer`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/UpdatePlayer.ts b/server/src/events/UpdatePlayer.ts index af1de08..f2dff42 100644 --- a/server/src/events/UpdatePlayer.ts +++ b/server/src/events/UpdatePlayer.ts @@ -32,6 +32,7 @@ export default (io: Server, socket: Socket, data: UpdatePlayer) => { }; } catch (err) { + log.prettyError(err); socket.emit(`PlayerUpdate`, { status: 500, message: `${err.name}: ${err.message}`, diff --git a/server/src/events/_template.ts b/server/src/events/_template.ts index c70c896..40d11d2 100644 --- a/server/src/events/_template.ts +++ b/server/src/events/_template.ts @@ -1,14 +1,16 @@ +import { log } from '../main'; import { Server, Socket } from 'socket.io'; export default (io: Server, socket: Socket, data: any) => { try { - socket.emit(`Error`, { + socket.emit(``, { status: 501, message: `: Not Implemented Yet`, source: ``, }); } catch (err) { - socket.emit(`Error`, { + log.prettyError(err); + socket.emit(``, { status: 500, message: `${err.name}: ${err.message}`, source: ``, diff --git a/server/src/main.ts b/server/src/main.ts index 7b38cfd..22eb409 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,12 +1,27 @@ import * as toml from "toml"; import { Logger } from "tslog"; -import { readFileSync } from "fs"; import { Game } from "./objects/Game"; import startWebsocket from "./websocket"; import { Validate } from "./utils/validate"; +import { processExit } from "./utils/cleanup"; +import { readdirSync, readFileSync } from "fs"; export const conf: config = toml.parse(readFileSync(`server.toml`, `utf-8`)); +/* + * These are the objects that we use to keep track of the different games, + * there are two different types of game, hibernated, and active. Hibernated + * games are games which have data associated with them in the datastore, but + * have not had any requests associated with them since they were hibernated. + * Active games are games that have active socket connections associated with + * them or have not been cleaned up by the game creation process yet. Active + * games become hibernated once the server has been closed while they are in + * the middle of a game, if they are in the lobby, the cleanup systems will + * just delete the game outright instead of saving it to disk. These methods + * allow us to keep games that are in the process of being played if/when the + * server crashes/restarts from having to completely start the game over again. + */ +export var hibernatedGames: string[] = []; export var games: {[index: string]: Game} = {}; export const log: Logger = new Logger({ @@ -18,6 +33,22 @@ export const log: Logger = new Logger({ name: `GLOBAL`, }); +// Ensure the config valid if (Validate.config(conf)) { + + // Add event listeners if we want to use the datastore saving game system + if (conf.datastores.enabled) { + log.info(`Loading list of hibernated games`); + + // Get game IDs from datastore + hibernatedGames = readdirSync(conf.datastores.directory) + .filter(g => g.endsWith(conf.datastores.filetype)) + .map(f => f.replace(`\.${conf.datastores.filetype}`, ``)); + + log.info(`Found ${hibernatedGames.length} hibernated games`); + process.on(`uncaughtException`, processExit); + process.on(`SIGINT`, processExit); + }; + startWebsocket(conf); } \ No newline at end of file diff --git a/server/src/objects/Deck.ts b/server/src/objects/Deck.ts index 1d04a7b..a65e249 100644 --- a/server/src/objects/Deck.ts +++ b/server/src/objects/Deck.ts @@ -56,4 +56,33 @@ export class Deck { this._unknown = this._unknown.filter(x => x != card); this._discard.push(card); }; + + + public reset() { + this._deck.push(...this._discard, ...this._unknown); + this._discard = []; + this._unknown = []; + }; + + + public toJSON(): datastoreDeck { + /** + * Converts this Deck into a JSON-compatible object + */ + return { + deck: this._deck, + unknown: this._unknown, + discard: this._discard, + }; + }; + + public static fromJSON(data: datastoreDeck): Deck { + /** + * Converts the JSON representation of a deck into a Deck + */ + let d = new Deck(data.deck); + d._discard = data.discard; + d._unknown = data.unknown; + return d; + }; }; \ No newline at end of file diff --git a/server/src/objects/Game.ts b/server/src/objects/Game.ts index 1552455..c3e2e99 100644 --- a/server/src/objects/Game.ts +++ b/server/src/objects/Game.ts @@ -1,41 +1,46 @@ import { Team } from "./Team"; import { Deck } from "./Deck"; +import { readFile } from "fs"; import neatCSV from "neat-csv"; import { Logger } from "tslog"; -import { games } from "../main"; import { Player } from "./Player"; -import { readFile } from "fs"; +import { games, hibernatedGames, conf } from "../main"; export class Game { readonly id: string; readonly host: Player; public log: Logger; public ingame: boolean; - public teams: [Team, Team]; + public teams: Team[]; public players: Player[]; private _questions: Deck; private _objects: Deck; - private _objectCard: string[]; + private _objectCard: string[]|null; public object: string; - constructor(conf: config, host: Player) { - this.id = Game.generateID(conf.game.code_length); + constructor(host: Player, options:any=null) { this.host = host; this.ingame = false; - this.players = []; + this.players = [host]; + this.id = options?.id || Game.generateID(conf.game.code_length); - // Get the decks based on what type of data they are. - switch (conf.game.cards.type) { - case "csv": - this.parseDeckCSV(conf); - break; - case "sheets": - this.parseDeckGoogleSheets(conf); - break; + // If the object is being instantiated from JSON we don't want to do + // any of the stuff that requires weird per-game stuff + if (!options) { + + // Get the decks based on what type of data they are. + switch (conf.game.cards.type) { + case "csv": + this.parseDeckCSV(conf); + break; + case "sheets": + this.parseDeckGoogleSheets(conf); + break; + }; + // Instantiate everything for the teams + this.teams = [ new Team(1), new Team(2) ]; }; - // Instantiate everything for the teams - this.teams = [ new Team(1), new Team(2) ]; }; get questions() { return this._questions; }; @@ -63,7 +68,7 @@ export class Game { } - private parseDeckCSV(conf: config): any { + private parseDeckCSV(conf: config) { /** * Parses out the CSV files and creates the decks for the game to run on * @@ -97,7 +102,7 @@ export class Game { }); }; - private parseDeckGoogleSheets(conf: config): void { + private parseDeckGoogleSheets(conf: config) { /** * Fetches and parses the CSV data from Google Sheets instead of local * CSV files. @@ -107,6 +112,62 @@ export class Game { }; + public resetObject() { + /** + * Resets the objects card, for restarting the game + */ + if (this._objectCard) { + this._objects.discard(this._objectCard); + this._objectCard = null; + }; + }; + + + public toJSON(): datastoreGame { + /** + * Returns a JSON representation of the game. + */ + return { + players: this.players.map(p => p.toJSON()), + teams: this.teams.map(t => t.toJSON()), + decks: { + questions: this._questions.toJSON(), + objects: this._objects.toJSON(), + }, + objectCard: this._objectCard, + object: this.object, + ingame: this.ingame, + id: this.id, + }; + }; + + public static fromJSON(host: Player, data: datastoreGame): Game { + /** + * Converts a JSON representation into a Game object + */ + let game = new this(host, { id: data.id }); + + // Re-create the deck objects + game._questions = Deck.fromJSON(data.decks.questions); + game._objects = Deck.fromJSON(data.decks.objects); + + game.teams = data.teams.map(t => Team.fromJSON(t)); + + // Re-instantiate all the players from the game. + for (var player of data.players) { + if (player.name !== host.name) { + player.host = false; + game.players.push(Player.fromJSON(player)); + }; + }; + + game._objectCard = data.objectCard; + game.object = data.object; + + return game; + }; + + public static generateID(length: number): string { /** * Generates a game code with the given length @@ -121,7 +182,7 @@ export class Game { for (var i = 0; i < length; i++) { code += `${Math.floor(Math.random() * 9)}`; }; - } while (games[code]); + } while (games[code] || hibernatedGames.includes(code)); return code; }; diff --git a/server/src/objects/Player.ts b/server/src/objects/Player.ts index 2d85416..2d279ba 100644 --- a/server/src/objects/Player.ts +++ b/server/src/objects/Player.ts @@ -4,12 +4,28 @@ export class Player { readonly name: string; public team: team|null = null; public role: role|null = null; - public socket: Socket; + public socket: Socket|null; readonly isHost: boolean; - constructor(name: string, socket: Socket, isHost=false) { + constructor(name: string, socket: Socket|null=null, isHost=false) { this.name = name; this.socket = socket; this.isHost = isHost; }; + + public toJSON(): datastorePlayer { + return { + name: this.name, + host: this.isHost, + team: this.team, + role: this.role, + }; + }; + + public static fromJSON(data: datastorePlayer): Player { + let player = new this(data.name, null, data.host); + player.role = data.role; + player.team = data.team; + return player; + }; }; \ No newline at end of file diff --git a/server/src/objects/Team.ts b/server/src/objects/Team.ts index acbeb6f..bf7cb21 100644 --- a/server/src/objects/Team.ts +++ b/server/src/objects/Team.ts @@ -69,4 +69,28 @@ export class Team { }; this._answers[answerIndex - 1] = answer; }; + + + public toJSON(): datastoreTeam { + /** + * Converts the given object into a JSON representation of the data + */ + return { + questions: this._questions, + answers: this._answers, + hand: this._hand, + id: this.id, + }; + }; + + public static fromJSON(data: datastoreTeam): Team { + /** + * Converts a team JSON object back into a Team object. + */ + let t = new Team(data.id); + t._questions = data.questions; + t._answers = data.answers; + t._hand = data.hand; + return t; + }; }; \ No newline at end of file diff --git a/server/src/types/config.d.ts b/server/src/types/config.d.ts index 3cc0527..f114c07 100644 --- a/server/src/types/config.d.ts +++ b/server/src/types/config.d.ts @@ -6,9 +6,10 @@ interface config { port: number; permitted_hosts: string | string[]; }; - webserver: { + datastores: { enabled: boolean; - port: number; + filetype: string; + directory: string; }; game: { hand_size: number; diff --git a/server/src/types/data.d.ts b/server/src/types/data.d.ts index c381569..07b35b8 100644 --- a/server/src/types/data.d.ts +++ b/server/src/types/data.d.ts @@ -29,7 +29,9 @@ interface GameCreated extends response { interface DeleteGame { game_code: string; } -interface GameDeleted extends response {} +interface GameDeleted extends response { + message?: string +} interface LeaveGame { @@ -43,6 +45,11 @@ interface StartGame { } interface GameStarted extends response {} +interface ResetGame { + game_code: string; +} +interface GameReset extends response {} + interface GetPastQuestions { game_code: string; diff --git a/server/src/types/datastore.d.ts b/server/src/types/datastore.d.ts new file mode 100644 index 0000000..e819395 --- /dev/null +++ b/server/src/types/datastore.d.ts @@ -0,0 +1,35 @@ +interface datastorePlayer { + team: team | null; + role: role | null; + host: boolean; + name: string; +} + +type datastoreQuestionCard = string; +type datastoreObjectCard = string[]; + +interface datastoreTeam { + questions: datastoreQuestionCard[]; + hand: datastoreQuestionCard[]; + answers: string[]; + id: team; +} + +interface datastoreDeck { + discard: T[]; + unknown: T[]; + deck: T[]; +} + +interface datastoreGame { + decks: { + questions: datastoreDeck; + objects: datastoreDeck; + }; + objectCard: datastoreObjectCard|null; + players: datastorePlayer[]; + teams: datastoreTeam[]; + ingame: boolean; + object: string; + id: string; +} \ No newline at end of file diff --git a/server/src/utils/cleanup.ts b/server/src/utils/cleanup.ts new file mode 100644 index 0000000..9ff7ca4 --- /dev/null +++ b/server/src/utils/cleanup.ts @@ -0,0 +1,58 @@ +import { writeFileSync } from "fs"; +import { Game } from "../objects/Game"; +import { games, conf, log } from "../main"; + +export function processExit() { + /** + * This is the cleanup code that runs when the server has been exited for + * any reason. We check all games if they have any active connection(s), + * then if they do, we save the game to disk, if not we just delete it + * completely from the system. + */ + log.info(`Cleaning up games`); + for (var gc in games) { + let game = games[gc]; + if (game.ingame && activeGame(game)) { + game.log.debug(`Saving to datastore`); + writeFileSync( + `${conf.datastores.directory}/${game.id}.${conf.datastores.filetype}`, + JSON.stringify(game.toJSON()) + ); + } else { + game.log.debug(`Not saving to datastore`); + }; + }; + log.info(`Done cleaning up games`); + process.exit(); +}; + + +export async function routineCheck() { + /** + * This is the cleanup that occurs whenever a new game has been started + */ + log.info(`[routineCheck] Checking for games to clean up`) + for (var gc in games) { + let game = games[gc]; + if (!activeGame(game)) { + game.log.debug(`[routineCheck] Deleting game`); + delete games[gc]; + }; + }; + log.info(`[routineCheck] Done cleaning up games`); +}; + + +function activeGame(game: Game): boolean { + /** + * This checks if a game is still active by checking that is at least one + * socket client still connected. If not then the game is considered + * stagnant and will be deleted by the cleanup code. + */ + for (var player of game.players) { + if (player.socket?.connected) { + return true; + }; + }; + return false; +}; \ No newline at end of file diff --git a/server/src/utils/validate.ts b/server/src/utils/validate.ts index 467d5cb..62ffe6a 100644 --- a/server/src/utils/validate.ts +++ b/server/src/utils/validate.ts @@ -27,16 +27,24 @@ export class Validate { valid = false; } - // Assert data in the web server object - if (conf.webserver.enabled) { - if (!conf.webserver.port) { - log.error(`Invalid webserver port value: ${conf.webserver.port}`); + if (!conf.websocket.permitted_hosts) { + log.error(`Can't have a blank or null websocket.permitted_hosts`); + valid = false; + }; + + if (!conf.datastores) { + log.error(`Datastores object must be defined`); + valid = false; + } else { + if (conf.datastores.enabled == null) { + log.error(`datastores.enabled must be defined`); + valid = false; + }; + + if (conf.datastores.enabled && conf.datastores.directory?.length == 0) { + log.error(`datastores.directory must be a filepath if datastores.enabled is set to true`); valid = false; }; - }; - if (!conf.websocket.permitted_hosts) { - log.error(`Can't have a blank or null webserver.hostname`); - valid = false; }; // Config is valid diff --git a/server/src/websocket.ts b/server/src/websocket.ts index 5a7bfdd..f3e6ac5 100644 --- a/server/src/websocket.ts +++ b/server/src/websocket.ts @@ -5,6 +5,7 @@ import GetHand from "./events/GetHand"; import NewHand from "./events/NewHand"; import JoinGame from "./events/JoinGame"; import SendCard from "./events/SendCard"; +import ResetGame from "./events/ResetGame"; import LeaveGame from "./events/LeaveGame"; import StartGame from "./events/StartGame"; import CreateGame from "./events/CreateGame"; @@ -33,6 +34,7 @@ export default async (conf: config) => { // Game Management socket.on(`CreateGame`, (data: CreateGame) => CreateGame(io, socket, data)); socket.on(`StartGame`, (data: StartGame) => StartGame(io, socket, data)); + socket.on(`ResetGame`, (data: ResetGame) => ResetGame(io, socket, data)); socket.on(`DeleteGame`, (data: DeleteGame) => DeleteGame(io, socket, data)); diff --git a/web/package.json b/web/package.json index 99c7754..2dc1b33 100644 --- a/web/package.json +++ b/web/package.json @@ -13,8 +13,7 @@ "vue": "^2.6.11", "vue-clipboard2": "^0.3.1", "vue-socket.io-extended": "^4.0.5", - "vuex": "^3.4.0", - "vuex-persist": "^3.1.3" + "vuex": "^3.4.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 8882ded..7ed15c1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -5,7 +5,6 @@ dependencies: vue-clipboard2: 0.3.1 vue-socket.io-extended: 4.0.5 vuex: 3.6.0_vue@2.6.12 - vuex-persist: 3.1.3_vuex@3.6.0 devDependencies: '@vue/cli-plugin-babel': 4.5.9_8ae91920fb9b3c76895c2e8acb765728 '@vue/cli-plugin-eslint': 4.5.9_6778c0324b153720448c6ab0d5359212 @@ -3243,12 +3242,6 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== - /deepmerge/4.2.2: - dev: false - engines: - node: '>=0.10.0' - resolution: - integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== /default-gateway/4.2.0: dependencies: execa: 1.0.0 @@ -4195,10 +4188,6 @@ packages: dev: true resolution: integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - /flatted/3.1.0: - dev: false - resolution: - integrity: sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== /flush-write-stream/1.1.1: dependencies: inherits: 2.0.4 @@ -8744,16 +8733,6 @@ packages: dev: false resolution: integrity: sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg== - /vuex-persist/3.1.3_vuex@3.6.0: - dependencies: - deepmerge: 4.2.2 - flatted: 3.1.0 - vuex: 3.6.0_vue@2.6.12 - dev: false - peerDependencies: - vuex: '>=2.5' - resolution: - integrity: sha512-QWOpP4SxmJDC5Y1+0+Yl/F4n7z27syd1St/oP+IYCGe0X0GFio0Zan6kngZFufdIhJm+5dFGDo3VG5kdkCGeRQ== /vuex/3.6.0_vue@2.6.12: dependencies: vue: 2.6.12 @@ -9161,4 +9140,3 @@ specifiers: vue-socket.io-extended: ^4.0.5 vue-template-compiler: ^2.6.11 vuex: ^3.4.0 - vuex-persist: ^3.1.3 diff --git a/web/src/App.vue b/web/src/App.vue index 4aa0092..e7a1d48 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -95,7 +95,7 @@ export default { this.handleError(data); } else { this.alert = { - message: `The game has been ended by the host.`, + message: data.message, type: `info`, }; this.$store.commit(`resetState`); diff --git a/web/src/components/Attributions.vue b/web/src/components/Attributions.vue index 930d964..221277b 100644 --- a/web/src/components/Attributions.vue +++ b/web/src/components/Attributions.vue @@ -42,7 +42,6 @@ export default { modal: false, tooling: { "Vue.JS (With VueX)": "https://vuejs.org", - "VueX-Persist": "https://www.npmjs.com/package/vuex-persist", "Vue-Socket.io": "https://github.com/MetinSeylan/Vue-Socket.io", "Vue-Clipboard2": "https://www.npmjs.com/package/vue-clipboard2", "Toml": "https://www.npmjs.com/package/toml", diff --git a/web/src/components/GameBoard.vue b/web/src/components/GameBoard.vue index 435d58d..81d94c6 100644 --- a/web/src/components/GameBoard.vue +++ b/web/src/components/GameBoard.vue @@ -10,8 +10,11 @@ a model attribute to keep them synced correctly. -->
input { + border-color: green !important; + border-width: 3px; +} .eye-container { position: absolute; @@ -217,9 +233,10 @@ h2 { } input[type="text"] { - font-family: var(--fonts); background-color: var(--board-background-alt); color: var(--board-background-alt-text); + font-family: var(--input-fonts); + text-transform: uppercase; border-color: transparent; border-style: solid; border-radius: 7px; diff --git a/web/src/components/Hand.vue b/web/src/components/Hand.vue index 54e972f..43a215d 100644 --- a/web/src/components/Hand.vue +++ b/web/src/components/Hand.vue @@ -1,8 +1,16 @@