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 * @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; } /** * Graphql endpoint * @param args {Object} - the arguments passed on the graphql interface * @returns {any[]|*} */ players(args) { if (args.id) return [this.users[args.id]]; else return Object.values(this.users); } } class BingoUser { /** * Bingo User class to store user information */ constructor() { this.id = generateBingoId(); this.game = null; this.username = 'anonymous'; this.grids = {}; } } class BingoWordField { /** * Represents a single bingo field with the word an the status. * It also holds the base64-encoded word. * @param word */ constructor(word) { this.word = word; this.base64Word = Buffer.from(word).toString('base64'); this.submitted = false; } } class BingoGrid { /** * Represents the bingo grid containing all the words. * @param wordGrid * @returns {BingoGrid} */ constructor(wordGrid) { this.wordGrid = wordGrid; this.fieldGrid = wordGrid.map(x => x.map(y => new BingoWordField(y))); this.bingo = false; 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; } /** * Inflates an array to a minimum Size * @param array {Array} - the array to inflate * @param minSize {Number} - the minimum size that the array needs to have * @returns {Array} */ function inflateArray(array, minSize) { let resultArray = array; let iterations = Math.ceil(minSize/array.length); for (let i = 0; i < iterations; i++) resultArray = [...resultArray, ...resultArray]; return resultArray } /** * 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} - the dimensions of the grid * @param words {Array} - the words included in the grid * @returns {BingoGrid} */ function generateWordGrid(dimensions, words) { let shuffledWords = shuffleArray(inflateArray(words, dimensions[0]*dimensions[1])); 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 base64Word {String} - base64 encoded bingo word * @param bingoGrid {BingoGrid} - the grid where the words are stored * @returns {boolean} */ function toggleHeared(base64Word, bingoGrid) { for (let row of bingoGrid.fieldGrid) for (let field of row) if (base64Word === field.base64Word) field.submitted = !field.submitted; checkBingo(bingoGrid); return true; } /** * 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) { bingoGrid.bingo = true; return true; } } 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) { bingoGrid.bingo = true; return true; } } if (bingoCheck) { bingoGrid.bingo = true; return true; } bingoGrid.bingo = false; 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].fieldGrid, 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) { console.log(typeof bingoUser.grids[gameId]); if (bingoUser.grids[gameId]) toggleHeared(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[gameId]; }, 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' }) } }); router.graphqlResolver = (req) => { let bingoUser = req.session.bingoUser || new BingoUser(); let gameId = req.query.game || bingoUser.game || null; let bingoSession = bingoSessions[gameId]; return { // queries gameInfo: (args) => { if (args.id) return bingoSessions[args.id]; else return bingoSession; }, checkBingo: (args) => { return checkBingo(bingoUser.grids[gameId]) }, activeGrid: (args) => { return bingoUser.grids[gameId]; }, // mutation createGame: (args) => { let words = args.words; let size = args.size; let game = new BingoSession(words, size); bingoSessions[game.id] = game; setTimeout(() => { // delete the game after one day delete bingoSessions[game.id]; }, 86400000); return game; }, submitBingo: (args) => { 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[gameId]; }, 360000); return true; } else { return false; } }, toggleWord: (args) => { if (args.word || args.base64Word) { args.base64Word = args.base64Word || Buffer.from(args.word).toString('base-64'); if (bingoUser.grids[gameId]) toggleHeared(args.base64Word, bingoUser.grids[gameId]); return bingoUser.grids[gameId]; } }, setUsername: (args) => { if (args.username) { bingoUser.username = args.username; bingoSession.addUser(bingoUser); return bingoUser; } } }; }; module.exports = router;