commit
5c650edda4
37 changed files with 611 additions and 124 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ node_modules
|
||||||
server/server.toml
|
server/server.toml
|
||||||
server/built/*
|
server/built/*
|
||||||
server/dist/*
|
server/dist/*
|
||||||
|
server/resources/games
|
||||||
|
|
||||||
#=============================================================================#
|
#=============================================================================#
|
||||||
# The files that were auto-generated into a .gitignore by Vue-cli
|
# The files that were auto-generated into a .gitignore by Vue-cli
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
|
import { games, log } from '../main';
|
||||||
import { Game } from '../objects/Game';
|
import { Game } from '../objects/Game';
|
||||||
import { Player } from '../objects/Player';
|
import { Player } from '../objects/Player';
|
||||||
import { Server, Socket } from 'socket.io';
|
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) => {
|
export default (io: Server, socket: Socket, data: CreateGame) => {
|
||||||
try {
|
try {
|
||||||
let host = new Player(data.name, socket, true);
|
let host = new Player(data.name, socket, true);
|
||||||
|
|
||||||
// Create the game object to save
|
// Create the game object to save
|
||||||
let game = new Game(conf, host);
|
let game = new Game(host);
|
||||||
games[game.id] = game;
|
games[game.id] = game;
|
||||||
game.players.push(host);
|
|
||||||
game.log = log.getChildLogger({
|
game.log = log.getChildLogger({
|
||||||
displayLoggerName: true,
|
displayLoggerName: true,
|
||||||
name: game.id,
|
name: game.id,
|
||||||
})
|
});
|
||||||
game.log.info(`New game created (host=${host.name})`);
|
game.log.info(`New game created (host=${host.name})`);
|
||||||
|
|
||||||
socket.join(game.id);
|
socket.join(game.id);
|
||||||
|
|
@ -23,8 +23,12 @@ export default (io: Server, socket: Socket, data: CreateGame) => {
|
||||||
game_code: game.id,
|
game_code: game.id,
|
||||||
players: game.playerData,
|
players: game.playerData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check for any inactive games that are still marked as active
|
||||||
|
routineCheck();
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`GameCreated`, {
|
socket.emit(`GameCreated`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,13 @@ export default (io: Server, socket: Socket, data: DeleteGame) => {
|
||||||
// Delete game
|
// Delete game
|
||||||
game.log.debug(`Game deleted.`)
|
game.log.debug(`Game deleted.`)
|
||||||
delete games[data.game_code];
|
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) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`GameDeleted`, {
|
socket.emit(`GameDeleted`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export default (io: Server, socket: Socket, data: GetHand) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`QuestionList`, {
|
socket.emit(`QuestionList`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export default (io: Server, socket: Socket, data: GetPastQuestions) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`PastQuestions`, {
|
socket.emit(`PastQuestions`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,83 @@
|
||||||
import { games, log } from '../main';
|
import { readFileSync } from 'fs';
|
||||||
|
import { Game } from '../objects/Game';
|
||||||
import { Player } from '../objects/Player';
|
import { Player } from '../objects/Player';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { games, hibernatedGames, log, conf } from '../main';
|
||||||
|
|
||||||
export default (io: Server, socket: Socket, data: JoinGame) => {
|
export default (io: Server, socket: Socket, data: JoinGame) => {
|
||||||
try {
|
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
|
// Assert game exists
|
||||||
if (!games[data.game_code]) {
|
if (!games[data.game_code]) {
|
||||||
|
|
@ -18,30 +92,45 @@ export default (io: Server, socket: Socket, data: JoinGame) => {
|
||||||
let game = games[data.game_code];
|
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);
|
let sameName = game.players.find(x => x.name == data.name);
|
||||||
if (sameName != null) {
|
if (sameName != null) {
|
||||||
if (!game.ingame) {
|
|
||||||
game.log.info(`Client attempted to connect using name already in use.`);
|
if (!sameName.socket?.connected) {
|
||||||
socket.emit(`GameJoined`, {
|
sameName.socket = socket;
|
||||||
status: 400,
|
game.log.info(`Player Reconnected to the game (name=${data.name})`);
|
||||||
message: `A player already has that name in the game.`,
|
|
||||||
source: `JoinGame`
|
// Get the hand of the player's team
|
||||||
});
|
let hand: string[] = [];
|
||||||
return;
|
if (sameName.team && sameName.role == `guesser`) {
|
||||||
|
hand = game.teams[sameName.team - 1].hand;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player has the same name but is allowed to rejoin if they
|
socket.emit(`GameRejoined`, {
|
||||||
// disconnect in the middle of the game
|
status: 200,
|
||||||
if (!sameName.socket.connected) {
|
ingame: game.ingame,
|
||||||
game.log.info(`Player Reconnected to the game (name=${data.name})`);
|
role: sameName.role,
|
||||||
socket.emit(`GameRejoined`, { status: 200 });
|
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;
|
return;
|
||||||
} else {
|
} 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`, {
|
socket.emit(`GameJoined`, {
|
||||||
status: 403,
|
status: 403,
|
||||||
message: `Can't connect to an already connected client`,
|
message: `A player already has that name in the game.`,
|
||||||
source: `JoinGame`
|
source: `JoinGame`
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -77,6 +166,7 @@ export default (io: Server, socket: Socket, data: JoinGame) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`GameJoined`, {
|
socket.emit(`GameJoined`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ export default (io: Server, socket: Socket, data: LeaveGame) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`GameLeft`, {
|
socket.emit(`GameLeft`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { conf, games, log } from '../main';
|
import { games, log } from '../main';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
export default (io: Server, socket: Socket, data: NewHand) => {
|
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 team = game.teams[data.team - 1];
|
||||||
let deck = game.questions;
|
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
|
// Empty the medium's hand to the discard pile so we know where the
|
||||||
// cards are.
|
// cards are.
|
||||||
for (var card of team.hand) {
|
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
|
// 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}.`);
|
game.log.silly(`Drew a new hand of cards for team ${data.team}.`);
|
||||||
io.to(game.id).emit(`UpdateHand`, {
|
io.to(game.id).emit(`UpdateHand`, {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|
@ -36,6 +43,7 @@ export default (io: Server, socket: Socket, data: NewHand) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`UpdateHand`, {
|
socket.emit(`UpdateHand`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default (io: Server, socket: Socket, data: ObjectList) => {
|
||||||
// Assert game exists
|
// Assert game exists
|
||||||
if (!games[data.game_code]) {
|
if (!games[data.game_code]) {
|
||||||
log.debug(`Can't get objects for game that doesn't exist: ${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,
|
status: 404,
|
||||||
message: `Game with code ${data.game_code} could not be found`,
|
message: `Game with code ${data.game_code} could not be found`,
|
||||||
source: `ObjectList`
|
source: `ObjectList`
|
||||||
|
|
@ -22,7 +22,8 @@ export default (io: Server, socket: Socket, data: ObjectList) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
socket.emit(`Error`, {
|
log.prettyError(err);
|
||||||
|
socket.emit(`ObjectList`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
source: `ObjectList`,
|
source: `ObjectList`,
|
||||||
|
|
|
||||||
29
server/src/events/ResetGame.ts
Normal file
29
server/src/events/ResetGame.ts
Normal file
|
|
@ -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`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -35,6 +35,7 @@ export default (io: Server, socket: Socket, data: SelectObject) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`ChosenObject`, {
|
socket.emit(`ChosenObject`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@ export default (io: Server, socket: Socket, data: SendCard) => {
|
||||||
|
|
||||||
// The writer is answering
|
// The writer is answering
|
||||||
if (data.from === "writer") {
|
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);
|
deck.discard(data.text);
|
||||||
|
team.addCardsToHand(game.questions.draw(conf.game.hand_size - team.hand.length));
|
||||||
team.selectQuestion(data.text);
|
team.selectQuestion(data.text);
|
||||||
|
|
||||||
socket.emit(`UpdateHand`, {
|
socket.emit(`UpdateHand`, {
|
||||||
|
|
@ -29,16 +32,20 @@ export default (io: Server, socket: Socket, data: SendCard) => {
|
||||||
mode: "replace",
|
mode: "replace",
|
||||||
questions: []
|
questions: []
|
||||||
});
|
});
|
||||||
|
io.to(`${game.id}:${team.id}:guesser`).emit(`UpdateHand`, {
|
||||||
|
status: 200,
|
||||||
|
mode: "replace",
|
||||||
|
questions: team.hand
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The writer is sending the card to the writer
|
// The writer is sending the card to the writer
|
||||||
else if (data.from === "guesser") {
|
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
|
// Update the team's hand
|
||||||
team.removeCard(data.text);
|
team.removeCard(data.text);
|
||||||
team.addCardsToHand(game.questions.draw(conf.game.hand_size - team.hand.length));
|
|
||||||
|
|
||||||
// send the question text to the writer player
|
// send the question text to the writer player
|
||||||
io.to(`${game.id}:${team.id}:writer`).emit(`UpdateHand`, {
|
io.to(`${game.id}:${team.id}:writer`).emit(`UpdateHand`, {
|
||||||
|
|
@ -67,9 +74,10 @@ export default (io: Server, socket: Socket, data: SendCard) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`UpdateHand`, {
|
socket.emit(`UpdateHand`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: err.message,
|
||||||
source: `SendCard`,
|
source: `SendCard`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default (io: Server, socket: Socket, data: StartGame) => {
|
||||||
// Assert game exists
|
// Assert game exists
|
||||||
if (!games[data.game_code]) {
|
if (!games[data.game_code]) {
|
||||||
log.debug(`Could not find a game with ID ${data.game_code} to start`);
|
log.debug(`Could not find a game with ID ${data.game_code} to start`);
|
||||||
socket.emit(`GameJoined`, {
|
socket.emit(`GameStarted`, {
|
||||||
status: 404,
|
status: 404,
|
||||||
message: `Game with code "${data.game_code}" could not be found`,
|
message: `Game with code "${data.game_code}" could not be found`,
|
||||||
source: `StartGame`,
|
source: `StartGame`,
|
||||||
|
|
@ -65,6 +65,7 @@ export default (io: Server, socket: Socket, data: StartGame) => {
|
||||||
io.to(game.id).emit(`GameStarted`, { status: 200 });
|
io.to(game.id).emit(`GameStarted`, { status: 200 });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`GameStarted`, {
|
socket.emit(`GameStarted`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export default (io: Server, socket: Socket, data: UpdateAnswer) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`UpdateAnswer`, {
|
socket.emit(`UpdateAnswer`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export default (io: Server, socket: Socket, data: UpdatePlayer) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
log.prettyError(err);
|
||||||
socket.emit(`PlayerUpdate`, {
|
socket.emit(`PlayerUpdate`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
|
import { log } from '../main';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
|
|
||||||
export default (io: Server, socket: Socket, data: any) => {
|
export default (io: Server, socket: Socket, data: any) => {
|
||||||
try {
|
try {
|
||||||
socket.emit(`Error`, {
|
socket.emit(``, {
|
||||||
status: 501,
|
status: 501,
|
||||||
message: `: Not Implemented Yet`,
|
message: `: Not Implemented Yet`,
|
||||||
source: ``,
|
source: ``,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
socket.emit(`Error`, {
|
log.prettyError(err);
|
||||||
|
socket.emit(``, {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `${err.name}: ${err.message}`,
|
message: `${err.name}: ${err.message}`,
|
||||||
source: ``,
|
source: ``,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import * as toml from "toml";
|
import * as toml from "toml";
|
||||||
import { Logger } from "tslog";
|
import { Logger } from "tslog";
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import { Game } from "./objects/Game";
|
import { Game } from "./objects/Game";
|
||||||
import startWebsocket from "./websocket";
|
import startWebsocket from "./websocket";
|
||||||
import { Validate } from "./utils/validate";
|
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`));
|
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 var games: {[index: string]: Game} = {};
|
||||||
|
|
||||||
export const log: Logger = new Logger({
|
export const log: Logger = new Logger({
|
||||||
|
|
@ -18,6 +33,22 @@ export const log: Logger = new Logger({
|
||||||
name: `GLOBAL`,
|
name: `GLOBAL`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure the config valid
|
||||||
if (Validate.config(conf)) {
|
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);
|
startWebsocket(conf);
|
||||||
}
|
}
|
||||||
|
|
@ -56,4 +56,33 @@ export class Deck<T> {
|
||||||
this._unknown = this._unknown.filter(x => x != card);
|
this._unknown = this._unknown.filter(x => x != card);
|
||||||
this._discard.push(card);
|
this._discard.push(card);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this._deck.push(...this._discard, ...this._unknown);
|
||||||
|
this._discard = [];
|
||||||
|
this._unknown = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public toJSON(): datastoreDeck<T> {
|
||||||
|
/**
|
||||||
|
* Converts this Deck into a JSON-compatible object
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
deck: this._deck,
|
||||||
|
unknown: this._unknown,
|
||||||
|
discard: this._discard,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public static fromJSON<A>(data: datastoreDeck<A>): Deck<A> {
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1,29 +1,33 @@
|
||||||
import { Team } from "./Team";
|
import { Team } from "./Team";
|
||||||
import { Deck } from "./Deck";
|
import { Deck } from "./Deck";
|
||||||
|
import { readFile } from "fs";
|
||||||
import neatCSV from "neat-csv";
|
import neatCSV from "neat-csv";
|
||||||
import { Logger } from "tslog";
|
import { Logger } from "tslog";
|
||||||
import { games } from "../main";
|
|
||||||
import { Player } from "./Player";
|
import { Player } from "./Player";
|
||||||
import { readFile } from "fs";
|
import { games, hibernatedGames, conf } from "../main";
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly host: Player;
|
readonly host: Player;
|
||||||
public log: Logger;
|
public log: Logger;
|
||||||
public ingame: boolean;
|
public ingame: boolean;
|
||||||
public teams: [Team, Team];
|
public teams: Team[];
|
||||||
public players: Player[];
|
public players: Player[];
|
||||||
private _questions: Deck<question_deck>;
|
private _questions: Deck<question_deck>;
|
||||||
private _objects: Deck<object_deck>;
|
private _objects: Deck<object_deck>;
|
||||||
private _objectCard: string[];
|
private _objectCard: string[]|null;
|
||||||
public object: string;
|
public object: string;
|
||||||
|
|
||||||
|
|
||||||
constructor(conf: config, host: Player) {
|
constructor(host: Player, options:any=null) {
|
||||||
this.id = Game.generateID(conf.game.code_length);
|
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.ingame = false;
|
this.ingame = false;
|
||||||
this.players = [];
|
this.players = [host];
|
||||||
|
this.id = options?.id || Game.generateID(conf.game.code_length);
|
||||||
|
|
||||||
|
// 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.
|
// Get the decks based on what type of data they are.
|
||||||
switch (conf.game.cards.type) {
|
switch (conf.game.cards.type) {
|
||||||
|
|
@ -37,6 +41,7 @@ export class Game {
|
||||||
// Instantiate everything for the teams
|
// Instantiate everything for the teams
|
||||||
this.teams = [ new Team(1), new Team(2) ];
|
this.teams = [ new Team(1), new Team(2) ];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
get questions() { return this._questions; };
|
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
|
* 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
|
* Fetches and parses the CSV data from Google Sheets instead of local
|
||||||
* CSV files.
|
* 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<question_deck>(data.decks.questions);
|
||||||
|
game._objects = Deck.fromJSON<object_deck>(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 {
|
public static generateID(length: number): string {
|
||||||
/**
|
/**
|
||||||
* Generates a game code with the given length
|
* Generates a game code with the given length
|
||||||
|
|
@ -121,7 +182,7 @@ export class Game {
|
||||||
for (var i = 0; i < length; i++) {
|
for (var i = 0; i < length; i++) {
|
||||||
code += `${Math.floor(Math.random() * 9)}`;
|
code += `${Math.floor(Math.random() * 9)}`;
|
||||||
};
|
};
|
||||||
} while (games[code]);
|
} while (games[code] || hibernatedGames.includes(code));
|
||||||
|
|
||||||
return code;
|
return code;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,28 @@ export class Player {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
public team: team|null = null;
|
public team: team|null = null;
|
||||||
public role: role|null = null;
|
public role: role|null = null;
|
||||||
public socket: Socket;
|
public socket: Socket|null;
|
||||||
readonly isHost: boolean;
|
readonly isHost: boolean;
|
||||||
|
|
||||||
constructor(name: string, socket: Socket, isHost=false) {
|
constructor(name: string, socket: Socket|null=null, isHost=false) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.isHost = isHost;
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -69,4 +69,28 @@ export class Team {
|
||||||
};
|
};
|
||||||
this._answers[answerIndex - 1] = answer;
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
5
server/src/types/config.d.ts
vendored
5
server/src/types/config.d.ts
vendored
|
|
@ -6,9 +6,10 @@ interface config {
|
||||||
port: number;
|
port: number;
|
||||||
permitted_hosts: string | string[];
|
permitted_hosts: string | string[];
|
||||||
};
|
};
|
||||||
webserver: {
|
datastores: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
port: number;
|
filetype: string;
|
||||||
|
directory: string;
|
||||||
};
|
};
|
||||||
game: {
|
game: {
|
||||||
hand_size: number;
|
hand_size: number;
|
||||||
|
|
|
||||||
9
server/src/types/data.d.ts
vendored
9
server/src/types/data.d.ts
vendored
|
|
@ -29,7 +29,9 @@ interface GameCreated extends response {
|
||||||
interface DeleteGame {
|
interface DeleteGame {
|
||||||
game_code: string;
|
game_code: string;
|
||||||
}
|
}
|
||||||
interface GameDeleted extends response {}
|
interface GameDeleted extends response {
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface LeaveGame {
|
interface LeaveGame {
|
||||||
|
|
@ -43,6 +45,11 @@ interface StartGame {
|
||||||
}
|
}
|
||||||
interface GameStarted extends response {}
|
interface GameStarted extends response {}
|
||||||
|
|
||||||
|
interface ResetGame {
|
||||||
|
game_code: string;
|
||||||
|
}
|
||||||
|
interface GameReset extends response {}
|
||||||
|
|
||||||
|
|
||||||
interface GetPastQuestions {
|
interface GetPastQuestions {
|
||||||
game_code: string;
|
game_code: string;
|
||||||
|
|
|
||||||
35
server/src/types/datastore.d.ts
vendored
Normal file
35
server/src/types/datastore.d.ts
vendored
Normal file
|
|
@ -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<T> {
|
||||||
|
discard: T[];
|
||||||
|
unknown: T[];
|
||||||
|
deck: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface datastoreGame {
|
||||||
|
decks: {
|
||||||
|
questions: datastoreDeck<question_deck>;
|
||||||
|
objects: datastoreDeck<object_deck>;
|
||||||
|
};
|
||||||
|
objectCard: datastoreObjectCard|null;
|
||||||
|
players: datastorePlayer[];
|
||||||
|
teams: datastoreTeam[];
|
||||||
|
ingame: boolean;
|
||||||
|
object: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
58
server/src/utils/cleanup.ts
Normal file
58
server/src/utils/cleanup.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -27,18 +27,26 @@ export class Validate {
|
||||||
valid = false;
|
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}`);
|
|
||||||
valid = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!conf.websocket.permitted_hosts) {
|
if (!conf.websocket.permitted_hosts) {
|
||||||
log.error(`Can't have a blank or null webserver.hostname`);
|
log.error(`Can't have a blank or null websocket.permitted_hosts`);
|
||||||
valid = false;
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Config is valid
|
// Config is valid
|
||||||
return valid;
|
return valid;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import GetHand from "./events/GetHand";
|
||||||
import NewHand from "./events/NewHand";
|
import NewHand from "./events/NewHand";
|
||||||
import JoinGame from "./events/JoinGame";
|
import JoinGame from "./events/JoinGame";
|
||||||
import SendCard from "./events/SendCard";
|
import SendCard from "./events/SendCard";
|
||||||
|
import ResetGame from "./events/ResetGame";
|
||||||
import LeaveGame from "./events/LeaveGame";
|
import LeaveGame from "./events/LeaveGame";
|
||||||
import StartGame from "./events/StartGame";
|
import StartGame from "./events/StartGame";
|
||||||
import CreateGame from "./events/CreateGame";
|
import CreateGame from "./events/CreateGame";
|
||||||
|
|
@ -33,6 +34,7 @@ export default async (conf: config) => {
|
||||||
// Game Management
|
// Game Management
|
||||||
socket.on(`CreateGame`, (data: CreateGame) => CreateGame(io, socket, data));
|
socket.on(`CreateGame`, (data: CreateGame) => CreateGame(io, socket, data));
|
||||||
socket.on(`StartGame`, (data: StartGame) => StartGame(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));
|
socket.on(`DeleteGame`, (data: DeleteGame) => DeleteGame(io, socket, data));
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-clipboard2": "^0.3.1",
|
"vue-clipboard2": "^0.3.1",
|
||||||
"vue-socket.io-extended": "^4.0.5",
|
"vue-socket.io-extended": "^4.0.5",
|
||||||
"vuex": "^3.4.0",
|
"vuex": "^3.4.0"
|
||||||
"vuex-persist": "^3.1.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
|
|
||||||
22
web/pnpm-lock.yaml
generated
22
web/pnpm-lock.yaml
generated
|
|
@ -5,7 +5,6 @@ dependencies:
|
||||||
vue-clipboard2: 0.3.1
|
vue-clipboard2: 0.3.1
|
||||||
vue-socket.io-extended: 4.0.5
|
vue-socket.io-extended: 4.0.5
|
||||||
vuex: 3.6.0_vue@2.6.12
|
vuex: 3.6.0_vue@2.6.12
|
||||||
vuex-persist: 3.1.3_vuex@3.6.0
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@vue/cli-plugin-babel': 4.5.9_8ae91920fb9b3c76895c2e8acb765728
|
'@vue/cli-plugin-babel': 4.5.9_8ae91920fb9b3c76895c2e8acb765728
|
||||||
'@vue/cli-plugin-eslint': 4.5.9_6778c0324b153720448c6ab0d5359212
|
'@vue/cli-plugin-eslint': 4.5.9_6778c0324b153720448c6ab0d5359212
|
||||||
|
|
@ -3243,12 +3242,6 @@ packages:
|
||||||
node: '>=0.10.0'
|
node: '>=0.10.0'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
|
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:
|
/default-gateway/4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
execa: 1.0.0
|
execa: 1.0.0
|
||||||
|
|
@ -4195,10 +4188,6 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
||||||
/flatted/3.1.0:
|
|
||||||
dev: false
|
|
||||||
resolution:
|
|
||||||
integrity: sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
|
|
||||||
/flush-write-stream/1.1.1:
|
/flush-write-stream/1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
|
|
@ -8744,16 +8733,6 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
|
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:
|
/vuex/3.6.0_vue@2.6.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 2.6.12
|
vue: 2.6.12
|
||||||
|
|
@ -9161,4 +9140,3 @@ specifiers:
|
||||||
vue-socket.io-extended: ^4.0.5
|
vue-socket.io-extended: ^4.0.5
|
||||||
vue-template-compiler: ^2.6.11
|
vue-template-compiler: ^2.6.11
|
||||||
vuex: ^3.4.0
|
vuex: ^3.4.0
|
||||||
vuex-persist: ^3.1.3
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ export default {
|
||||||
this.handleError(data);
|
this.handleError(data);
|
||||||
} else {
|
} else {
|
||||||
this.alert = {
|
this.alert = {
|
||||||
message: `The game has been ended by the host.`,
|
message: data.message,
|
||||||
type: `info`,
|
type: `info`,
|
||||||
};
|
};
|
||||||
this.$store.commit(`resetState`);
|
this.$store.commit(`resetState`);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ export default {
|
||||||
modal: false,
|
modal: false,
|
||||||
tooling: {
|
tooling: {
|
||||||
"Vue.JS (With VueX)": "https://vuejs.org",
|
"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-Socket.io": "https://github.com/MetinSeylan/Vue-Socket.io",
|
||||||
"Vue-Clipboard2": "https://www.npmjs.com/package/vue-clipboard2",
|
"Vue-Clipboard2": "https://www.npmjs.com/package/vue-clipboard2",
|
||||||
"Toml": "https://www.npmjs.com/package/toml",
|
"Toml": "https://www.npmjs.com/package/toml",
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@
|
||||||
a model attribute to keep them synced correctly.
|
a model attribute to keep them synced correctly.
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class="answer"
|
|
||||||
v-for="answerIndex in 8"
|
v-for="answerIndex in 8"
|
||||||
|
:class="[
|
||||||
|
`answer`,
|
||||||
|
answers[`team_${3 - $store.state.team}`][answerIndex-1].toLowerCase() == $store.state.chosen_object+`.` ? `correct`: ``
|
||||||
|
]"
|
||||||
:key="`${otherTeamID}-answer-container-${answerIndex}`"
|
:key="`${otherTeamID}-answer-container-${answerIndex}`"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -51,8 +54,11 @@
|
||||||
and having them be disabled for all other players
|
and having them be disabled for all other players
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class="answer"
|
|
||||||
v-for="answerIndex in 8"
|
v-for="answerIndex in 8"
|
||||||
|
:class="[
|
||||||
|
`answer`,
|
||||||
|
answers[`team_${$store.state.team}`][answerIndex-1].toLowerCase() == $store.state.chosen_object+`.` ? `correct`: ``
|
||||||
|
]"
|
||||||
:key="`${teamID}-answer-container-${answerIndex}`"
|
:key="`${teamID}-answer-container-${answerIndex}`"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -105,10 +111,6 @@ export default {
|
||||||
},
|
},
|
||||||
data() {return {
|
data() {return {
|
||||||
visible: false,
|
visible: false,
|
||||||
answers: {
|
|
||||||
team_1: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
|
||||||
team_2: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
|
||||||
},
|
|
||||||
}},
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
teamID() {
|
teamID() {
|
||||||
|
|
@ -117,8 +119,18 @@ export default {
|
||||||
otherTeamID() {
|
otherTeamID() {
|
||||||
return this.$store.getters.otherTeamName.replace(/\s/g, `-`).toLowerCase();
|
return this.$store.getters.otherTeamName.replace(/\s/g, `-`).toLowerCase();
|
||||||
},
|
},
|
||||||
|
answers() {
|
||||||
|
return this.$store.state.answers;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isCorrect(team, answerIndex) {
|
||||||
|
let typedAnswer = this.answers[`team_${team}`][answerIndex - 1].toLowerCase();
|
||||||
|
if (this.$store.state.chosen_object == typedAnswer) {
|
||||||
|
return `correct`;
|
||||||
|
};
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
answerInputHandler(answerIndex) {
|
answerInputHandler(answerIndex) {
|
||||||
/**
|
/**
|
||||||
* Sends input data updates to the server when they occur, indicating
|
* Sends input data updates to the server when they occur, indicating
|
||||||
|
|
@ -147,7 +159,7 @@ export default {
|
||||||
* value: string
|
* value: string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
this.answers[`team_${data.team}`].splice(data.answer - 1, 1, data.value);
|
this.$store.commit(`updateAnswer`, data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +203,10 @@ h2 {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.answer.correct > input {
|
||||||
|
border-color: green !important;
|
||||||
|
border-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.eye-container {
|
.eye-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -217,9 +233,10 @@ h2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
font-family: var(--fonts);
|
|
||||||
background-color: var(--board-background-alt);
|
background-color: var(--board-background-alt);
|
||||||
color: var(--board-background-alt-text);
|
color: var(--board-background-alt-text);
|
||||||
|
font-family: var(--input-fonts);
|
||||||
|
text-transform: uppercase;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="PlayerHand">
|
<div id="PlayerHand">
|
||||||
<div class="recentQuestion" v-if="mostRecentQuestion">
|
<div class="flex-center" v-if="mostRecentQuestion">
|
||||||
{{ mostRecentQuestion }}
|
{{ mostRecentQuestion }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-center" v-else-if="gameOver">
|
||||||
|
<button
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="endGame"
|
||||||
|
>
|
||||||
|
Go to Lobby
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="hand" v-else>
|
<div class="hand" v-else>
|
||||||
<div
|
<div
|
||||||
class="card"
|
class="card"
|
||||||
|
|
@ -43,16 +51,21 @@ export default {
|
||||||
},
|
},
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
if (this.isGuesser) {
|
if (this.isGuesser) {
|
||||||
return this.$store.state.guesser_card_button
|
return this.$store.state.guesser_card_button;
|
||||||
} else if (this.isWriter) {
|
} else if (this.isWriter) {
|
||||||
return this.$store.state.writer_card_button
|
return this.$store.state.writer_card_button;
|
||||||
} else {
|
} else {
|
||||||
return `Unknown Role`
|
return `Unknown Role`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
questions() {
|
questions() {
|
||||||
return this.$store.state.questions;
|
return this.$store.state.questions;
|
||||||
}
|
},
|
||||||
|
gameOver() {
|
||||||
|
let targetAnswer = this.$store.state.chosen_object.toLowerCase()+`.`;
|
||||||
|
return this.$store.state.answers.team_1.includes(targetAnswer)
|
||||||
|
|| this.$store.state.answers.team_2.includes(targetAnswer);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCard(cardIndex) {
|
sendCard(cardIndex) {
|
||||||
|
|
@ -74,7 +87,12 @@ export default {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$socket.client.emit(`SendCard`, data);
|
this.$socket.client.emit(`SendCard`, data);
|
||||||
}
|
},
|
||||||
|
endGame() {
|
||||||
|
this.$socket.client.emit(`ResetGame`, {
|
||||||
|
game_code: this.$store.state.game_code
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.isGuesser) {
|
if (this.isGuesser) {
|
||||||
|
|
@ -127,7 +145,7 @@ export default {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recentQuestion {
|
.flex-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto+Slab&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The fonts and font colours the site will use
|
The fonts and font colours the site will use
|
||||||
*/
|
*/
|
||||||
--fonts: "Roboto", "Open Sans", sans-serif;
|
--fonts: "Roboto", "Open Sans", sans-serif;
|
||||||
|
--input-fonts: "Roboto Slab", var(--fonts);
|
||||||
--light-font-colour: #ECE3BB;
|
--light-font-colour: #ECE3BB;
|
||||||
--dark-font-colour: #000F3D;
|
--dark-font-colour: #000F3D;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,14 @@ import VueSocketIOExt from 'vue-socket.io-extended';
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
// Get the URI for dev enfironments
|
||||||
|
let websocket_uri = `/`;
|
||||||
|
if (process.env.NODE_ENV === `development`) {
|
||||||
|
websocket_uri = `http://${window.location.hostname}:8081`;
|
||||||
|
};
|
||||||
|
|
||||||
Vue.use(clipboard);
|
Vue.use(clipboard);
|
||||||
Vue.use(VueSocketIOExt, io(`http://${window.location.hostname}:8081`));
|
Vue.use(VueSocketIOExt, io(websocket_uri));
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
store,
|
store,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import VuexPersistence from 'vuex-persist';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
|
@ -11,9 +10,9 @@ export default new Vuex.Store({
|
||||||
icon: `sun.svg`,
|
icon: `sun.svg`,
|
||||||
eyes: {
|
eyes: {
|
||||||
1: 0, 2: 0,
|
1: 0, 2: 0,
|
||||||
3: 0, 4: 0,
|
3: 0, 4: 1,
|
||||||
5: 0, 6: 0,
|
5: 0, 6: 1,
|
||||||
7: 0, 8: 0,
|
7: 1, 8: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
team_2: {
|
team_2: {
|
||||||
|
|
@ -21,8 +20,8 @@ export default new Vuex.Store({
|
||||||
icon: `moon.svg`,
|
icon: `moon.svg`,
|
||||||
eyes: {
|
eyes: {
|
||||||
1: 0, 2: 0,
|
1: 0, 2: 0,
|
||||||
3: 0, 4: 0,
|
3: 1, 4: 0,
|
||||||
5: 0, 6: 0,
|
5: 1, 6: 1,
|
||||||
7: 0, 8: 0,
|
7: 0, 8: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -47,6 +46,10 @@ export default new Vuex.Store({
|
||||||
questions: [],
|
questions: [],
|
||||||
game_code: null,
|
game_code: null,
|
||||||
players: [],
|
players: [],
|
||||||
|
answers: {
|
||||||
|
team_1: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
||||||
|
team_2: [ ``, ``, ``, ``, ``, ``, ``, `` ],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
teamName(state) {
|
teamName(state) {
|
||||||
|
|
@ -73,6 +76,10 @@ export default new Vuex.Store({
|
||||||
state.questions = [];
|
state.questions = [];
|
||||||
state.game_code = null;
|
state.game_code = null;
|
||||||
state.players = [];
|
state.players = [];
|
||||||
|
state.answers = {
|
||||||
|
team_1: new Array(8).fill(``),
|
||||||
|
team_2: new Array(8).fill(``),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
player(state, data) {
|
player(state, data) {
|
||||||
if (data.name)
|
if (data.name)
|
||||||
|
|
@ -84,7 +91,7 @@ export default new Vuex.Store({
|
||||||
if (data.host)
|
if (data.host)
|
||||||
state.is_host = data.host
|
state.is_host = data.host
|
||||||
},
|
},
|
||||||
game_code(state, game_code) {
|
gameCode(state, game_code) {
|
||||||
state.game_code = game_code;
|
state.game_code = game_code;
|
||||||
},
|
},
|
||||||
view(state, target) {
|
view(state, target) {
|
||||||
|
|
@ -113,13 +120,16 @@ export default new Vuex.Store({
|
||||||
appendToHand(state, questions) {
|
appendToHand(state, questions) {
|
||||||
state.questions.push(...questions);
|
state.questions.push(...questions);
|
||||||
},
|
},
|
||||||
|
updateAnswer(state, data) {
|
||||||
|
state.answers[`team_${data.team}`].splice(data.answer - 1, 1, data.value)
|
||||||
|
},
|
||||||
|
setAnswers(state, data) {
|
||||||
|
state.answers.team_1 = data.team_1;
|
||||||
|
state.answers.team_2 = data.team_2;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
},
|
},
|
||||||
plugins:
|
|
||||||
process.env.NODE_ENV === `production`
|
|
||||||
? [new VuexPersistence({ key: `ghost-writer-save` }).plugin]
|
|
||||||
: []
|
|
||||||
});
|
});
|
||||||
|
|
@ -71,7 +71,7 @@ export default {
|
||||||
|
|
||||||
// Save the data in the store
|
// Save the data in the store
|
||||||
this.$store.commit(`playerList`, data.players);
|
this.$store.commit(`playerList`, data.players);
|
||||||
this.$store.commit(`game_code`, this.game_code);
|
this.$store.commit(`gameCode`, this.game_code);
|
||||||
this.$store.commit(`player`, {
|
this.$store.commit(`player`, {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
host: false,
|
host: false,
|
||||||
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
|
|
||||||
// Update storage
|
// Update storage
|
||||||
this.$store.commit(`playerList`, data.players);
|
this.$store.commit(`playerList`, data.players);
|
||||||
this.$store.commit(`game_code`, data.game_code);
|
this.$store.commit(`gameCode`, data.game_code);
|
||||||
this.$store.commit(`player`, {
|
this.$store.commit(`player`, {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
host: true,
|
host: true,
|
||||||
|
|
@ -97,11 +97,42 @@ export default {
|
||||||
this.$store.commit(`view`, `lobby`);
|
this.$store.commit(`view`, `lobby`);
|
||||||
},
|
},
|
||||||
GameRejoined(data) {
|
GameRejoined(data) {
|
||||||
|
/**
|
||||||
|
* data = {
|
||||||
|
* status: integer,
|
||||||
|
* ingame: boolean,
|
||||||
|
* role: role,
|
||||||
|
* team: team,
|
||||||
|
* is_host: boolean,
|
||||||
|
* players: Player[],
|
||||||
|
* chosen_object: string,
|
||||||
|
* hand: string[],
|
||||||
|
* answers: {
|
||||||
|
* team_1: string[],
|
||||||
|
* team_2: string[],
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
*/
|
||||||
|
console.log(data)
|
||||||
if (!(200 <= data.status && data.status < 300)) {
|
if (!(200 <= data.status && data.status < 300)) {
|
||||||
this.$emit(`error`, data);
|
this.$emit(`error`, data);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// TODO -> Update all data that is received from the server
|
this.$store.commit(`resetState`);
|
||||||
|
this.$store.commit(`player`, {
|
||||||
|
name: this.name,
|
||||||
|
host: data.is_host,
|
||||||
|
role: data.role,
|
||||||
|
team: data.team,
|
||||||
|
});
|
||||||
|
this.$store.commit(`setObject`, data.chosen_object);
|
||||||
|
this.$store.commit(`view`, data.ingame ? `in-game` : `lobby`);
|
||||||
|
this.$store.commit(`setAnswers`, data.answers);
|
||||||
|
this.$store.commit(`playerList`, data.players);
|
||||||
|
this.$store.commit(`replaceHand`, data.hand);
|
||||||
|
|
||||||
|
history.replaceState(null, ``, `?game=${this.game_code}`);
|
||||||
|
this.$store.commit(`gameCode`, this.game_code);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue