gcloud config, various bugfixes

This commit is contained in:
Alec
2021-12-27 23:17:30 -05:00
parent dd9a7f9c88
commit fb3a447f26
28 changed files with 391 additions and 775 deletions

View File

@@ -1,5 +1,19 @@
runtime: nodejs
env: flex
entrypoint: npm run start:prod:linux
automatic_scaling:
max_instances: 1
network:
session_affinity: true
liveness_check:
path: "/liveness_check"
check_interval_sec: 60
timeout_sec: 4
failure_threshold: 2
success_threshold: 2
readiness_check:
path: "/readiness_check"
check_interval_sec: 60
timeout_sec: 4
failure_threshold: 2
success_threshold: 2
app_start_timeout_sec: 600
manual_scaling:
instances: 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
export class DeckStateManager {
constructor() {
this.deck = null;
this.customRoleOptions = null;
this.customRoleOptions = [];
}
addToDeck(role) {

View File

@@ -67,7 +67,6 @@ export class GameCreationStepManager {
this.currentGame.hasTimer = false;
this.currentGame.timerParams = null;
}
console.log(this.currentGame);
cancelCurrentToast();
this.removeStepElementsFromDOM(this.step);
this.incrementStep();
@@ -102,6 +101,10 @@ export class GameCreationStepManager {
) {
window.location = ('/game/' + res.content);
}
}).catch((e) => {
if (e.status === 429) {
toast('You\'ve sent this request too many times.', 'error', true, true, 6);
}
});
}
}
@@ -450,8 +453,9 @@ function initializeRemainingEventListeners(deckManager) {
e.preventDefault();
let name = document.getElementById("role-name").value.trim();
let description = document.getElementById("role-description").value.trim();
let team = document.getElementById("role-alignment").value.toLowerCase().trim();
if (!deckManager.getCustomRoleOption(name)) { // confirm there is no existing custom role with the same name
deckManager.addToCustomRoleOptions({role: name, description: description});
deckManager.addToCustomRoleOptions({role: name, description: description, team: team});
updateCustomRoleOptionsList(deckManager, document.getElementById("deck-select"))
ModalManager.dispelModal("add-role-modal", "add-role-modal-background");
toast("Role Added", "success", true);
@@ -481,10 +485,10 @@ function addOptionsToList(options, selectEl) {
});
for (let i = 0; i < options.length; i ++) {
let optionEl = document.createElement("option");
let alignmentClass = customCards[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL
let alignmentClass = options[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL
optionEl.classList.add(alignmentClass);
optionEl.setAttribute("value", customCards[i].role);
optionEl.innerText = customCards[i].role;
optionEl.setAttribute("value", options[i].role);
optionEl.innerText = options[i].role;
selectEl.appendChild(optionEl);
}
}

View File

@@ -39,7 +39,9 @@ export class GameStateRenderer {
title.innerText = "Lobby";
document.getElementById("game-title").appendChild(title);
let gameLinkContainer = document.getElementById("game-link");
gameLinkContainer.innerText = window.location;
let linkDiv = document.createElement("div");
linkDiv.innerText = window.location;
gameLinkContainer.prepend(linkDiv);
gameLinkContainer.addEventListener('click', () => {
navigator.clipboard.writeText(gameLinkContainer.innerText).then(() => {
toast('Link copied!', 'success', true);
@@ -265,7 +267,6 @@ function renderPotentialMods(gameState, group, transferModHandlers, socket) {
container.addEventListener('click', transferModHandlers[member.id]);
modalContent.appendChild(container);
console.log('test');
}
}
}

View File

@@ -114,7 +114,6 @@ export class GameTimerManager {
if(!socket.hasListeners(globals.COMMANDS.GET_TIME_REMAINING)) {
socket.on(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => {
console.log('received time remaining from server');
if (paused) {
this.displayPausedTime(timeRemaining);
} else if (timeRemaining === 0) {
@@ -151,6 +150,16 @@ export class GameTimerManager {
pauseBtn.addEventListener('click', this.pauseListener);
document.getElementById('play-pause').appendChild(pauseBtn);
}
processTimeRemaining(timeRemaining, paused, timerWorker) {
if (paused) {
this.displayPausedTime(timeRemaining);
} else if (timeRemaining === 0) {
this.displayExpiredTime();
} else {
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
}
}
}
function returnHumanReadableTime(milliseconds, tenthsOfSeconds=false) {

View File

@@ -1,5 +1,6 @@
/* It started getting confusing where I am reading/writing to the game state, and thus the state started to get inconsistent.
Creating a bucket to hold it so I can overwrite the gameState object whilst still preserving a reference to the containing bucket.
Now several components can read a shared game state.
*/
export const stateBucket = {
currentGameState: null,

View File

@@ -128,6 +128,9 @@ export const templates = {
"</div>" +
"<div id='play-pause'>" + "</div>" +
"</div>" +
"<div>" +
"<button id='role-info-button'>View Role Info <img src='/images/info.svg'</button>" +
"</div>" +
"</div>" +
"<div id='game-role' style='display:none'>" +
"<h4 id='role-name'></h4>" +

View File

@@ -7,7 +7,7 @@ const create = () => {
let deckManager = new DeckStateManager();
let gameCreationStepManager = new GameCreationStepManager(deckManager);
loadDefaultCards(deckManager);
loadCustomRoles(deckManager);
//loadCustomRoles(deckManager);
gameCreationStepManager.renderStep("creation-step-container", 1);
}

View File

@@ -41,7 +41,6 @@ function prepareGamePage(environment, socket, timerWorker) {
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 2);
console.log(gameState);
userId = gameState.client.cookie;
UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(stateBucket, socket);
@@ -66,7 +65,7 @@ function prepareGamePage(environment, socket, timerWorker) {
ModalManager.dispelModal("change-name-modal", "change-name-modal-background")
toast('Name set.', 'success', true, true, 5);
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer);
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
}
})
} else {
@@ -82,10 +81,10 @@ function prepareGamePage(environment, socket, timerWorker) {
function initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager) {
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer);
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
}
function processGameState (currentGameState, userId, socket, gameStateRenderer) {
function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker) {
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
switch (currentGameState.status) {
case globals.STATUS.LOBBY:
@@ -133,7 +132,9 @@ function processGameState (currentGameState, userId, socket, gameStateRenderer)
break;
}
if (currentGameState.timerParams) {
socket.emit(globals.COMMANDS.GET_TIME_REMAINING, currentGameState.accessCode);
socket.emit(globals.COMMANDS.GET_TIME_REMAINING, currentGameState.accessCode, (timeRemaining, paused) => {
gameTimerManager.processTimeRemaining(timeRemaining, paused, timerWorker);
});
} else {
document.querySelector('#game-timer')?.remove();
document.querySelector('label[for="game-timer"]')?.remove();
@@ -183,7 +184,7 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
stateBucket.currentGameState.client.cookie,
function (gameState) {
stateBucket.currentGameState = gameState;
processGameState(stateBucket.currentGameState, gameState.client.cookie, socket, gameStateRenderer);
processGameState(stateBucket.currentGameState, gameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
}
);
});
@@ -252,7 +253,7 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => {
propagateNameChange(stateBucket.currentGameState, name, personId);
updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer);
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer);
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
});
}
@@ -260,7 +261,7 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
socket.on(globals.COMMANDS.END_GAME, (people) => {
stateBucket.currentGameState.people = people;
stateBucket.currentGameState.status = globals.STATUS.ENDED;
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer);
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
});
}
}

View File

@@ -31,6 +31,19 @@ body {
flex-direction: column;
align-items: flex-start;
margin: 0 auto;
position: relative;
}
.bmc-btn {
height: 40px !important;
border-radius: 3px !important;
font-size: 18px !important;
min-width: 180px !important;
padding: 0 17px !important;
}
.bmc-btn-text {
margin: 0 !important;
}
h1 {
@@ -76,6 +89,7 @@ h3 {
text-decoration: none;
cursor: pointer;
font-family: 'diavlo', sans-serif;
margin: 0 0.25em;
}
#footer a:hover {

View File

@@ -257,6 +257,7 @@ input[type="number"] {
#game-creation-container {
width: 95%;
max-width: 60em;
position: relative;
}
#tracker-container {

View File

@@ -58,6 +58,7 @@ body {
#lobby-header {
margin-bottom: 1em;
max-width: 95%;
}
h1 {
@@ -115,13 +116,6 @@ h1 {
}
#game-link {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
margin-top: 10px;
padding: 7px;
@@ -134,6 +128,13 @@ h1 {
transition: background-color 0.2s;
}
#game-link > div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 95%;
}
.role-info-name {
display: flex;
}
@@ -277,6 +278,9 @@ h1 {
font-size: 35px;
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
border: 1px solid #747474;
min-width: 5em;
display: flex;
justify-content: center;
}
#game-timer.low {

View File

@@ -1,7 +1,7 @@
.modal {
border-radius: 2px;
text-align: center;
position: fixed;
position: absolute;
width: 85%;
z-index: 100;
top: 50%;
@@ -11,7 +11,6 @@
align-items: center;
justify-content: center;
max-width: 25em;
height: fit-content;
font-family: sans-serif;
flex-direction: column;
padding: 1em;
@@ -49,3 +48,11 @@
justify-content: space-between;
flex-direction: row;
}
#change-name-modal, #transfer-mod-modal, #role-info-modal {
position: fixed;
}
#role-info-modal, #transfer-mod-modal {
max-height: 80%;
}

View File

@@ -62,8 +62,8 @@
</div>
<footer id="footer" class="game-page-footer">
<div>
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-32px.png'/></a>
<a href="mailto:leohfx@gmail.com?Subject=Werewolf App Question" target="_top"><img src='/images/email.svg'/></a>
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-120px-plus.png'/></a>
<a href="mailto:play.werewolf.contact@gmail.com?Subject=Werewolf App" target="_top"><img src='/images/email.svg'/></a>
</div>
<div>
<p>Werewolf created by Andrew Plotkin</p>

View File

@@ -36,13 +36,10 @@
</form>
</div>
<footer id="footer">
<a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/alecm33">
<span alt="Buy me a beer">🍺</span>
<span style="margin-left:5px;">Buy me a beer</span>
</a>
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="alecm33" data-color="#333243" data-emoji="" data-font="Lato" data-text="Buy me a coffee" data-outline-color="#ffffff" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
<div>
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-32px.png'/></a>
<a href="mailto:leohfx@gmail.com?Subject=Werewolf App Question" target="_top"><img src='/images/email.svg'/></a>
<a href="https://github.com/AlecM33/Werewolf"><img src='/images/GitHub-Mark-Light-120px-plus.png'/></a>
<a href="mailto:play.werewolf.contact@gmail.com?Subject=Werewolf App" target="_top"><img src='/images/email.svg'/></a>
</div>
<div>
<p>Werewolf created by Andrew Plotkin</p>

View File

@@ -12,6 +12,7 @@ module.exports = {
},
mode: "development",
node: false,
devtool: 'source-map',
module: {
rules: [
{

View File

@@ -1,4 +1,5 @@
const path = require('path');
'use strict';
module.exports = {
entry: {
@@ -12,7 +13,6 @@ module.exports = {
},
mode: "production",
node: false,
devtool: 'source-map',
module: {
rules: [
{

919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,25 +15,26 @@
"test:unit": "jasmine",
"test:e2e": "node browsertest.js"
},
"engines": {
"node": ">=14.0.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"express-force-https": "^1.0.0",
"jasmine": "^3.5.0",
"socket.io": "^4.4.0"
},
"devDependencies": {
"@babel/core": "^7.16.5",
"@babel/plugin-transform-object-assign": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"acorn": "^8.6.0",
"babel-loader": "^8.2.3",
"css-loader": "^6.5.1",
"express": "^4.17.1",
"express-force-https": "^1.0.0",
"express-rate-limit": "^6.0.1",
"open": "^7.0.3",
"socket.io": "^4.4.0",
"socket.io-client": "^4.4.0",
"style-loader": "^3.3.1",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-remove-debug": "^0.1.0"
}
},
"devDependencies": {}
}

View File

@@ -3,9 +3,21 @@ const router = express.Router();
const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug');
const logger = require('../modules/Logger')(debugMode);
const GameManager = require('../modules/GameManager.js');
const rateLimit = require('express-rate-limit').default
const gameManager = new GameManager().getInstance();
const apiLimiter = rateLimit({
windowMs: 600000,
max: 3,
standardHeaders: true,
legacyHeaders: false,
})
if (process.env.NODE_ENV.trim() === 'production') { // in prod, limit clients to creating 3 games per 10 minutes.
router.use('/create', apiLimiter);
}
router.post('/create', function (req, res) {
logger.debug('Received request to create new game: ' + JSON.stringify(req.body, null, 4));
const gameCreationPromise = gameManager.createGame(req.body, false);

View File

@@ -2,6 +2,7 @@ const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6,
CLOCK_TICK_INTERVAL_MILLIS: 10,
STALE_GAME_HOURS: 12,
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment',
@@ -50,7 +51,7 @@ const globals = {
TRACE: "trace"
},
GAME_PROCESS_COMMANDS: {
END_GAME: "endGame",
END_TIMER: "endTimer",
START_GAME: "startGame",
START_TIMER: "startTimer",
PAUSE_TIMER: "pauseTimer",

View File

@@ -3,7 +3,6 @@ const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const socketIO = require('socket.io');
const app = express();
const bodyParser = require('body-parser');
const GameManager = require('./modules/GameManager.js');
@@ -62,10 +61,31 @@ if (localServer) {
app.use(secure);
}
const io = socketIO(main);
app.set('port', port);
let io;
if (process.env.NODE_ENV.trim() === 'development') {
io = require("socket.io")(main, {
cors: {
origin: "http://localhost:" + port,
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "X-Requested-With", "Accept"],
credentials: false
}
});
} else {
io = require("socket.io")(main, {
cors: {
origin: ["https://playwerewolf.uk.r.appspot.com", "wss://playwerewolf.uk.r.appspot.com"],
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "X-Requested-With", "Accept"],
credentials: false
},
transports: ["polling"]
});
}
const inGame = io.of('/in-game');
@@ -76,7 +96,6 @@ const gameManager = new GameManager(logger, globals.ENVIRONMENT.LOCAL).getInstan
/* Instantiate the singleton queue manager */
//const queueManager = new QueueManager(matchmaking, logger).getInstance();
/* api endpoints */
const games = require('./api/GamesAPI');
app.use('/api/games', games);

View File

@@ -18,12 +18,10 @@ class ActiveGameRunner {
this.timerThreads[game.accessCode] = gameProcess;
gameProcess.on('message', (msg) => {
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.END_GAME:
//game.status = globals.STATUS.ENDED;
case globals.GAME_PROCESS_COMMANDS.END_TIMER:
game.timerParams.paused = false;
game.timerParams.timeRemaining = 0;
this.logger.trace('PARENT: END GAME');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
this.logger.trace('PARENT: END TIMER');
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
game.timerParams.paused = true;

View File

@@ -178,6 +178,8 @@ class GameManager {
this.logger.error('Tried to create game with invalid options: ' + JSON.stringify(gameParams));
return Promise.reject('Tried to create game with invalid options: ' + gameParams);
} else {
// to avoid excessive memory build-up, every time a game is created, check for and purge any stale games.
pruneStaleGames(this.activeGameRunner.activeGames, this.activeGameRunner.timerThreads, this.logger);
const newAccessCode = this.generateAccessCode();
let moderator = initializeModerator(UsernameGenerator.generate(), gameParams.hasDedicatedModerator);
if (gameParams.timerParams !== null) {
@@ -192,6 +194,7 @@ class GameManager {
moderator,
gameParams.timerParams
);
this.activeGameRunner.activeGames[newAccessCode].createTime = new Date().toJSON();
return Promise.resolve(newAccessCode);
}
}
@@ -359,12 +362,11 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
logger.trace('this person is already associated with a socket connection');
let alreadyConnectedSocket = namespace.connected[matchingPerson.socketId];
if (alreadyConnectedSocket && alreadyConnectedSocket.leave) {
alreadyConnectedSocket.leave(accessCode, ()=> {
logger.trace('kicked existing connection out of room ' + accessCode);
socket.join(accessCode);
matchingPerson.socketId = socket.id;
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger));
})
alreadyConnectedSocket.leave(accessCode);
logger.trace('kicked existing connection out of room ' + accessCode);
socket.join(accessCode);
matchingPerson.socketId = socket.id;
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger));
}
}
}
@@ -398,7 +400,7 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
}
} else {
rejectClientRequestForGameState(ackFn);
logger.trace('the game' + accessCode + ' was not found');
logger.trace('the game ' + accessCode + ' was not found');
}
}
@@ -442,4 +444,21 @@ function isNameTaken(game, name) {
|| (game.spectators.find((spectator) => spectator.name.toLowerCase().trim() === processedName))
}
function pruneStaleGames(activeGames, timerThreads, logger) {
for (const [accessCode, game] of Object.entries(activeGames)) {
if (game.createTime) {
let createDate = new Date(game.createTime);
if (createDate.setHours(createDate.getHours() + globals.STALE_GAME_HOURS) < Date.now()) { // clear games created more than 12 hours ago
logger.info('PRUNING STALE GAME ' + accessCode);
delete activeGames[accessCode];
if (timerThreads[accessCode]) {
logger.info('KILLING STALE TIMER PROCESS FOR ' + accessCode);
timerThreads[accessCode].kill();
delete timerThreads[accessCode];
}
}
}
}
}
module.exports = Singleton;

View File

@@ -16,7 +16,7 @@ process.on('message', (msg) => {
);
timer.runTimer().then(() => {
logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER });
process.exit(0);
});
@@ -31,7 +31,7 @@ process.on('message', (msg) => {
case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
timer.resumeTimer().then(() => {
logger.debug('Timer finished for ' + msg.accessCode);
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_TIMER });
process.exit(0);
});
logger.trace('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');

View File

@@ -14,5 +14,13 @@ router.get('/game/:code', function (request, response) {
response.sendFile(path.join(__dirname, '../../client/src/views/game.html'));
});
router.get('/liveness_check', (req, res) => {
res.sendStatus(200);
});
router.get('/readiness_check', (req, res) => {
res.sendStatus(200);
});
module.exports = router;