"use strict"; const express = require("express"), crypto = require("crypto"), { readFile, writeFile } = require("fs").promises, fs = require("fs"), accessSync = fs.accessSync, randomWords = require("random-words"); const { corners } = require("./layout.js"); const layout = require('./layout.js'); 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: "desert", card: 0 }, { type: "wood", card: 0 }, { type: "wood", card: 1 }, { type: "wood", card: 2 }, { type: "wood", card: 3 }, { type: "wheat", card: 0 }, { type: "wheat", card: 1 }, { type: "wheat", card: 2 }, { type: "wheat", card: 3 }, { type: "stone", card: 0 }, { type: "stone", card: 1 }, { type: "stone", card: 2 }, { type: "sheep", card: 0 }, { type: "sheep", card: 1 }, { type: "sheep", card: 2 }, { type: "sheep", card: 3 }, { type: "brick", card: 0 }, { type: "brick", card: 1 }, { type: "brick", card: 2 } ], pips: [ { roll: 5, pips: 4 }, { roll: 2, pips: 1 }, { roll: 6, pips: 5 }, { roll: 3, pips: 2 }, { roll: 8, pips: 5 }, { roll: 10, pips: 3 }, { roll: 9, pips: 4 }, { roll: 12, pips: 1 }, { roll: 11, pips: 2 }, { roll: 4, pips: 3 }, { roll: 8, pips: 5 }, { roll: 10, pips: 3 }, { roll: 9, pips: 4 }, { roll: 4, pips: 3 }, { roll: 5, pips: 4 }, { roll: 6, pips: 6 }, { roll: 3, pips: 2 }, { roll: 11, pips: 2 }, { roll: 7, pips: 0 }, /* Robber is at the end or indexing gets off */ ], borders: [ [ "bank", undefined, "sheep" ], [ undefined, "bank", undefined ], [ "bank", undefined, "brick" ], [ undefined, "wood", undefined ], [ "bank", undefined, "wheat" ], [ undefined, "stone", undefined ] ], developmentCards: [ ] }; for (let i = 1; i <= 14; i++) { assetData.developmentCards.push({ type: 'army', card: i }); } [ 'monopoly', 'road-1', 'road-2', 'yeard-of-plenty'].forEach(card => assetData.developmentCards.push({ type: 'progress', card: card })); [ 'market', 'library', 'palace', 'university'].forEach(card => assetData.developmentCards.push({ type: 'vp', card: card })); const games = {}; const processTies = (players) => { players.sort((A, B) => { if (A.order === B.order) { return B.orderRoll - A.orderRoll; } return A.order - B.order; }); /* Sort the players into buckets based on their * order, and their current roll. If a resulting * roll array has more than one element, then there * is a tie that must be resolved */ let slots = []; players.forEach(player => { if (!slots[player.order]) { slots[player.order] = []; } if (!(player.orderRoll in slots[player.order])) { slots[player.order][player.orderRoll] = []; } slots[player.order][player.orderRoll].push(player); }); let ties = false, order = 0; slots.forEach((slot) => { slot.forEach(pips => { if (pips.length !== 1) { ties = true; pips.forEach(player => { player.orderRoll = 0; player.order = order; player.orderStatus = `Tied for ${order+1}.`; }); } else { pips[0].order = order; pips[0].orderStatus = `Placed in ${order+1}.`; } order += pips.length }) }); return !ties; } const getPlayerName = (game, player) => { for (let id in game.sessions) { if (game.sessions[id].player === player) { return game.sessions[id].name; } } return ''; }; const getPlayerColor = (game, player) => { for (let color in game.players) { if (game.players[color] === player) { return color; } } return ''; } const playerNameFromColor = (game, color) => { for (let id in game.sessions) { if (game.sessions[id].color === color) { return game.sessions[id].name; } } return ''; }; const playerFromColor = (game, color) => { for (let id in game.sessions) { if (game.sessions[id].color === color) { return game.sessions[id].player; } } return undefined; }; const processGameOrder = (game, player, dice) => { let message; player.orderRoll = dice; let players = []; let doneRolling = true; for (let key in game.players) { const tmp = game.players[key]; if (tmp.status === 'Not active') { continue; } if (!tmp.orderRoll) { doneRolling = false; } players.push(tmp); } /* If 'doneRolling' is TRUE then everyone has rolled */ if (doneRolling) { if (processTies(players)) { message = `Player order set to ${players.map((player, index) => { return `${index+1}. ${getPlayerName(game, player)}`; }).join(', ')}.`; addChatMessage(game, null, message); game.playerOrder = players.map(player => getPlayerColor(game, player)); game.state = 'initial-placement'; message = `Initial settlement placement has started!`; game.direction = 'forward'; game.turn = { actions: [ 'place-settlement' ], limits: { corners: getValidCorners(game) }, name: getPlayerName(game, players[0]), color: getPlayerColor(game, players[0]) }; addChatMessage(game, null, message); message = `It is ${game.turn.name}'s turn to place a settlement.`; } else { message = `There are still ties for player order!`; } } if (message) { addChatMessage(game, null, message); } } const roll = (game, session) => { let message, error; const player = session.player, name = session.name ? session.name : "Unnamed"; switch (game.state) { case "lobby": error = `Rolling dice in the lobby is not allowed!`; case "game-order": if (!player) { error = `This player is not active!`; break; } if (player.order || player.orderRoll) { error = `Player ${name} has already rolled for player order.`; break; } game.dice = [ Math.ceil(Math.random() * 6) ]; message = `${name} rolled ${game.dice[0]}.`; addChatMessage(game, session, message); message = undefined; processGameOrder(game, player, game.dice[0]); break; case "normal": if (game.turn.color !== session.color) { error = `It is not your turn.`; break; } if (game.turn.roll) { error = `You already rolled this turn.`; break; } processRoll(game, [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ]); break; default: error = `Invalid game state (${game.state}) in roll.`; break; } if (!error && message) { addChatMessage(game, session, message); } return error; }; const distributeResources = (game, roll) => { console.log(`Roll: ${roll}`); /* Find which tiles have this roll */ let tiles = []; for (let i = 0; i < game.pipOrder.length; i++) { let index = game.pipOrder[i]; if (assetData.pips[index].roll === roll) { if (game.robber === i) { addChatMessage(game, null, `That pesky Robber stole resources!`); } else { tiles.push(i); } } } console.log(`Matched tiles: ${tiles.join(',')}.`); const receives = { "O": { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, "R": { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, "W": { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, "B": { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, }; /* Find which corners are on each tile */ tiles.forEach(index => { let shuffle = game.tileOrder[index]; console.log(index, game.tiles[shuffle]); const resource = game.tiles[shuffle]; layout.tiles[index].corners.forEach(cornerIndex => { const active = game.placements.corners[cornerIndex]; if (active && active.color) { receives[active.color][resource.type] += active.type === 'settlement' ? 1 : 2; } }) }); for (let color in receives) { const entry = receives[color]; if (!entry.wood && !entry.brick && !entry.sheep && !entry.wheat && !entry.stone) { continue; } let message = []; for (let type in receives[color]) { const player = playerFromColor(game, color); player[type] += receives[color][type]; if (receives[color][type]) { message.push(`${receives[color][type]} ${type}`); } } addChatMessage(game, null, `${playerNameFromColor(game, color)} receives ${message.join(', ')}.`); } } const processRoll = (game, dice) => { let session; for (let id in game.sessions) { if (game.sessions[id].name === game.turn.name) { session = game.sessions[id]; } } if (!session) { console.error(`Cannot process roll without an active player session`); return; } game.dice = dice; addChatMessage(game, session, `${session.name} rolled ${game.dice[0]}, ${game.dice[1]}.`); game.turn.roll = game.dice[0] + game.dice[1]; if (game.turn.roll === 7) { addChatMessage(game, null, `ROBBER! Robber Roberson!`); game.turn.robberDone = false; delete game.turn.placedRobber; for (let id in game.sessions) { const player = game.sessions[id].player; if (player) { let discard = player.stone + player.wheat + player.brick + player.wood + player.sheep; if (discard > 7) { discard = Math.floor(discard / 2); player.mustDiscard = discard; addChatMessage(game, null, `${game.sessions[id].name} must discard ${discard} resource cards.`); } else { delete player.mustDiscard; } } } /* if you roll a 7, no one receives any resource cards. instead, every player who has more than 7 resource cards must select half (rounded down) of their resource cards and return them to the bank. then you muyst move the robber: 1. you must move the robber immediately to the number token of any other terrain ohex or to the desert hex, 2. you then steal 1 (random) resourcde card from an opponent who has a settlement or city adjacent to the target terrain hex. the player who is robbed holds their resource cards face down. you then take 1 card at random. if the target hex is adjacent to 2 or more player's settlements or cities, you choose which one you want to steal from. If the production number for the hex containing the robber is rolled, the owners of adjacent settlements and citieis do not receive resourcres. The robber prevents it. */ } else { distributeResources(game, game.turn.roll); } } const getPlayer = (game, color) => { if (!game) { return { roads: 15, cities: 4, settlements: 5, points: 0, status: "Not active", lastActive: 0, order: 0, stone: 0, wheat: 0, sheep: 0, wood: 0, brick: 0, development: [] }; } return game.players[color]; }; const getSession = (game, session) => { if (!game.sessions) { game.sessions = {}; } if (!session.player_id) { session.player_id = crypto.randomBytes(32).toString('hex'); } const id = session.player_id; /* If this session is not yet in the game, * add it and set the player's name */ if (!(id in game.sessions)) { game.sessions[id] = { name: undefined, color: undefined, player: undefined }; } return game.sessions[id]; }; const loadGame = async (id) => { if (/^\.|\//.exec(id)) { return undefined; } if (id in games) { return games[id]; } let game = await readFile(`games/${id}`) .catch(() => { return; }); if (!game) { game = createGame(id); } else { try { game = JSON.parse(game); } catch (error) { console.error(error, game); return null; } } if (!game.pipOrder || !game.borderOrder || !game.tileOrder) { console.log("Shuffling old save file"); shuffleBoard(game); } if (!game.pips || !game.borders || !game.tiles) { [ "pips", "borders", "tiles" ].forEach((field) => { game[field] = assetData[field] }); } if (game.state === 'active') { game.state = 'initial-placement'; } if (typeof game.turn !== 'object') { delete game.turn; } if (!game.placements) { resetGame(game); } /* Reconnect session player colors to the player objects */ for (let id in game.sessions) { const session = game.sessions[id]; if (session.color && session.color in game.players) { session.player = game.players[session.color]; } else { session.color = undefined; session.player = undefined; } } for (let color in game.players) { if (!game.players[color].development) { game.players[color].development = []; } } games[id] = game; return game; }; const clearPlayer = (player) => { player.status = 'Not active'; player.lastActive = 0; player.order = 0; delete player.orderRoll; delete player.orderStatus; } const adminActions = (game, action, value) => { let color, player, parts, session; switch (action) { case "state": switch (value) { case 'game-order': resetGame(game); game.state = 'game-order'; break; } break; case "give": parts = value.match(/^([^-]+)-([0-9]+)$/); if (!parts) { return `Unable to parse give request.`; } for (let id in game.sessions) { if (game.sessions[id].name === game.turn.name) { session = game.sessions[id]; } } if (!session) { return `Unable to determine current player turn to give resources.`; } if (!(parts[1] in session.player)) { return `Invalid resource request.`; } session.player[parts[1]] += parseInt(parts[2]); addChatMessage(game, null, `Admin gave ${parseInt(parts[2])} ${parts[1]} to ${game.turn.name}.`); break; case "roll": parts = value.match(/^([1-6])(-([1-6]))?$/); if (!parts) { return `Unable to parse roll request.`; } let dice = [ parseInt(parts[1]) ]; if (parts[3]) { dice.push(parseInt(parts[3])); } for (let id in game.sessions) { if (game.sessions[id].name === game.turn.name) { session = game.sessions[id]; } } if (!session) { return `Unable to determine current player turn for admin roll.`; } console.log(dice, parts); addChatMessage(game, null, `Admin rolling ${dice.join(', ')} for ${game.turn.name}.`); switch (game.state) { case 'game-order': game.dice = dice; message = `${game.turn.name} rolled ${game.dice[0]}.`; addChatMessage(game, session, message); message = undefined; processGameOrder(game, session.player, game.dice[0]); break; case 'normal': processRoll(game, dice); break; } break; case "kick": switch (value) { case 'orange': color = 'O'; break; case 'red': color = 'R'; break; case 'blue': color = 'B'; break; case 'white': color = 'W'; break; } if (!color) { return `Unable to find player ${value}` } player = game.players[color]; for (let id in game.sessions) { const session = game.sessions[id]; if (session.player !== player) { continue; } console.log(`Kicking ${value} from ${game.id}.`); const preamble = session.name ? `${session.name}, playing as ${color},` : color; addChatMessage(game, null, `${preamble} was kicked from game by the Admin.`); if (player) { session.player = undefined; clearPlayer(player); } session.color = undefined; return; } return `Unable to find active session for ${color} (${value})`; default: return `Invalid admin action ${action}.`; } }; const setPlayerName = (game, session, name) => { if (session.color) { return `You cannot change your name while you are in game.`; } /* Check to ensure name is not already in use */ if (game && name) for (let key in game.sessions) { const tmp = game.sessions[key]; if (tmp.name && tmp.name.toLowerCase() === name.toLowerCase()) { return `${name} is already taken.`; } } const old = session.name; let message; session.name = name; if (name) { if (!old) { message = `A new player has entered the lobby as ${name}.`; } else { message = `${old} has changed their name to ${name}.`; } } else { return `You can not set your name to nothing!`; } addChatMessage(game, null, message); } const setPlayerColor = (game, session, color) => { if (!game) { return `No game found`; } const name = session.name, player = session.player; /* Selecting the same color is a NO-OP */ if (session.color === color) { return; } const priorActive = getActiveCount(game); let message; if (player) { /* Deselect currently active player for this session */ clearPlayer(player); if (game.state !== 'lobby') { message = `${name} has exited to the lobby and is no longer playing as ${session.color}.` addChatMessage(game, null, message); } else { message = `${name} is no longer ${session.color}.`; } session.player = undefined; session.color = undefined; } /* Verify the player has a name set */ if (!name) { return `You may only select a player when you have set your name.`; } /* If the player is not selecting a color, then return */ if (!color) { if (message) { addChatMessage(game, null, message); } return; } /* Verify selection is valid */ if (!(color in game.players)) { return `An invalid player selection was attempted.`; } /* Verify selection is not already taken */ for (let key in game.sessions) { const tmp = game.sessions[key].player; if (tmp && tmp.color === color) { return `${game.sessions[key].name} already has ${color}`; } } /* All good -- set this player to requested selection */ session.player = getPlayer(game, color); session.player.status = `Active`; session.player.lastActive = Date.now(); session.color = color; addChatMessage(game, session, `${session.name} has chosen to play as ${color}.`); const afterActive = getActiveCount(game); if (afterActive !== priorActive) { if (priorActive < 2 && afterActive >= 2) { addChatMessage(game, null, `There are now enough players to start the game when you are ready.`); } } }; const addChatMessage = (game, session, message) => { game.chat.push({ from: session ? session.name : undefined, color: session ? session.color : undefined, date: Date.now(), message: message }); }; const getColorFromName = (game, name) => { for (let id in game.sessions) { if (game.sessions[id].name === name) { return game.sessions[id].color; } } return ''; }; const getLastPlayerName = (game) => { let index = game.playerOrder.length - 1; for (let id in game.sessions) { if (game.sessions[id].color === game.playerOrder[index]) { return game.sessions[id].name; } } return ''; } const getFirstPlayerName = (game) => { let index = 0; for (let id in game.sessions) { if (game.sessions[id].color === game.playerOrder[index]) { return game.sessions[id].name; } } return ''; } const getNextPlayer = (game, name) => { let color; for (let id in game.sessions) { if (game.sessions[id].name === name) { color = game.sessions[id].color; break; } } if (!color) { return name; } let index = game.playerOrder.indexOf(color); index = (index + 1) % game.playerOrder.length; for (let id in game.sessions) { if (game.sessions[id].color === game.playerOrder[index]) { return game.sessions[id].name; } } return name; } const getPrevPlayer = (game, name) => { let color; for (let id in game.sessions) { if (game.sessions[id].name === name) { color = game.sessions[id].color; break; } } if (!color) { return name; } let index = game.playerOrder.indexOf(color); index = (index - 1) % game.playerOrder.length; for (let id in game.sessions) { if (game.sessions[id].color === game.playerOrder[index]) { return game.sessions[id].name; } } return name; } const processCorner = (game, color, cornerIndex, placedCorner) => { /* If this corner is allocated and isn't assigned to the walking color, skip it */ if (placedCorner.color && placedCorner.color !== color) { return -1; } /* If this corner is already being walked, skip it */ if (placedCorner.walking) { return -1; } placedCorner.walking = true; /* Calculate the longest road branching from both corners */ let longest = 0; layout.corners[cornerIndex].roads.forEach(roadIndex => { const placedRoad = game.placements.roads[roadIndex]; longest = Math.max(processRoad(game, color, roadIndex, placedRoad), longest); }); return longest; }; const buildCornerGraph = (game, color, cornerIndex, placedCorner, set) => { /* If this corner is allocated and isn't assigned to the walking color, skip it */ if (placedCorner.color && placedCorner.color !== color) { return; } /* If this corner is already being walked, skip it */ if (placedCorner.walking) { return; } placedCorner.walking = true; /* Calculate the longest road branching from both corners */ layout.corners[cornerIndex].roads.forEach(roadIndex => { const placedRoad = game.placements.roads[roadIndex]; buildRoadGraph(game, color, roadIndex, placedRoad, set); }); }; const processRoad = (game, color, roadIndex, placedRoad) => { /* If this road isn't assigned to the walking color, skip it */ if (placedRoad.color !== color) { return -1; } /* If this road is already being walked, skip it */ if (placedRoad.walking) { return -1; } placedRoad.walking = true; /* Calculate the longest road branching from both corners */ let longest = 0; layout.roads[roadIndex].corners.forEach(cornerIndex => { const placedCorner = game.placements.corners[cornerIndex]; longest = Math.max(processCorner(game, color, cornerIndex, placedCorner), longest); }); placedRoad.longest = 1 + longest; return placedRoad.longest; }; const buildRoadGraph = (game, color, roadIndex, placedRoad, set) => { /* If this road isn't assigned to the walking color, skip it */ if (placedRoad.color !== color) { return; } /* If this road is already being walked, skip it */ if (placedRoad.walking) { return; } placedRoad.walking = true; set.push(roadIndex); /* Calculate the longest road branching from both corners */ layout.roads[roadIndex].corners.forEach(cornerIndex => { const placedCorner = game.placements.corners[cornerIndex]; buildCornerGraph(game, color, cornerIndex, placedCorner, set) }); }; const clearRoadMarkers = (game) => { /* Clear out walk markers on roads */ layout.roads.forEach((item, itemIndex) => { const placed = game.placements.roads[itemIndex]; placed.walking = false; placed.longest = 0; }); /* Clear out walk markers on corners */ layout.corners.forEach((item, itemIndex) => { const placed = game.placements.corners[itemIndex]; placed.walking = false; }); } const calculateRoadLengths = (game, session) => { clearRoadMarkers(game); /* Clear out player longest road counts */ for (let key in game.players) { game.players[key].roadLength = 0; } /* Build a set of connected road graphs. Once all graphs are * constructed, walk through each graph, starting from each * location in the graph. If the length ever equals the * number of items in the graph, short circuit--longest path. * Otherwise, check all paths from each segment. This is * needed to catch loops where starting from an outside end * point may result in not counting the length of the loop */ let graphs = []; layout.roads.forEach((road, roadIndex) => { const placedRoad = game.placements.roads[roadIndex]; if (placedRoad.color) { let set = []; buildRoadGraph(game, placedRoad.color, roadIndex, placedRoad, set); if (set.length) { graphs.push({ color: placedRoad.color, set }); } } }); console.log(graphs); let currentLength = game.longestRoad ? game.players[game.longestRoad].roadLength : -1, currentLongest = game.longestRoad; clearRoadMarkers(game); graphs.forEach(graph => { graph.set.forEach(roadIndex => { const placedRoad = game.placements.roads[roadIndex]; clearRoadMarkers(game); const length = processRoad(game, placedRoad.color, roadIndex, placedRoad); game.players[placedRoad.color].roadLength = Math.max(game.players[placedRoad.color].roadLength, length); }); }); const checkForTies = false; if (currentLongest && game.players[game.currentLongest].roadLength < currentLength) { addChatMessage(game, session, `${getPlayerNameFromColor(game, game.currentLongest)} had their longest road split!`); checkForTies = true; } let longest = game.longestRoad ? game.players[game.longestRoad].roadLength : 4, longestPlayers = []; for (let key in game.players) { if (game.players[key].status === 'Not active') { continue; } if (game.players[key].roadLength > longest) { longestPlayers = [ key ]; longest = game.players[key].roadLength; } else if (game.players[key].roadLength == longest && checkForTies) { longestPlayers.push(key); } } if (longestPlayers.length > 0) { if (longestPlayers.length === 1) { if (game.longestRoad !== longestPlayers[0]) { game.longestRoad = longestPlayers[0]; addChatMessage(game, session, `${playerNameFromColor(game, game.longestRoad)} now has the longest road (${longest})!`); } } else { if (checkForTies) { const names = longestPlayers.map(color => playerNameFromColor(color)); addChatMessage(game, session, `${names.join(', ')} are tied for longest road (${longest})!`); } game.longestRoad = null; } } else { game.longestRoad = null; } }; const getValidCorners = (game, color, type) => { const limits = []; /* For each corner, if the corner already has a color set, skip it if type * isn't set. If type is set, if it is a match, and the color is a match, * add it to the list. * * If we are limiting based on active player, a corner is only valid * if it connects to a road that is owned by that player. * If no color is set, walk each road that leaves that corner and * check to see if there is a settlement placed at the end of that road * If so, this location cannot have a settlement. */ layout.corners.forEach((corner, cornerIndex) => { const placement = game.placements.corners[cornerIndex]; if (type) { if (placement.color === color && placement.type === type) { limits.push(cornerIndex); } return; } if (placement.color) { return; } let valid; if (!color) { valid = true; /* Not filtering based on current player */ } else { valid = false; for (let r = 0; !valid && r < corner.roads.length; r++) { valid = game.placements.roads[corner.roads[r]].color === color; } } for (let r = 0; valid && r < corner.roads.length; r++) { const road = layout.roads[corner.roads[r]]; for (let c = 0; valid && c < road.corners.length; c++) { /* This side of the road is pointing to the corner being validated. Skip it. */ if (road.corners[c] === cornerIndex) { continue; } /* There is a settlement within one segment from this * corner, so it is invalid for settlement placement */ if (game.placements.corners[road.corners[c]].color) { valid = false; } } } if (valid) { limits.push(cornerIndex); } }); return limits; } const getValidRoads = (game, color) => { const limits = []; /* For each road, if the road is set, skip it. * If no color is set, check the two corners. If the corner * has a matching color, add this to the set. Otherwise skip. */ layout.roads.forEach((road, roadIndex) => { if (game.placements.roads[roadIndex].color) { return; } let valid = false; for (let c = 0; !valid && c < road.corners.length; c++) { const corner = layout.corners[road.corners[c]], cornerColor = game.placements.corners[road.corners[c]].color; /* Roads do not pass through other player's settlements */ if (cornerColor && cornerColor !== color) { continue; } for (let r = 0; !valid && r < corner.roads.length; r++) { /* This side of the corner is pointing to the road being validated. Skip it. */ if (corner.roads[r] === roadIndex) { continue; } if (game.placements.roads[corner.roads[r]].color === color) { valid = true; } } } if (valid) { limits.push(roadIndex); } }); return limits; } router.put("/:id/:action/:value?", async (req, res) => { const { action, id } = req.params, value = req.params.value ? req.params.value : ""; 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); } let error; if ('private-token' in req.headers) { if (req.headers['private-token'] !== req.app.get('admin')) { error = `Invalid admin credentials.`; } else { error = adminActions(game, action, value); } return sendGame(req, res, game, error); } const session = getSession(game, req.session), player = session.player; switch (action) { case 'player-name': error = setPlayerName(game, session, value); return sendGame(req, res, game, error); case 'player-selected': error = setPlayerColor(game, session, value); return sendGame(req, res, game, error); case 'chat': const chat = req.body; addChatMessage(game, session, chat.message); return sendGame(req, res, game); } if (!session.player) { error = `Player must have an active color.`; return sendGame(req, res, game, error); } const name = session.name; let message, index; let corners, corner; switch (action) { case "trade": if (game.state !== "normal") { error = `Game not in correct state to begin trading.`; break; } if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) { /* Only the active player can begin trading */ if (game.turn.name !== name) { error = `You cannot start trading negotiations when it is not your turn.` break; } game.turn.actions = [ 'trade' ]; game.turn.limits = {}; addChatMessage(game, session, `${name} has requested to begin trading negotiations.`); break; } /* Only the active player can cancel trading */ if (value === 'cancel') { /* TODO: Perhaps 'cancel' is how a player can remove an offer... */ if (game.turn.name !== name) { error = `Only the active player can cancel trading negotiations.`; break; } game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} has cancelled trading negotiations.`); break; } /* Any player can make an offer */ if (value === 'offer') { const offer = req.body; console.log('TODO: Verify player has sufficient resources.'); session.player.gives = offer.gives; session.player.gets = offer.gets; if (game.turn.name === name) { game.turn.offer = offer; } addChatMessage(game, session, `${session.name} has submitted a trade offer.`); break; } /* Only the active player can accept an offer */ if (value === 'accept') { if (game.turn.name !== name) { error = `Only the active player can accept a trade offer.`; break; } const offer = req.body; let target; /* Verify that the offer sent by the active player matches what * the latest offer was that was received by the requesting player */ if (!offer.name || offer.name !== 'The bank') { let mismatch = false; target = game.players[offer.color]; offer.gives.forEach(item => { const isOffered = target.gives.find( match => match.type === item.type && match.count === item.count); if (!isOffered) { mismatch = true; } }); offer.gets.forEach(item => { const isOffered = target.gets.find( match => match.type === item.type && match.count === item.count); if (!isOffered) { mismatch = true; } }); if (mismatch) { error = `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`; break; } } /* Verify the requesting offer wasn't jacked */ console.log('TODO: Verify the player trade matches the offer target'); /* Transfer goods */ player.gets.forEach(item => { if (target) { target[item.type] -= item.count; if (target[item.type] < 0) { console.log(`Cheating!!!`); target[item.type] = 0; } } player[item.type] += item.count; }); player.gives.forEach(item => { if (target) { target[item.type] += item.count; } player[item.type] -= item.count; if (player[item.type] < 0) { console.log(`Cheating!!!`); player[item.type] = 0; } }); delete game.turn.offer; if (target) { delete target.gives; delete target.gets; } delete session.player.gives; delete session.player.gets; game.turn.actions = []; addChatMessage(game, session, `${session.name} has accepted a trade ` + `offer from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`); break; } break; case "roll": error = roll(game, session); 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.`; addChatMessage(game, null, message); console.log(message); } break; case 'pass': if (game.turn.name !== name) { error = `You cannot pass when it isn't your turn.` break; } /* If the current turn is a robber placement, and everyone has * discarded, set the limits for where the robber can be placed */ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) { error = `Robber is in action. Turn can not stop until all Robber tasks are resolved.`; break; } const next = getNextPlayer(game, name); game.turn = { name: next, color: getColorFromName(game, next) }; addChatMessage(game, session, `${name} passed their turn.`); addChatMessage(game, null, `It is ${next}'s turn.`); break; case 'place-robber': if (game.state !== 'normal' && game.turn.roll !== 7) { error = `You cannot place robber unless 7 was rolled!`; break; } if (game.turn.name !== name) { error = `You cannot place the robber when it isn't your turn.`; break; } for (let color in game.players) { if (game.players[color].status === 'Not active') { continue; } if (game.players[color].mustDiscard > 0) { error = `You cannot place the robber until everyone has discarded!`; break; } } const robber = parseInt(value ? value : 0); if (game.robber === robber) { error = `You must move the robber to a new location!`; break; } game.robber = robber; game.turn.placedRobber = true; addChatMessage(game, session, `Robber has been moved!`); let colors = []; layout.tiles[robber].corners.forEach(cornerIndex => { const active = game.placements.corners[cornerIndex]; if (active && active.color && active.color !== game.turn.color && colors.indexOf(active.color) == -1) { colors.push(active.color); } }); if (colors.length) { game.turn.actions = [ 'steal-resource' ], game.turn.limits = { players: colors }; addChatMessage(game, session, `${session.name} must select player to steal resource from.`); } else { game.turn.actions = []; game.turn.robberDone = true; delete game.turn.limits; addChatMessage(game, session, `The Robber was moved to a terrain with no other players.`); } break; case 'steal-resource': if (game.turn.actions.indexOf('steal-resource') === -1) { error = `You can only steal a resource when it is valid to do so!`; break; } if (game.turn.limits.players.indexOf(value) === -1) { error = `You can only steal a resource from a player on this terrain!`; break; } let victim = game.players[value]; const cards = []; [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(field => { for (let i = 0; i < victim[field]; i++) { cards.push(field); } }); if (cards.length === 0) { addChatMessage(game, session, `Victim did not have any cards to steal.`); game.turn.actions = []; game.turn.limits = {}; } else { let index = Math.floor(Math.random() * cards.length), type = cards[index]; victim[type]--; session.player[type]++ game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${session.name} randomly stole ${type} from ${playerNameFromColor(game, value)}.`); } game.turn.robberDone = true; break; case 'buy-development': if (game.state !== 'normal') { error = `You cannot purchase a development card unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } if (!game.turn.roll) { error = `You cannot build until you have rolled.`; break; } if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) { error = `You have insufficient resources to purchase a development card.`; break; } if (game.developmentCards.length < 1) { error = `There are no more development cards!`; break; } if (game.turn.developmentPurchased) { error = `You have already purchased a development card this turn.`; } addChatMessage(game, session, `Purchased a development card.`); player.stone--; player.wheat--; player.sheep--; player.development.push(game.developmentCards.pop()); break; case 'buy-settlement': if (game.state !== 'normal') { error = `You cannot purchase a settlement unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } if (!game.turn.roll) { error = `You cannot build until you have rolled.`; break; } if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) { error = `You have insufficient resources to build a settlement.`; break; } if (player.settlements < 1) { error = `You have already built all of your settlements.`; break; } corners = getValidCorners(game, session.color); if (corners.length === 0) { error = `There are no valid locations for you to place a settlement.`; break; } game.turn.actions = [ 'place-settlement' ]; game.turn.limits = { corners }; addChatMessage(game, session, `${game.turn.name} is considering placing a settlement.`); break; case 'place-settlement': if (game.state !== 'initial-placement' && game.state !== 'normal') { error = `You cannot place an item unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } index = parseInt(value); if (game.placements.corners[index] === undefined) { error = `You have requested to place a settlement illegally!`; break; } /* If this is not a valid road in the turn limits, discard it */ if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) { error = `You tried to cheat! You should not try to break the rules.`; break; } corner = game.placements.corners[index]; if (corner.color) { error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`; break; } if (!player.banks) { player.banks = []; } if (game.state === 'normal') { if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) { error = `You have insufficient resources to build a settlement.`; break; } if (player.settlements < 1) { error = `You have already built all of your settlements.`; break; } player.settlements--; player.brick--; player.wood--; player.wheat--; player.sheep--; corner.color = session.color; corner.type = 'settlement'; if (layout.corners[index].banks.length) { layout.corners[index].banks.forEach(bank => { const border = game.borderOrder[Math.floor(bank / 3)], type = game.borders[border][bank % 3]; console.log(`Bank ${bank} = ${type}`); if (!type) { console.log(`Bank ${bank}`) return; } if (player.banks.indexOf(type) === -1) { player.banks.push(type); } }); } game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} placed a settlement.`); calculateRoadLengths(game, session); } else if (game.state === 'initial-placement') { if (game.direction && game.direction === 'backward') { session.initialSettlement = index; } corner.color = session.color; corner.type = 'settlement'; if (layout.corners[index].banks.length) { layout.corners[index].banks.forEach(bank => { console.log(game.borderOrder); console.log(game.borders); const border = game.borderOrder[Math.floor(bank / 3)], type = game.borders[border][bank % 3]; console.log(`Bank ${bank} = ${type}`); if (!type) { return; } if (player.banks.indexOf(type) === -1) { player.banks.push(type); } }); } player.maritime = player.banks.map(bank => game.borders[Math.floor(bank / 3) + bank % 3]); game.turn.actions = ['place-road']; game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */ addChatMessage(game, session, `Placed a settlement. Next, they need to place a road.`); } break; case 'buy-city': if (game.state !== 'normal') { error = `You cannot purchase a city unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } if (!game.turn.roll) { error = `You cannot build until you have rolled.`; break; } if (player.wheat < 2 || player.stone < 3) { error = `You have insufficient resources to build a city.`; break; } if (player.city < 1) { error = `You have already built all of your cities.`; break; } corners = getValidCorners(game, session.color, 'settlement'); if (corners.length === 0) { error = `There are no valid locations for you to place a city.`; break; } game.turn.actions = ['place-city']; game.turn.limits = { corners }; addChatMessage(game, session, `${game.turn.name} is considering upgrading a settlement to a city.`); break; case 'place-city': if (game.state !== 'normal') { error = `You cannot place an item unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } index = parseInt(value); if (game.placements.corners[index] === undefined) { error = `You have requested to place a city illegally!`; break; } /* If this is not a placement the turn limits, discard it */ if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) { error = `You tried to cheat! You should not try to break the rules.`; break; } corner = game.placements.corners[index]; if (corner.color !== session.color) { error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`; break; } if (corner.type !== 'settlement') { error = `This location already has a city!`; break; } if (player.wheat < 2 || player.stone < 3) { error = `You have insufficient resources to build a city.`; break; } if (player.city < 1) { error = `You have already built all of your cities.`; break; } corner.color = session.color; corner.type = 'city'; player.cities--; player.settlements++; player.wheat -= 2; player.stone -= 3; game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} upgraded a settlement to a city!`); break; case 'buy-road': if (game.state !== 'normal') { error = `You cannot purchase a road unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } if (!game.turn.roll) { error = `You cannot build until you have rolled.`; break; } if (player.brick < 1 || player.wood < 1) { error = `You have insufficient resources to build a road.`; break; } if (player.roads < 1) { error = `You have already built all of your roads.`; break; } let roads = getValidRoads(game, session.color); if (roads.length === 0) { error = `There are no valid locations for you to place a road.`; break; } game.turn.actions = ['place-road']; game.turn.limits = { roads }; addChatMessage(game, session, `${game.turn.name} is considering building a road.`); break; case 'place-road': if (game.state !== 'initial-placement' && game.state !== 'normal') { error = `You cannot place an item unless the game is active.`; break; } if (session.color !== game.turn.color) { error = `It is not your turn! It is ${game.turn.name}'s turn.`; break; } index = parseInt(value); if (game.placements.roads[index] === undefined) { error = `You have requested to place a road illegally!`; break; } /* If this is not a valid road in the turn limits, discard it */ if (game.turn && game.turn.limits && game.turn.limits.roads && game.turn.limits.roads.indexOf(index) === -1) { error = `You tried to cheat! You should not try to break the rules.`; break; } const road = game.placements.roads[index]; if (road.color) { error = `This location already has a road belonging to ${playerNameFromColor(game, road.color)}!`; break; } if (game.state === 'normal') { if (player.brick < 1 || player.wood < 1) { error = `You have insufficient resources to build a road.`; break; } if (player.roads < 1) { error = `You have already built all of your roads.`; break; } player.roads--; player.brick--; player.wood--; road.color = session.color; game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} placed a road.`); calculateRoadLengths(game, session); } else if (game.state === 'initial-placement') { road.color = session.color; addChatMessage(game, session, `${name} placed a road.`); calculateRoadLengths(game, session); let next; if (game.direction === 'forward' && getLastPlayerName(game) === name) { game.direction = 'backward'; next = name; } else if (game.direction === 'backward' && getFirstPlayerName(game) === name) { /* Done! */ delete game.direction; } else { if (game.direction === 'forward') { next = getNextPlayer(game, name); } else { next = getPrevPlayer(game, name); } } if (next) { game.turn = { actions: ['place-settlement'], limits: { corners: getValidCorners(game) }, name: next, color: getColorFromName(game, next) }; calculateRoadLengths(game, session); addChatMessage(game, null, `It is ${next}'s turn. Place a settlement.`); } else { game.turn = { actions: [], limits: { }, name: name, color: getColorFromName(game, name) }; addChatMessage(game, null, `Everyone has placed their two settlements!`); /* Figure out which players received which resources */ for (let id in game.sessions) { const session = game.sessions[id], player = session.player, receives = {}; if (!player) { continue; } if (session.initialSettlement) { layout.tiles.forEach((tile, index) => { if (tile.corners.indexOf(session.initialSettlement) !== -1) { const resource = assetData.tiles[game.tileOrder[index]].type; if (!(resource in receives)) { receives[resource] = 0; } receives[resource]++; } }); let message = []; for (let type in receives) { player[type] += receives[type]; message.push(`${receives[type]} ${type}`); } addChatMessage(game, null, `${session.name} receives ${message.join(', ')}.`); } } addChatMessage(game, null, `It is ${name}'s turn.`); game.state = 'normal'; } } break; case 'discard': if (game.turn.roll !== 7) { error = `You can only discard due to the Robber!`; break; } const discards = req.body; let sum = 0; for (let type in discards) { if (player[type] < parseInt(discards[type])) { error = `You have requested to discard more ${type} than you have.` break; } sum += parseInt(discards[type]); } if (sum > player.mustDiscard) { error = `You have requested to discard more cards than you are allowed!`; break; } for (let type in discards) { player[type] -= parseInt(discards[type]); player.mustDiscard -= parseInt(discards[type]) } addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`); if (player.mustDiscard) { addChatMessage(game, null, `${session.name} must discard ${player.mustDiscard} more cards.`); } break; case "state": const state = value; if (!state) { error = `Invalid state.`; break; } if (state === game.state) { break; } switch (state) { case "game-order": if (game.state !== 'lobby') { error = `You cannot start a game from other than the lobby.`; break; } resetGame(game); message = `${name} requested to start the game.`; addChatMessage(game, null, message); game.state = state; break; } break; } return sendGame(req, res, game, error); }) router.get("/:id", async (req, res/*, next*/) => { const { id } = req.params; // console.log("GET games/" + id); let game = await loadGame(id); if (game) { return sendGame(req, res, game) } game = createGame(id); return sendGame(req, res, game); }); const getActiveCount = (game) => { let active = 0; for (let color in game.players) { const player = game.players[color]; active += ((player.status && player.status != 'Not active') ? 1 : 0); } return active; } const sendGame = async (req, res, game, error) => { const active = getActiveCount(game); /* Enforce game limit of >= 2 players */ if (active < 2 && game.state != 'lobby' && game.state != 'invalid') { let message = "Insufficient players in game. Setting back to lobby." console.log(game); addChatMessage(game, null, message); console.log(message); /* It is no one's turn in the lobby */ delete game.turn; game.state = 'lobby'; } game.active = active; /* If the current turn is a robber placement, and everyone has * discarded, set the limits for where the robber can be placed */ if (game.turn && game.turn.roll === 7) { let move = true; for (let color in game.players) { const discard = game.players[color].mustDiscard; if (discard) { move = false; } } if (move && !game.turn.placedRobber) { game.turn.actions = [ 'place-robber' ]; game.turn.limits = { pips: [] }; for (let i = 0; i < 19; i++) { if (i === game.robber) { continue; } game.turn.limits.pips.push(i); } } else { /* game.turn.limits = {}; game.turn.actions = []; */ } } /* Update the session lastActive clock */ let session; if (req.session) { session = getSession(game, req.session); session.lastActive = Date.now(); if (session.player) { session.player.lastActive = session.lastActive; } } else { session = { name: "command line" }; } /* Ensure chat messages have a unique date: stamp as it is used as the index key */ let lastTime = 0; if (game.chat) game.chat.forEach((message) => { if (message.date <= lastTime) { message.date = lastTime + 1; } lastTime = message.date; }); /* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then * delete the player field from them */ const reducedGame = Object.assign({}, game, { sessions: {} }), reducedSessions = []; for (let id in game.sessions) { const reduced = Object.assign({}, game.sessions[id]); if (reduced.player) { delete reduced.player; } reducedGame.sessions[id] = reduced; /* Do not send session-id as those are secrets */ reducedSessions.push(reduced); } await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2)) .catch((error) => { console.error(`Unable to write to games/${game.id}`); console.error(error); }); const playerGame = Object.assign({}, reducedGame, { timestamp: Date.now(), status: error ? error : "success", name: session.name, color: session.color, order: (session.color in game.players) ? game.players[session.color].order : 0, player: session.player, sessions: reducedSessions, layout: layout }); return res.status(200).send(playerGame); } const resetGame = (game) => { delete game.turn; game.state = 'lobby'; game.placements = { corners: [], roads: [] }; Object.assign(game, { sheep: 19, ore: 19, wool: 19, brick: 19, wheat: 19, longestRoad: null, largestArmy: null, developmentCards: assetData.developmentCards.slice() }); for (let key in game.players) { Object.assign(game.players[key], { wheat: 0, sheep: 0, stone: 0, brick: 0, wood: 0, roads: 15, cities: 4, settlements: 5, points: 0, development: [] }); } game.developmentCards = assetData.developmentCards.slice(); shuffle(game.developmentCards); for (let i = 0; i < layout.corners.length; i++) { game.placements.corners[i] = { color: undefined, type: undefined }; } for (let i = 0; i < layout.roads.length; i++) { game.placements.roads[i] = { color: undefined, type: undefined }; } for (let key in game.players) { game.players[key].order = 0; delete game.players[key].orderRoll; delete game.players[key].orderStatus; } delete game.turn; } const createGame = (id) => { /* Look for a new game with random words that does not already exist */ while (!id) { id = randomWords(4).join('_'); console.log(`Looking for ${id}`); try { /* If file can be read, it already exists so look for a new name */ accessSync(`games/${id}`, fs.F_OK); id = ''; } catch (error) { console.log(error); break; } } const game = { startTime: Date.now(), turns: 0, state: "lobby", /* lobby, active, finished */ tokens: [], players: { R: getPlayer(), O: getPlayer(), B: getPlayer(), W: getPlayer() }, developmentCards: assetData.developmentCards.slice(), dice: [ 0, 0 ], sheep: 19, ore: 19, wool: 19, brick: 19, wheat: 19, longestRoad: null, largestArmy: null, chat: [], id: id }; addChatMessage(game, null, `New game started for ${id}`); [ "pips", "borders", "tiles" ].forEach((field) => { game[field] = assetData[field] }); resetGame(game); games[game.id] = game; shuffleBoard(game); console.log(`New game created: ${game.id}`); return game; }; 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 = createGame(id); return sendGame(req, res, game); }); const shuffleBoard = (game) => { const seq = []; for (let i = 0; i < 6; i++) { seq.push(i); } shuffle(seq); game.borderOrder = seq.slice(); for (let i = 6; i < 19; i++) { seq.push(i); } shuffle(seq); game.tileOrder = seq.slice(); /* Pip order is from one of the random corners, then rotate around * and skip over the desert (robber) */ /* Board: * 0 1 2 * 3 4 5 6 * 7 8 9 10 11 * 12 13 14 15 * 16 17 18 */ const order = [ [ 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 4, 5, 10, 14, 13, 8, 9 ], [ 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 5, 10, 14, 13, 8, 4, 9 ], [ 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 10, 14, 13, 8, 4, 5, 9 ], [ 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 14, 13, 8, 4, 5, 10, 9 ], [ 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 13, 8, 4, 5, 10, 14, 9 ], [ 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9 ] ] const sequence = order[Math.floor(Math.random() * order.length)]; game.pipOrder = []; for (let i = 0, p = 0; i < sequence.length; i++) { const target = sequence[i]; /* If the target tile is the desert (18), then set the * pip value to the robber (18) otherwise set * the target pip value to the currently incremeneting * pip value. */ if (game.tiles[game.tileOrder[target]].type === 'desert') { game.robber = target; game.pipOrder[target] = 18; } else { game.pipOrder[target] = p++; } } 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;