"use strict"; const express = require("express"), config = require("config"), moment = require("moment"), crypto = require("crypto"), util = require("util"), Promise = require("bluebird"), { readFile, writeFile } = require("fs").promises; let gameDB; require("../db/games").then(function(db) { gameDB = db; }); const router = express.Router(); function shuffle(array) { var currentIndex = array.length, temporaryValue, randomIndex; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } const assetData = { tiles: [ { type: "wood", y: 0. / 4. }, { type: "wood", y: 1. / 4. }, { type: "wood", y: 2. / 4. }, { type: "wood", y: 3. / 4. }, { type: "wheat", y: 0. / 4. }, { type: "wheat", y: 1. / 4. }, { type: "wheat", y: 2. / 4. }, { type: "wheat", y: 3. / 4. }, { type: "stone", y: 0. / 4. }, { type: "stone", y: 1. / 4. }, { type: "stone", y: 2. / 4. }, { type: "sheep", y: 0. / 4. }, { type: "sheep", y: 1. / 4. }, { type: "sheep", y: 2. / 4. }, { type: "sheep", y: 3. / 4. }, { type: "brick", y: 0. / 4. }, { type: "brick", y: 1. / 4. }, { type: "brick", y: 2. / 4. }, { type: "robber", y: 0 } ], pips: [ { roll: 7, pips: 0, y: 3. / 6., x: 0. / 6. }, { roll: 5, pips: 4, y: 0. / 6., x: 0. / 6. }, { roll: 2, pips: 1, y: 0. / 6., x: 1. / 6. }, { roll: 6, pips: 5, y: 0. / 6., x: 2. / 6. }, { roll: 3, pips: 2, y: 0. / 6., x: 3. / 6. }, { roll: 8, pips: 5, y: 0. / 6., x: 4. / 6. }, { roll: 10, pips: 3, y: 0. / 6., x: 5. / 6. }, { roll: 9, pips: 4, y: 1. / 6., x: 0. / 6. }, { roll: 12, pips: 1, y: 1. / 6., x: 1. / 6. }, { roll: 11, pips: 2, y: 1. / 6., x: 2. / 6. }, { roll: 4, pips: 3, y: 1. / 6., x: 3. / 6. }, { roll: 8, pips: 5, y: 1. / 6., x: 4. / 6. }, { roll: 10, pips: 3, y: 1. / 6., x: 5. / 6. }, { roll: 9, pips: 4, y: 2. / 6., x: 0. / 6. }, { roll: 4, pips: 3, y: 2. / 6., x: 1. / 6. }, { roll: 5, pips: 4, y: 2. / 6., x: 2. / 6. }, { roll: 6, pips: 6, y: 2. / 6., x: 3. / 6. }, { roll: 3, pips: 2, y: 2. / 6., x: 4. / 6. }, { roll: 11, pips: 2, y: 2. / 6., x: 5. / 6. } ], borders: [ { file: 'borders-1.6.png', left: "sheep", right: "bank" }, { file: 'borders-2.1.png', center: "sheep" }, { file: 'borders-3.2.png', left: "wheat", right: "bank" }, { file: 'borders-4.3.png', center: "wood" }, { file: 'borders-5.4.png', left: "sheep", right: "bank" }, { file: 'borders-6.5.png', center: "bank" } ], developmentCards: [] }; for (let i = 0; i < 14; i++) { assetData.developmentCards.push("knight"); } for (let i = 0; i < 6; i++) { assetData.developmentCards.push("progress"); } for (let i = 0; i < 5; i++) { assetData.developmentCards.push("victoryPoint"); } const games = {}; const roll = (game, player) => { let error; if (!player) { error = "No player active; roll has no action"; console.log(error); return error; } const name = player.name; switch (game.state) { case "lobby": if (player.order) { error = `Player ${name} already rolled for order.`; console.log(error); return error; } game.dice = [ Math.ceil(Math.random() * 6) ]; player.order = game.dice[0]; const message = `${name} rolled ${game.dice[0]} for play order.`; game.chat.push({ date: Date.now(), message: message }); console.log(message); return; } error = `Invalid game state (${game.state}) in roll.`; return error; } const loadGame = async (id) => { if (/^\.|\//.exec(id)) { return undefined; } if (id in games) { return games[id]; } const game = await readFile(`games/${id}`) .catch(() => { return; }); if (!game) { return undefined; } try { games[id] = JSON.parse(game); } catch (error) { console.error(error, game); return null; } return games[id]; }; router.put("/:id/:action/:value?", async (req, res) => { const { action, id, value } = req.params; console.log(`PUT games/${id}/${action}/${value}`); const game = await loadGame(id); if (!game) { const error = `Game not found and cannot be created: ${id}`; return res.status(404).send(error); } const color = req.session && req.session.playerColor in game.players ? req.session.playerColor : undefined; let player; let error; if ('private-token' in req.headers) { if (req.headers['private-token'] !== req.app.get('admin')) { error = `Invalid admin credentials.`; } switch (action) { case "kick": error = `Unable to find player ${value}` for (let color in game.players) { player = game.players[color]; if (player.name.toLowerCase() === value.toLowerCase()) { console.log(`Kicking ${value} from ${id}.`); game.chat.push({ date: Date.now(), message: `${player.name} has been kicked from game.` }); player.session = ''; player.name = ''; player.status = 'Not active'; player.lastActive = 0; error = undefined; break; } } break; } return sendGame(res, req, game, error); } if (action == "player-name") { const name = value ? value : ""; if (color) { error = `You cannot change your name while you are in game.`; } else { if (game) for (let key in game.players) { player = game.players[key]; if (player.name && player.name.toLowerCase() === name.toLowerCase()) { error = `${name} is already taken by ${key}`; } } } if (!error) { const old = req.session.playerName ? req.session.PlayerName : ""; let message; req.session.playerName = name; if (name) { message = `${old} is no known as ${name}.`; } else { message = `${old} no longer has a name.`; } game.chat.push({ date: Date.now(), message: message }); } return sendGame(res, req, game, error); } if (action == "player-selected") { if (!game) { error = `No game found`; return sendGame(res, req, game, error); } const selected = req.params.value, name = req.session.playerName; console.log(`player-selected requested for ${selected} by ${name}`); /* Deselect currently active player for this session */ for (let key in game.players) { if (key === color && selected !== key) { player = game.players[key]; if (player.session === req.session.id) { player.session = ''; player.name = ''; player.status = 'Not active'; req.session.playerColor = ''; game.chat.push({ date: Date.now(), message: `${key} is no longer claimed.` }); break; } } } /* Verify the player has a name set */ if (!name) { error = `You may only select a player when you have set your name.`; } /* Verify this player's name is not already active in the game */ if (!error) { for (let key in game.players) { player = game.players[key]; if (key === color && selected !== key && player.name == name) { error = `This name is already taken by ${key}`; break; } } } /* Verify selection is valid */ if (!error && !(selected in game.players)) { error = `An invalid player selection was attempted.`; } /* Verify selection is not already taken */ if (!error) { player = game.players[selected]; if (player.session) { error = `${player.name} already has ${selected}`; } } /* All good -- set this player to requested selection */ if (!error) { player.session = req.session.id; player.name = req.session.playerName; player.status = `Active`; player.lastActive = Date.now(); req.session.playerColor = selected; game.chat.push({ date: Date.now(), message: `${selected} is now '${player.name}'.` }); } return sendGame(res, req, game, error); } if (!req.session.playerColor || !(req.session.playerColor in game.players)) { error = `Invalid player: ${req.session.playerColor}`; return sendGame(res, req, game, error); } player = game.players[req.session.playerColor]; const name = player.name; switch (action) { case "roll": error = roll(game, player); break; case "shuffle": if (game.state !== "lobby") { error = `Game no longer in lobby (${game.state}). Can not shuffle board.`; } if (!error && game.turns > 0) { error = `Game already in progress (${game.turns} so far!) and cannot be shuffled.`; } if (!error) { shuffleBoard(game); const message = `${name} requested a new board.`; game.chat.push({ date: Date.now(), message: message }); console.log(message); } break case "state": const state = req.params.value ? req.params.value : "active"; if (state != game.state) { game.state = state; const message = `${name} set game state to ${state}.`; game.chat.push({ date: Date.now(), message: message }); } break; } return sendGame(res, req, game, error); }) router.get("/:id", async (req, res/*, next*/) => { const { id } = req.params; console.log("GET games/" + id); let error; const game = await loadGame(id); if (game) { return sendGame(res, req, game) } error = `Game ${id} not found -- returning invalid game state.`; const invalid = { id: id, players: {}, state: 'invalid' }; return sendGame(res, req, invalid, error); }); router.put("/:id", (req, res/*, next*/) => { console.log("PUT games/" + req.params.id); if (req.params.id in games) { const game = games[req.params.id], changes = req.body; console.log(req.session.id, req.session.playerColor); console.log(JSON.stringify(changes, null, 2)); for (let change in changes) { switch (change) { case "players": break; case "chat": console.log("Chat change."); game.chat.push({ from: changes.chat.player, date: Date.now(), message: changes.chat.message }); if (game.chat.length > 10) { game.chat.splice(0, game.chat.length - 10); } break; } } return sendGame(res, req, game); } else { const error = `Game not found: ${req.params.id}`; return res.status(404).send(error); } }); const sendGame = async (res, req, game, error) => { let active = 0; for (let player in game.players) { player = game.players[player]; active += ((player.status && player.status != 'Not active') ? 1 : 0); } if (active < 2 && game.state != 'lobby' && game.state != 'invalid') { let message = "Insufficient players in game. Setting back to lobby." game.chat.push({ date: Date.now(), message: message }); console.log(message); game.state = 'lobby'; } const playerColor = (req.session && req.session.playerColor) ? req.session.playerColor : "", playerName = (req.session && req.session.playerName) ? req.session.playerName : ""; if (playerColor in game.players) { game.players[playerColor].lastActive = Date.now(); } await writeFile(`games/${game.id}`, JSON.stringify(game, null, 2)) .catch((error) => { console.error(`Unable to write to games/${game.id}`); console.error(error); }); const playerGame = Object.assign({}, game, { timestamp: Date.now(), status: error ? error : "success", activePlayerName: playerName, activePlayer: playerColor }); if (game.id == 'b3c4bd15efe212a2') { // console.log(req.session); } return res.status(200).send(playerGame); } router.post("/:id?", (req, res/*, next*/) => { console.log("POST games/"); const { id } = req.params; if (id && id in games) { const error = `Can not create new game for ${id} -- it already exists.` console.error(error); return res.status(400).send(error); } const game = { startTime: Date.now(), turns: 0, state: "lobby", /* lobby, in-game, finished */ tiles: [], pips: [], borders: [], tokens: [], players: { R: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, O: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, B: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, W: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" } }, developmentCards: assetData.developmentCards.slice(), dice: [ 0, 0 ], sheep: 19, ore: 19, wool: 19, brick: 19, wheat: 19, longestRoad: null, largestArmy: null, chat: [ { from: "R", date: Date.now(), message: "Server initialized!" } ], id: id ? id : crypto.randomBytes(8).toString('hex') }; games[game.id] = game; req.session.playerColor = null; shuffleBoard(game); console.log(`New game created: ${game.id}`); return sendGame(res, req, game); }); const shuffleBoard = (game) => { [ "tiles", "pips", "borders" ].forEach((field) => { game[field] = [] for (let i = 0; i < assetData[field].length; i++) { game[field].push(i); } /* Shuffle an array of indexes */ shuffle(game[field]); /* Convert from an index array to a full array */ for (let i = 0; i < assetData[field].length; i++) { game[field][i] = assetData[field][game[field][i]]; } }); shuffle(game.developmentCards) } /* return gameDB.sequelize.query("SELECT " + "photos.*,albums.path AS path,photohashes.hash,modified,(albums.path || photos.filename) AS filepath FROM photos " + "LEFT JOIN albums ON albums.id=photos.albumId " + "LEFT JOIN photohashes ON photohashes.photoId=photos.id " + "WHERE photos.id=:id", { replacements: { id: id }, type: gameDB.Sequelize.QueryTypes.SELECT, raw: true }).then(function(photos) { if (photos.length == 0) { return null; } */ if (0) { router.get("/*", (req, res/*, next*/) => { return gameDB.sequelize.query(query, { replacements: replacements, type: gameDB.Sequelize.QueryTypes.SELECT }).then((photos) => { }); }); } module.exports = router;