basic game creation

This commit is contained in:
Alec
2021-11-09 22:54:44 -05:00
parent 226b0c25e2
commit 3dc2cca465
28 changed files with 4411 additions and 43 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea
node_modules/*
.vscode/launch.json
package-lock.json

BIN
client/images/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

7
client/model/Game.js Normal file
View File

@@ -0,0 +1,7 @@
export class Game {
constructor(deck, hasTimer, timerParams=null) {
this.deck = deck;
this.hasTimer = hasTimer;
this.timerParams = timerParams;
}
}

View File

@@ -5,8 +5,8 @@ export class DeckStateManager {
}
addToDeck(role) {
let option = this.customRoleOptions.find((option) => option.role === role)
let existingCard = this.deck.find((card) => card.role === role)
let option = this.customRoleOptions.find((option) => option.role === role);
let existingCard = this.deck.find((card) => card.role === role);
if (option && !existingCard) {
option.quantity = 0;
this.deck.push(option);
@@ -32,11 +32,21 @@ export class DeckStateManager {
}
}
getCurrentDeck() { return this.deck }
getCurrentDeck() { return this.deck; }
getCard(role) { return this.deck.find((card) => card.role === role) }
getCard(role) {
return this.deck.find(
(card) => card.role.toLowerCase().trim() === role.toLowerCase().trim()
);
}
getCurrentCustomRoleOptions() { return this.customRoleOptions }
getCurrentCustomRoleOptions() { return this.customRoleOptions; }
getCustomRoleOption(role) {
return this.customRoleOptions.find(
(option) => option.role.toLowerCase().trim() === role.toLowerCase().trim()
)
};
getDeckSize() {
let total = 0;

View File

@@ -1,5 +1,6 @@
export const ModalManager = {
displayModal: displayModal
displayModal: displayModal,
dispelModal: dispelModal
}
function displayModal(modalId, backgroundId, closeButtonId) {

View File

@@ -0,0 +1,40 @@
export const XHRUtility =
{
standardHeaders: [['Content-Type', 'application/json'], ['Accept', 'application/json'], ['X-Requested-With', 'XMLHttpRequest']],
// Easily make XHR calls with a promise wrapper. Defaults to GET and MIME type application/JSON
xhr (url, method = 'GET', headers, body = null) {
if (headers === undefined || headers === null) {
headers = this.standardHeaders;
}
if (typeof url !== 'string' || url.trim().length < 1) {
return Promise.reject('Cannot request with empty URL: ' + url);
}
const req = new XMLHttpRequest();
req.open(method, url.trim());
for (const hdr of headers) {
if (hdr.length !== 2) continue;
req.setRequestHeader(hdr[0], hdr[1]);
}
return new Promise((resolve, reject) => {
req.onload = function () {
const response = {
status: this.status,
statusText: this.statusText,
content: this.responseText
};
if (this.status >= 200 && this.status < 400) {
resolve(response);
} else {
reject(response);
}
};
body ? req.send(body) : req.send();
});
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,8 @@ import { ModalManager } from "../modules/ModalManager.js";
import { defaultCards } from "../config/defaultCards.js";
import { customCards } from "../config/customCards.js";
import { DeckStateManager } from "../modules/DeckStateManager.js";
import {XHRUtility} from "../modules/XHRUtility.js";
import {Game} from "../model/Game.js";
export const create = () => {
let deckManager = new DeckStateManager();
@@ -10,6 +12,35 @@ export const create = () => {
loadCustomCards(deckManager);
document.getElementById("game-form").onsubmit = (e) => {
e.preventDefault();
let timerBool = hasTimer();
let timerParams = timerBool
? {
hours: document.getElementById("game-hours").value,
minutes: document.getElementById("game-minutes").value
}
: null;
if (deckManager.getDeckSize() >= 5) {
createGameForHosting(
deckManager.getCurrentDeck().filter((card) => card.quantity > 0),
timerBool,
timerParams
);
} else {
toast("You must include enough cards for 5 players.", "error", true);
}
}
document.getElementById("add-role-form").onsubmit = (e) => {
e.preventDefault();
let name = document.getElementById("role-name").value.trim();
let description = document.getElementById("role-description").value.trim();
if (!deckManager.getCustomRoleOption(name)) { // confirm there is no existing custom role with the same name
deckManager.addToCustomRoleOptions({role: name, description: description});
updateCustomRoleOptionsList(deckManager, document.getElementById("deck-select"))
ModalManager.dispelModal("add-role-modal", "add-role-modal-background");
toast("Role Added", "success", true);
} else {
toast("There is already a custom role with this name.", "error", true);
}
}
document.getElementById("custom-role-btn").addEventListener(
"click", () => {
@@ -47,23 +78,25 @@ function loadCustomCards(deckManager) {
});
let selectEl = document.createElement("select");
selectEl.setAttribute("id", "deck-select");
selectEl.setAttribute("class", "ui search dropdown");
addOptionsToList(customCards, selectEl);
form.appendChild(selectEl);
let submitBtn = document.createElement("input");
submitBtn.setAttribute("type", "submit");
submitBtn.setAttribute("value", "Add Role to Deck");
submitBtn.setAttribute("value", "Add Role");
submitBtn.addEventListener('click', (e) => {
e.preventDefault();
if (selectEl.selectedIndex > 0) {
if (selectEl.value && selectEl.value.length > 0) {
deckManager.addToDeck(selectEl.value);
let cardEl = constructCompactDeckBuilderElement(deckManager.getCard(selectEl.value), deckManager);
updateCustomRoleOptionsList(deckManager, selectEl);
document.getElementById("deck").appendChild(cardEl);
document.querySelector("#add-card-to-deck-form .text").innerText = "";
}
})
form.appendChild(submitBtn);
$('.ui.dropdown')
.dropdown();
deckManager.customRoleOptions = customCards;
}
@@ -73,12 +106,7 @@ function updateCustomRoleOptionsList(deckManager, selectEl) {
}
function addOptionsToList(options, selectEl) {
let noneSelected = document.createElement("option");
noneSelected.innerText = "None selected"
noneSelected.disabled = true;
noneSelected.selected = true;
selectEl.appendChild(noneSelected);
for (let i = 0; i < options.length; i ++) { // each dropdown should include every
for (let i = 0; i < options.length; i ++) {
let optionEl = document.createElement("option");
optionEl.setAttribute("value", customCards[i].role);
optionEl.innerText = customCards[i].role;
@@ -91,30 +119,61 @@ function constructCompactDeckBuilderElement(card, deckManager) {
cardContainer.setAttribute("class", "compact-card");
cardContainer.setAttribute("id", "card-" + card.role);
cardContainer.setAttribute("id", "card-" + card.role);
cardContainer.setAttribute("id", "card-" + card.role.replaceAll(' ', '-'));
cardContainer.innerHTML =
"<div class='compact-card-left'>" +
"<p>-</p>" +
"</div>" +
"<div class='compact-card-header'>" +
"<p class='card-role'>" + card.role + "</p>" +
"<p class='card-role'></p>" +
"<div class='card-quantity'>0</div>" +
"</div>" +
"<div class='compact-card-right'>" +
"<p>+</p>" +
"</div>";
cardContainer.querySelector('.card-role').innerText = card.role;
cardContainer.querySelector('.compact-card-right').addEventListener('click', () => {
deckManager.addCopyOfCard(card.role);
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players';
if (deckManager.getCard(card.role).quantity > 0) {
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.add('selected-card')
}
});
cardContainer.querySelector('.compact-card-left').addEventListener('click', () => {
deckManager.removeCopyOfCard(card.role);
cardContainer.querySelector('.card-quantity').innerText = deckManager.getCard(card.role).quantity;
document.querySelector('label[for="deck"]').innerText = 'Game Deck: ' + deckManager.getDeckSize() + ' Players';
if (deckManager.getCard(card.role).quantity === 0) {
document.getElementById('card-' + card.role.replaceAll(' ', '-')).classList.remove('selected-card')
}
});
return cardContainer;
}
function hasTimer() {
return document.getElementById("game-hours").value.length > 0 || document.getElementById("game-minutes").value.length > 0
}
function createGameForHosting(deck, hasTimer, timerParams) {
XHRUtility.xhr(
'/api/games/create',
'POST',
null,
JSON.stringify(
new Game(deck, hasTimer, timerParams)
)
)
.then((res) => {
if (res
&& typeof res === 'object'
&& Object.prototype.hasOwnProperty.call(res, 'content')
&& typeof res.content === 'string'
) {
window.location = ('/games/' + res.content);
}
});
}

View File

@@ -1,3 +1,2 @@
export const home = () => {
};

View File

@@ -58,6 +58,11 @@ label {
font-weight: normal;
}
textarea, input {
font-family: 'signika-negative', sans-serif;
font-size: 16px;
}
button, input[type="submit"] {
font-family: 'signika-negative', sans-serif !important;
padding: 10px;
@@ -76,3 +81,65 @@ button:hover, input[type="submit"]:hover {
input {
padding: 10px;
}
.info-message {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
z-index: 1000;
padding: 10px;
border-radius: 3px;
font-family: 'signika-negative', sans-serif;
font-weight: 100;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 25%);
left: 0;
right: 0;
width: fit-content;
max-width: 30em;
min-width: 15em;
font-size: 20px;
margin: 0 auto;
animation: fade-in-slide-down-then-exit 6s ease;
animation-fill-mode: forwards;
animation-direction: normal;
}
#navbar {
display: flex;
align-items: center;
padding: 5px;
margin-bottom: -2em;
width: 100%;
}
#navbar a {
color: #f7f7f7;
text-decoration: none;
cursor: pointer;
font-size: 25px;
}
#navbar a:hover {
color: gray;
}
@keyframes fade-in-slide-down-then-exit {
0% {
opacity: 0;
transform: translateY(-20px);
}
5% {
opacity: 1;
transform: translateY(0px);
}
95% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}

View File

@@ -1,4 +1,5 @@
.compact-card {
border: 2px solid transparent;
text-align: center;
cursor: pointer;
position: relative;
@@ -28,6 +29,11 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: 8em;
font-size: 16px;
}
.selected-card {
border: 2px solid #0075F2;
}
.compact-card-right p {
@@ -51,6 +57,7 @@
.compact-card .card-quantity {
text-align: center;
margin: 0;
font-size: 20px;
}
.compact-card-header {
@@ -78,6 +85,8 @@
#deck {
display: flex;
flex-wrap: wrap;
overflow: auto;
max-height: 20em;
}
form {
@@ -116,13 +125,20 @@ select {
label[for="game-time"], label[for="add-card-to-deck-form"], label[for="deck"] {
color: #0075F2;
font-size: 30px;
font-size: 20px;
border-radius: 3px;
margin-bottom: 10px;
font-weight: bold;
}
#create-game{
color: #45a445;
font-size: 30px;
margin-top: 2em;
padding: 10px 50px;
margin: 1em auto 3em auto;
display: flex;
}
.dropdown {
margin: 0.5em;
}

View File

@@ -0,0 +1,52 @@
body {
align-items: center;
}
button {
padding: 20px;
font-size: 25px;
margin-bottom: 1em;
}
form {
display: flex;
flex-wrap: wrap;
margin: 1em 0;
padding: 10px;
border-radius: 3px;
background-color: #1f1f1f;
}
h3 {
max-width: 30em;
font-size: 16px;
margin-bottom: 2em;
}
img {
max-width: 650px;
width: 63vw;
min-width: 430px;
}
form > div {
margin: 1em;
}
input[type="text"] {
background-color: transparent;
border: 1px solid white;
border-radius: 3px;
color: #f7f7f7;
}
#join-container > label {
font-size: 35px;
font-family: 'diavlo', sans-serif;
color: #ab2626;
filter: drop-shadow(2px 2px 4px black);
}
label[for="room-code"], label[for="player-name"] {
margin-right: 0.5em;
}

View File

@@ -8,7 +8,7 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f7f7f7;
background-color: #23282b;
align-items: center;
justify-content: center;
max-width: 17em;
@@ -40,3 +40,14 @@
display: flex;
font-size: 16px;
}
#modal-button-container {
display: flex;
width: 100%;
justify-content: space-between;
flex-direction: row;
}
#modal-button-container input {
color: #21ba45;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,21 +16,37 @@
<link rel="stylesheet" href="../styles/GLOBAL.css">
<link rel="stylesheet" href="../styles/create.css">
<link rel="stylesheet" href="../styles/modal.css">
<link rel="stylesheet" href="../styles/third_party/dropdown.min.css">
<link rel="stylesheet" href="../styles/third_party/transition.min.css">
<link rel="stylesheet" href="../styles/third_party/search.min.css">
<script src="../modules/third_party/jQuery/jquery-3.6.0.min.js"></script>
<script src="../modules/third_party/semantic-ui/transition.min.js"></script>
<script src="../modules/third_party/semantic-ui/dropdown.min.js"></script>
<script src="../modules/third_party/semantic-ui/search.min.js"></script>
</head>
<body>
<div id="navbar">
<a href="/">
<img alt="logo" src="../images/Werewolf_Small.png"/>
</a>
<a href="/">Home</a>
</div>
<div id="add-role-modal-background" class="modal-background" style="display: none"></div>
<div id="add-role-modal" class="modal" style="display: none">
<form id="add-role-form">
<div>
<label for="role-name">Role Name</label>
<input id="role-name" type="text" placeholder="Name your role..."/>
<input id="role-name" type="text" placeholder="Name your role..." required/>
</div>
<div>
<label for="role-description">Description</label>
<textarea style="resize:none" id="role-description" rows="10" cols="30" placeholder="Describe your role..."></textarea>
<textarea style="resize:none" id="role-description" rows="10" cols="30" placeholder="Describe your role..." required></textarea>
</div>
<div id="modal-button-container">
<button id="close-modal-button">Close</button>
<input type="submit" id="create-role-button" value="Create Role"/>
</div>
</form>
<button id="close-modal-button">Close</button>
</div>
<h1>Create A Game</h1>
<h3>
@@ -51,10 +67,10 @@
<div id="game-time">
<label for="game-hours">Hours (max 5)</label>
<input type="number" id="game-hours" name="game-hours"
min="0" max="5" placeholder="e.g. 1"/>
min="0" max="5" />
<label for="game-hours">Minutes</label>
<input type="number" id="game-minutes" name="game-minutes"
min="0" max="60" placeholder="e.g. 30"/>
min="1" max="60" />
</div>
</div>
<input id="create-game" type="submit" value="Create"/>

View File

@@ -20,9 +20,24 @@
<link rel="stylesheet" href="../styles/home.css">
</head>
<body>
<img src="../images/logo.gif"/>
<h3>This is a tool to run games of Werewolf when not in-person, or when you don't possess a deck of cards. Create a game and moderate, or join one and have a role dealt to your device.</h3>
<a href="/create">
<button>Create A Game</button>
</a>
<div id="join-container">
<label for="join-form">Join A Game</label>
<form id="join-form">
<div>
<label for="room-code">Room Code</label>
<input id="room-code" type="text" placeholder="six-character code..." required/>
</div>
<div>
<label for="player-name">Your Name</label>
<input id="player-name" type="text" required/>
</div>
</form>
</div>
<script type="module">
import { home } from "../scripts/home.js";
home();

View File

@@ -12,11 +12,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"cron": "^1.7.1",
"express": "^4.17.1",
"express-force-https": "^1.0.0",
"jasmine": "^3.5.0",
"socket.io": "^2.2.0"
"socket.io": "^2.4.1"
},
"devDependencies": {
"open": "^7.0.3"

20
server/api/GamesAPI.js Normal file
View File

@@ -0,0 +1,20 @@
const express = require('express');
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 gameManager = new GameManager().getInstance();
router.post('/create', function (req, res) {
const gameCreationPromise = gameManager.createGame(req.body, false);
gameCreationPromise.then((result) => {
if (result instanceof Error) {
res.status(500).send();
} else {
res.send(result); // game was created successfully, and access code was returned
}
});
});
module.exports = router;

6
server/config/globals.js Normal file
View File

@@ -0,0 +1,6 @@
const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6,
};
module.exports = globals;

View File

@@ -7,7 +7,7 @@ const socketIO = require('socket.io');
const app = express();
let main;
const bodyParser = require('body-parser');
// const GameManager = require('./modules/managers/GameManager.js');
const GameManager = require('./modules/GameManager.js');
// const QueueManager = require('./modules/managers/QueueManager');
app.use(bodyParser.json()); // to support JSON-encoded bodies
@@ -55,17 +55,15 @@ const io = socketIO(main);
app.set('port', port);
/* Instantiate the singleton game manager */
//const gameManager = new GameManager(logger).getInstance();
const gameManager = new GameManager(logger).getInstance();
/* Instantiate the singleton queue manager */
//const queueManager = new QueueManager(matchmaking, logger).getInstance();
/* api endpoints */
// const games = require('./api/GamesAPI');
// const words = require('./api/WordsAPI');
// app.use('/api/games', games);
// app.use('/api/words', words);
const games = require('./api/GamesAPI');
app.use('/api/games', games);
/* serve all the app's pages */
app.use('/manifest.json', (req, res) => {
@@ -88,7 +86,6 @@ app.use(function (req, res) {
res.sendFile(path.join(__dirname, '../client/views/404.html'));
});
// Starts the main.
main.listen(port, function () {
logger.log(`Starting server on port ${port} http://localhost:${port}` );
});

View File

@@ -0,0 +1,52 @@
const globals = require('../config/globals');
class GameManager {
constructor (logger) {
this.logger = logger;
//this.activeGameRunner = new ActiveGameRunner(this.postGame).getInstance();
//this.gameSocketUtility = GameSocketUtility;
}
createGame = (gameParams) => {
const expectedKeys = ['deck', 'hasTimer', 'timerParams'];
if (typeof gameParams !== 'object' || expectedKeys.some((key) => !Object.keys(gameParams).includes(key))) {
this.logger.error('Tried to create game with invalid options: ' + JSON.stringify(gameParams));
return Promise.reject('Tried to create game with invalid options: ' + gameParams);
} else {
const newAccessCode = this.generateAccessCode();
return Promise.resolve(newAccessCode);
}
}
generateAccessCode = () => {
const numLetters = globals.ACCESS_CODE_CHAR_POOL.length;
const codeDigits = [];
let iterations = globals.ACCESS_CODE_LENGTH;
while (iterations > 0) {
iterations--;
codeDigits.push(globals.ACCESS_CODE_CHAR_POOL[getRandomInt(numLetters)]);
}
return codeDigits.join('');
}
}
function getRandomInt (max) {
return Math.floor(Math.random() * Math.floor(max));
}
class Singleton {
constructor (logger) {
if (!Singleton.instance) {
logger.log('CREATING SINGLETON GAME MANAGER');
Singleton.instance = new GameManager(logger);
}
}
getInstance () {
return Singleton.instance;
}
}
module.exports = Singleton;

View File

@@ -3,11 +3,11 @@ const staticRouter = express.Router();
const path = require('path');
const checkIfFileExists = require("./util");
staticRouter.use('/styles/*', (req, res) => {
staticRouter.use('/styles/**', (req, res) => {
let filePath = path.join(__dirname, ('../../client/' + req.baseUrl));
let extension = path.extname(filePath);
checkIfFileExists(filePath).then((fileExists) => {
if (fileExists && (extension === '.css')) {
if (fileExists && (extension === '.css' || extension === '.min.css')) {
res.sendFile(filePath);
} else {
res.sendStatus(404);
@@ -27,11 +27,11 @@ staticRouter.use('/client/webfonts/*', (req, res) => {
});
});
staticRouter.use('/client/images/*', (req, res) => {
let filePath = path.join(__dirname, ('../' + req.baseUrl));
staticRouter.use('/images/*', (req, res) => {
let filePath = path.join(__dirname, ('../../client/' + req.baseUrl));
let extension = path.extname(filePath);
checkIfFileExists(filePath).then((fileExists) => {
if (fileExists && (extension === '.svg' || extension === '.png' || extension === '.jpg')) {
if (fileExists && (extension === '.svg' || extension === '.png' || extension === '.jpg' || extension === '.gif')) {
res.sendFile(filePath);
} else {
res.sendStatus(404);
@@ -88,7 +88,19 @@ staticRouter.use('/config/*', (req, res) => {
});
});
staticRouter.use('/modules/*', (req, res) => {
staticRouter.use('/modules/**', (req, res) => {
let filePath = path.join(__dirname, ('../../client/' + req.baseUrl));
let extension = path.extname(filePath);
checkIfFileExists(filePath).then((fileExists) => {
if (fileExists && (extension === '.js' || extension === '.min.js')) {
res.sendFile(filePath);
} else {
res.sendFile('../views/404.html');
}
});
});
staticRouter.use('/model/**', (req, res) => {
let filePath = path.join(__dirname, ('../../client/' + req.baseUrl));
let extension = path.extname(filePath);
checkIfFileExists(filePath).then((fileExists) => {