"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 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: [ { left: "sheep", right: "bank" }, { center: "sheep" }, { left: "wheat", right: "bank" }, { center: "wood" }, { left: "sheep", right: "bank" }, { 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 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! ROBBER!`); 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 }; } 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; } } 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; switch (action) { case "state": switch (value) { case 'game-order': resetGame(game); game.state = 'game-order'; break; } break; case "roll": let 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])); } let session; 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 getValidCorners = (game) => { const limits = []; /* For each corner, if the corner already has a color set, skip it * 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) => { if (game.placements.corners[cornerIndex].color) { return; } let valid = true; 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; } 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); 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; switch (action) { 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; } if (!error) { 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.`); } 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 '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; } const corner = game.placements.corners[index]; if (corner.color) { error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`; break; } corner.color = session.color; corner.type = 'settlement'; if (game.state === 'initial-placement') { if (game.direction && game.direction === 'backward') { session.initialSettlement = index; } 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.`); } else { error = `Settlement placement not enabled for normal game play.`; break; } 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 === 'initial-placement') { road.color = session.color; addChatMessage(game, session, `${name} placed a road.`); 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) }; 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'; } } else { error = `Road placement not enabled for normal game play.`; break; } break; case 'place-city': error = `City placement not yet implemented!`; break; case 'discard': if (game.turn.roll !== 7) { error = `You can only discard due to the Robber!`; break; } const discards = req.body, player = session.player; 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: [] }; for (let key in game.players) { game.players[key].wheat = game.players[key].sheep = game.players[key].stone = game.players[key].brick = game.players[key].wood = 0; } 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;