Added bingo

- added bingo front and backend script
- added sessions
- added configuration files (default-config.yaml is needed, config.yaml for custom configuration)
pull/3/head
Julius 6 years ago
parent 229a6b6ee7
commit 5cde198890

1
.gitignore vendored

@ -4,3 +4,4 @@ bin
node_modules
scripts/*
tmp
config.yaml

@ -3,26 +3,50 @@ const createError = require('http-errors'),
path = require('path'),
cookieParser = require('cookie-parser'),
logger = require('morgan'),
compileSass = require('express-compile-sass'),
session = require('express-session'),
fsx = require('fs-extra'),
yaml = require('js-yaml'),
indexRouter = require('./routes/index'),
usersRouter = require('./routes/users'),
riddleRouter = require('./routes/riddle');
riddleRouter = require('./routes/riddle'),
bingoRouter = require('./routes/bingo');
let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml'));
if (fsx.existsSync('config.yaml'))
Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml')));
let app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.set('trust proxy', 1);
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
secret: settings.sessions.secret,
resave: false,
saveUninitialized: true,
cookie: { maxAge: settings.sessions.maxAge }
}));
app.use('/sass', compileSass({
root: './public/stylesheets/sass',
sourceMap: true,
watchFiles: true,
logToConsole: true
}));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use(/\/riddle(\/.*)?/, riddleRouter);
app.use(/\/bingo?.*/, bingoRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
@ -40,4 +64,4 @@ app.use(function(err, req, res, next) {
res.render('error');
});
module.exports = app;
app.listen(settings.port);

@ -0,0 +1,5 @@
sessions:
secret: averysecuresessionsecret
maxAge: 1000000
port: 3000

@ -12,6 +12,9 @@
"fs-extra": "^7.0.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"pug": "2.0.0-beta11"
"pug": "2.0.0-beta11",
"express-compile-sass": "latest",
"express-session": "latest",
"js-yaml": "latest"
}
}

@ -0,0 +1,88 @@
async function submitBingoWords() {
let textContent = document.querySelector('#bingo-textarea').value;
let words = textContent.replace(/[<>]/g, '').split('\n');
let size = document.querySelector('#bingo-grid-size').value;
let dimY = document.querySelector('#bingo-grid-y').value;
let response = await postLocData({
bingoWords: words,
size: size
});
let data = JSON.parse(response.data);
let gameid = data.id;
insertParam('game', gameid);
}
async function submitUsername() {
let username = document.querySelector('#username-input').value;
let response = await postLocData({
username: username
});
console.log(response);
}
async function submitWord(word) {
let response = await postLocData({
bingoWord: word
});
console.log(response);
let data = JSON.parse(response.data);
for (let row of data.fieldGrid) {
for (let field of row) {
document.querySelector(`.bingo-word-panel[b-word="${field.word}"]`)
.setAttribute('b-sub', field.submitted);
}
}
if (data.bingo) {
document.querySelector('#bingo-button').setAttribute('class', '');
}
}
async function submitBingo() {
let response = await postLocData({
bingo: true
});
let data = JSON.parse(response.data);
if (data.bingos.length > 0) {
displayWinner(data.users[data.bingos[0]].username);
clearInterval(refrInterval)
}
console.log(response);
}
async function refresh() {
let response = await postLocData({});
if (response.status === 400)
clearInterval(refrInterval);
let data = JSON.parse(response.data);
if (data.bingos.length > 0) {
displayWinner(data.users[data.bingos[0]].username);
clearInterval(refrInterval)
}
console.log(response);
}
function displayWinner(name) {
let winnerDiv = document.createElement('div');
let greyoverDiv = document.createElement('div');
let winnerSpan = document.createElement('span');
winnerDiv.setAttribute('class', 'popup');
greyoverDiv.setAttribute('class', 'greyover');
winnerSpan.innerText = `${name} has won!`;
winnerDiv.appendChild(winnerSpan);
document.body.append(greyoverDiv);
document.body.appendChild(winnerDiv);
}
window.onload = () => {
if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000);
}
let gridSizeElem = document.querySelector('#bingo-grid-size');
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;
gridSizeElem.oninput = () => {
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;
};
};

@ -0,0 +1,45 @@
function postLocData(postBody) {
let request = new XMLHttpRequest();
return new Promise((res, rej) => {
request.onload = () => {
res({
status: request.status,
data: request.responseText
});
};
request.onerror = () => {
rej(request.error);
};
request.open('POST', '#', true);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(postBody));
});
}
function insertParam(key, value) {
key = encodeURI(key);
value = encodeURI(value);
let kvp = document.location.search.substr(1).split('&');
let i = kvp.length;
let x;
while (i--) {
x = kvp[i].split('=');
if (x[0] === key) {
x[1] = value;
kvp[i] = x.join('=');
break;
}
}
if (i < 0) {
kvp[kvp.length] = [key, value].join('=');
}
document.location.search = kvp.join('&');
}

@ -1,24 +1,3 @@
function postLocData(postBody) {
let request = new XMLHttpRequest();
return new Promise((res, rej) => {
request.onload = () => {
res({
status: request.status,
data: request.responseText
});
};
request.onerror = () => {
rej(request.error);
};
request.open('POST', '#', true);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(postBody));
});
}
async function startSubredditDownload(subredditName) {
let data = await postLocData({
subreddit: subredditName
@ -45,13 +24,13 @@ async function refreshDownloadInfo(downloadId) {
} else {
let dlLink = document.createElement('a');
dlLink.setAttribute('href', response.file);
dlLink.setAttribute('filename', `${subredditName}`);
for (let cNode of dlDiv.childNodes)
dlLink.appendChild(cNode);
dlLink.setAttribute('download', `${subredditName}`);
dlLink.innerHTML = dlDiv.innerHTML;
dlDiv.innerHTML = '';
dlDiv.appendChild(dlLink);
setTimeout(() => {
dlDiv.remove();
}, 30000);
}, 300000);
}
}
@ -66,13 +45,13 @@ async function submitDownload() {
document.querySelector('#download-list').prepend(dlDiv);
let subnameSpan = document.createElement('span');
subnameSpan.innerText = subredditName;
subnameSpan.setAttribute('class', 'subredditName');
subnameSpan.innerText = 'r/'+subredditName;
subnameSpan.setAttribute('class', 'subredditName tableRow');
dlDiv.appendChild(subnameSpan);
let dlStatusSpan = document.createElement('span');
dlStatusSpan.innerText = response.status;
dlStatusSpan.setAttribute('class', 'downloadStatus');
dlStatusSpan.setAttribute('class', 'downloadStatus tableRow');
dlDiv.appendChild(dlStatusSpan);
await refreshDownloadInfo(response.id);

@ -0,0 +1,62 @@
@import ../mixins
@import ../vars
button
margin: 1rem
textarea
@include default-element
display: block
margin: 1rem
border-radius: 0
height: 50%
width: 50%
font-size: 15pt
.number-input
width: 4rem
margin: 1rem
#words-container
display: table
.bingo-word-row
display: table-row
.bingo-word-panel
@include default-element
display: table-cell
padding: 3rem
transition-duration: 0.3s
max-width: 15rem
.bingo-word-panel:hover
background-color: darken($primary, 2%)
cursor: pointer
.bingo-word-panel:active
background-color: $primary
.bingo-word-panel[b-sub="true"]
background-color: forestgreen
.popup
@include default-element
height: 5%
width: 40%
top: 47.5%
left: 30%
text-align: center
transition-duration: 1s
span
margin: 2%
display: block
.greyover
width: 100%
height: 100%
position: fixed
z-index: 99
top: 0
left: 0
background-color: transparentize($primary, 0.5)

@ -0,0 +1,13 @@
.tableRow
display: table-row
.hidden
display: None
.popup
height: 60%
width: 40%
z-index: 1000
position: fixed
top: 20%
left: 30%

@ -0,0 +1,7 @@
@import vars
@mixin default-element
background: lighten($primary, 10%)
color: $primarySurface
border: 2px solid $primarySurface
border-radius: $borderRadius

@ -0,0 +1,22 @@
@import ../mixins
@import ../vars
#download-list
margin: 1rem 0
.download-container
@include default-element
display: inline-block
margin: 1rem
padding: 1rem
.subredditName
font-weight: bold
a
text-decoration: none
color: $primarySurface
#submit-download
margin: 0 1rem
#subreddit-input
margin: 0 1rem

@ -0,0 +1,28 @@
@import vars
@import classes
@import mixins
body
background-color: $primary
color: $primarySurface
font-size: 18pt
font-family: Arial, sans-serif
button
@include default-element
font-size: 20pt
padding: 10px
transition-duration: 0.2s
button:hover
background-color: darken($primary, 2%)
cursor: pointer
button:active
background-color: lighten($primary, 15%)
input
@include default-element
font-size: 20pt
background-color: lighten($primary, 10%)
padding: 9px

@ -0,0 +1,4 @@
$primary: #223
$primarySurface: white
$borderRadius: 20px

@ -0,0 +1,259 @@
const express = require('express'),
router = express.Router(),
cproc = require('child_process'),
fsx = require('fs-extra');
const rWordOnly = /^\w+$/;
let bingoSessions = {};
class BingoSession {
/**
* constructor
* @param words List<String>
* @param [size] Number
*/
constructor(words, size = 3) {
this.id = generateBingoId();
this.words = words;
this.gridSize = size;
this.users = {};
this.bingos = []; // array with the users that already had bingo
this.finished = false;
}
/**
* Adds a user to the session
* @param user
*/
addUser(user) {
let id = user.id;
this.users[id] = user;
}
}
class BingoUser {
constructor() {
this.id = generateBingoId();
this.game = null;
this.username = 'anonymous';
this.grids = {};
this.submittedWords = {};
}
}
class BingoWordField {
constructor(word) {
this.word = word;
this.submitted = false;
}
}
class BingoGrid {
constructor(wordGrid) {
this.wordGrid = wordGrid;
this.fieldGrid = wordGrid.map(x => x.map(y => new BingoWordField(y)));
return this;
}
}
/**
* Shuffles the elements in an array
* @param array {Array<*>}
* @returns {Array<*>}
*/
function shuffleArray(array) {
let counter = array.length;
while (counter > 0) {
let index = Math.floor(Math.random() * counter);
counter--;
let temp = array[counter];
array[counter] = array[index];
array[index] = temp;
}
return array;
}
/**
* Generates an id for a subreddit download.
* @returns {string}
*/
function generateBingoId() {
return Date.now().toString(16);
}
/**
* Generates a word grid with random word placements in the given dimensions
* @param dimensions {Array<Number>} - the dimensions of the grid
* @param words {Array<String>} - the words included in the grid
* @returns {BingoGrid}
*/
function generateWordGrid(dimensions, words) {
let shuffledWords = shuffleArray(words);
let grid = [];
for (let x = 0; x < dimensions[1]; x++) {
grid[x] = [];
for (let y = 0; y < dimensions[0]; y++) {
grid[x][y] = shuffledWords[(x * dimensions[0]) + y];
}
}
return (new BingoGrid(grid));
}
/**
* Sets the submitted parameter of the words in the bingo grid that match to true.
* @param word {String}
* @param bingoGrid {BingoGrid}
* @returns {boolean}
*/
function submitWord(word, bingoGrid) {
let results = bingoGrid.fieldGrid.find(x => x.find(y => (y.word === word))).find(x => x.word === word);
if (results) {
(results instanceof Array)? results.forEach(x => {x.submitted = true}): results.submitted = true;
checkBingo(bingoGrid);
return true;
}
return false;
}
/**
* Checks if a bingo exists in the bingo grid.
* @param bingoGrid {BingoGrid}
* @returns {boolean}
*/
function checkBingo(bingoGrid) {
let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted));
let diagonalBingo = true;
// diagonal check
for (let i = 0; i < fg.length; i++)
diagonalBingo = fg[i][i] && diagonalBingo;
if (diagonalBingo) {
bingoGrid.bingo = true;
return true;
}
diagonalBingo = true;
for (let i = 0; i < fg.length; i++)
diagonalBingo = fg[i][fg.length - i - 1] && diagonalBingo;
if (diagonalBingo) {
bingoGrid.bingo = true;
return true;
}
let bingoCheck = true;
// horizontal check
for (let row of fg) {
bingoCheck = true;
for (let field of row)
bingoCheck = field && bingoCheck;
if (bingoCheck)
break;
}
if (bingoCheck) {
bingoGrid.bingo = true;
return true;
}
bingoCheck = true;
// vertical check
for (let i = 0; i < fg.length; i++) {
bingoCheck = true;
for (let j = 0; j < fg.length; j++)
bingoCheck = fg[j][i] && bingoCheck;
if (bingoCheck)
break;
}
if (bingoCheck) {
bingoGrid.bingo = true;
return true;
}
return false;
}
// -- Router stuff
router.use((req, res, next) => {
if (!req.session.bingoUser) {
req.session.bingoUser = new BingoUser();
}
next();
});
router.get('/', (req, res) => {
let bingoUser = req.session.bingoUser;
if (req.query.game) {
let gameId = req.query.game;
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) {
bingoUser.game = gameId;
let bingoSession = bingoSessions[gameId];
bingoSession.addUser(bingoUser);
if (!bingoUser.grids[gameId]) {
bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words);
}
res.render('bingo/bingo-game', {grid: bingoUser.grids[gameId].wordGrid, username: bingoUser.username});
} else {
res.render('bingo/bingo-submit');
}
} else {
res.render('bingo/bingo-submit');
}
});
router.post('/', (req, res) => {
let data = req.body;
let gameId = req.query.game;
let bingoUser = req.session.bingoUser;
let bingoSession = bingoSessions[gameId];
if (data.bingoWords) {
let words = data.bingoWords;
let size = data.size;
let game = new BingoSession(words, size);
bingoSessions[game.id] = game;
setTimeout(() => { // delete the game after one day
delete bingoSessions[game.id];
}, 86400000);
res.send(game);
} else if (data.username) {
bingoUser.username = data.username;
bingoSessions[gameId].addUser(bingoUser);
res.send(bingoUser);
} else if (data.game) {
res.send(bingoSessions[data.game]);
} else if (data.bingoWord) {
if (!bingoUser.submittedWords[gameId])
bingoUser.submittedWords[gameId] = [];
bingoUser.submittedWords[gameId].push(data.bingoWord);
console.log(typeof bingoUser.grids[gameId]);
if (bingoUser.grids[gameId])
submitWord(data.bingoWord, bingoUser.grids[gameId]);
res.send(bingoUser.grids[gameId]);
} else if (data.bingo) {
if (checkBingo(bingoUser.grids[gameId])) {
if (!bingoSession.bingos.includes(bingoUser.id))
bingoSession.bingos.push(bingoUser.id);
bingoSession.finished = true;
setTimeout(() => { // delete the finished game after five minutes
delete bingoSessions[game.id];
}, 360000);
res.send(bingoSession);
} else {
res.status(400);
res.send({'error': "this is not a bingo!"})
}
} else if (bingoSession) {
res.send(bingoSession);
} else {
res.status(400);
res.send({
error: 'invalid request data'
})
}
});
module.exports = router;

@ -37,7 +37,7 @@ function startDownload(subreddit) {
let dlWebPath = `/static/${downloadId}.zip`;
let dl = new RedditDownload(dlWebPath);
dl.process = cproc.exec(`python -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`,
dl.process = cproc.exec(`python3 -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`,
{cwd: './scripts/reddit-riddle', env: {PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: true}},
(err, stdout) => {
if (err) {

@ -0,0 +1,13 @@
include bingo-layout
block content
div(id='username-form')
input(type='text', id='username-input', placeholder='username', value=username)
button(onclick='submitUsername()') Set Username
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo!
div(id='words-container')
each val in grid
div(class='bingo-word-row')
each word in val
div(class='bingo-word-panel', onclick=`submitWord('${word}')`, b-word=word, b-sub='false')
span= word

@ -0,0 +1,8 @@
html
head
include ../includes/head
script(type='text/javascript', src='/javascripts/bingo-web.js')
link(rel='stylesheet', href='/sass/bingo/style.sass')
body
block content

@ -0,0 +1,9 @@
extends bingo-layout
block content
div(id='bingoform')
input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8)
span x
span(id='bingo-grid-y', class='number-input') 3
button(onclick='submitBingoWords()') Submit
textarea(id='bingo-textarea', placeholder='Bingo Words')

@ -0,0 +1,2 @@
link(rel='stylesheet', href='/sass/style.sass')
script(type='text/javascript', src='/javascripts/common.js')

@ -1,8 +1,9 @@
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
include includes/head.pug
script(type='text/javascript', src='/javascripts/riddle-web.js')
link(rel='stylesheet', href='/sass/riddle/style.sass')
body
h1 Riddle Reddit downloader
input(type='text' placeholder='subreddit' id='subreddit-input')

Loading…
Cancel
Save