1
0
James Ketrenos c6b29ff580 Rebuilding Trade menu
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-03-02 20:32:46 -08:00

3125 lines
88 KiB
JavaScript
Executable File

"use strict";
const express = require("express"),
router = express.Router(),
crypto = require("crypto"),
{ readFile, writeFile } = require("fs").promises,
fs = require("fs"),
accessSync = fs.accessSync,
randomWords = require("random-words");
const session = require("express-session");
const layout = require('./layout.js');
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
let gameDB;
require("../db/games").then(function(db) {
gameDB = db;
});
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 staticData = {
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 ]
]
};
const games = {};
const processTies = (players) => {
players.sort((A, B) => {
if (A.order === B.order) {
return B.orderRoll - A.orderRoll;
}
return B.order - A.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;
/* Reverse from high to low */
slots.reverse().forEach((slot) => {
slot.forEach(pips => {
if (pips.length !== 1) {
ties = true;
pips.forEach(player => {
player.orderRoll = 0;
player.order = order;
player.orderStatus = `Tied.`;
});
} 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';
game.direction = 'forward';
game.turn = {
name: getPlayerName(game, players[0]),
color: getPlayerColor(game, players[0])
};
placeSettlement(game, getValidCorners(game));
addChatMessage(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
addChatMessage(game, null, `Initial settlement placement has started!`);
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 sessionFromColor = (game, color) => {
for (let key in game.sessions) {
if (game.sessions[key].color === color) {
return game.sessions[key];
}
}
}
const distributeResources = (session, 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 (staticData.pips[index].roll === roll) {
if (game.robber === i) {
addChatMessage(game, null, `That pesky ${game.robberName} Roberson 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, sessionFromColor(game, color), `${playerNameFromColor(game, color)} receives ${message.join(', ')}.`);
}
}
const pickRobber = (game) => {
const selection = Math.floor(Math.random() * 3);
switch (selection) {
case 0:
game.robberName = 'Robert';
break;
case 1:
game.robberName = 'Roberta';
break;
case 2:
game.robberName = 'Velocirobber';
break;
}
}
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;
addActivity(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) {
game.turn.robberInAction = true;
delete game.turn.placedRobber;
const mustDiscard = [];
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;
mustDiscard.push(player);
} else {
delete player.mustDiscard;
}
}
}
if (mustDiscard.length === 0) {
addChatMessage(game, null, `ROBBER! ${game.robberName} Robber Roberson has fled, and no one had to discard!`);
addChatMessage(game, null, `A new robber has arrived and must be placed by ${game.turn.name}.`);
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 {
mustDiscard.forEach(player =>
addChatMessage(game, null, `The robber was rolled and ${getPlayerName(game, player)} must discard ${player.mustDiscard} resource cards!`)
);
}
} else {
distributeResources(session, game, game.turn.roll);
}
}
const newPlayer = () => {
return {
roads: MAX_ROADS,
cities: MAX_CITIES,
settlements: MAX_SETTLEMENTS,
points: 0,
status: "Not active",
lastActive: 0,
order: 0,
stone: 0,
wheat: 0,
sheep: 0,
wood: 0,
brick: 0,
army: 0,
development: []
};
}
const getPlayer = (game, color) => {
if (!game) {
return newPlayer();
}
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
};
}
/* Expire old unused sessions */
for (let id in game.sessions) {
const tmp = game.sessions[id];
if (tmp.color || tmp.name || tmp.player) {
continue;
}
if (tmp.player_id === session.player_id) {
continue;
}
/* 10 minutes */
if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) {
console.log(`Expiring old session ${id}`);
delete game.sessions[id];
}
}
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) {
try {
game = JSON.parse(game);
console.log(`Creating backup of games/${id}`);
await writeFile(`games/${id}.bk`, JSON.stringify(game));
} catch (error) {
console.log(`Attempting to load backup from games/${id}.bk`);
game = await readFile(`games/${id}.bk`)
.catch(() => {
console.error(error, game);
});
if (game) {
try {
game = JSON.parse(game);
console.log(`Restoring backup to games/${id}`);
await writeFile(`games/${id}`, JSON.stringify(game, null, 2));
} catch (error) {
console.error(error);
game = null;
}
}
}
}
if (!game) {
game = createGame(id);
}
/* 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) => {
for (let key in player) {
delete player[key];
}
Object.assign(player, newPlayer());
}
const canGiveBuilding = (game) => {
if (!game.turn.roll) {
return `Admin cannot give a building until the dice have been rolled.`;
}
if (game.turn.actions && game.turn.actions.length !== 0) {
return `Admin cannot give a building while other actions in play: ${game.turn.actions.join(', ')}.`
}
return undefined;
}
const adminActions = (game, action, value) => {
let color, player, parts, session, corners, error;
switch (action) {
case "debug":
if (parseInt(value) === 0 || value === 'false') {
delete game.debug;
} else {
game.debug = true;
}
break;
case "give":
parts = value.match(/^([^-]+)(-(.*))?$/);
if (!parts) {
return `Unable to parse give request.`;
}
const type = parts[1], card = 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 to give resources.`;
}
let done = true;
switch (type) {
case 'road':
error = canGiveBuilding(game);
if (error) {
return error;
}
if (session.player.roads === 0) {
return `Player ${game.turn.name} does not have any more roads to give.`;
}
let roads = getValidRoads(game, session.color);
if (roads.length === 0) {
return `There are no valid locations for ${game.turn.name} to place a road.`;
}
game.turn.free = true;
placeRoad(game, roads);
addChatMessage(game, null, `Admin gave a road to ${game.turn.name}.` +
`They must now place the road.`);
break;
case 'city':
error = canGiveBuilding(game);
if (error) {
return error;
}
if (session.player.cities === 0) {
return `Player ${game.turn.name} does not have any more cities to give.`;
}
corners = getValidCorners(game, session.color);
if (corners.length === 0) {
return `There are no valid locations for ${game.turn.name} to place a settlement.`;
}
corners = getValidCorners(game, session.color, 'settlement');
game.turn.free = true;
placeCity(game, corners);
addChatMessage(game, null, `Admin gave a city to ${game.turn.name}. ` +
`They must now place the city.`);
break;
case 'settlement':
error = canGiveBuilding(game);
if (error) {
return error;
}
if (session.player.settlements === 0) {
return `Player ${game.turn.name} does not have any more settlements to give.`;
}
corners = getValidCorners(game, session.color);
if (corners.length === 0) {
return `There are no valid locations for ${game.turn.name} to place a settlement.`;
}
game.turn.free = true;
placeSettlement(game, corners);
addChatMessage(game, null, `Admin gave a settlment to ${game.turn.name}. ` +
`They must now place the settlement.`);
break;
case 'wheat':
case 'sheep':
case 'wood':
case 'stone':
case 'brick':
const count = parseInt(card);
session.player[type] += count;
addChatMessage(game, null, `Admin gave ${count} ${type} to ${game.turn.name}.`);
break;
default:
done = false;
break;
}
if (done) {
break;
}
const index = game.developmentCards.findIndex(item =>
item.card.toString() === card && item.type === type);
if (index === -1) {
console.log({ card, type}, game.developmentCards);
return `Unable to find ${type}-${card} in the current deck of development cards.`;
}
let tmp = game.developmentCards.splice(index, 1)[0];
tmp.turn = game.turns ? game.turns - 1 : 0;
session.player.development.push(tmp);
addChatMessage(game, null, `Admin gave a ${card}-${type} to ${game.turn.name}.`);
break;
case "cards":
let results = game.developmentCards.map(card => `${card.type}-${card.card}`)
.join(', ');
return results;
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]}.`;
addActivity(game, session, message);
message = undefined;
processGameOrder(game, session.player, game.dice[0]);
break;
case 'normal':
processRoll(game, dice);
break;
}
break;
case "pass":
let name = game.turn.name;
const next = getNextPlayer(game, name);
game.turn = {
name: next,
color: getColorFromName(game, next)
};
game.turns++;
addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
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 ${colorToWord(color)},` : colorToWord(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 ${colorToWord(color)} (${value})`;
default:
return `Invalid admin action ${action}.`;
}
};
const setPlayerName = (game, session, name) => {
if (session.color) {
return `You cannot change your name while you have a color selected.`;
}
/* 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()) {
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
Object.assign(session, tmp);
delete game.sessions[key];
} else {
return `${name} is already taken and has been active in the last minute.`;
}
}
}
if (name.toLowerCase() === 'the bank') {
return `You cannot play as the bank!`;
}
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);
return undefined;
}
const colorToWord = (color) => {
switch (color) {
case 'O': return 'orange';
case 'W': return 'white';
case 'B': return 'blue';
case 'R': return 'red';
default:
return undefined;
}
}
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 ${colorToWord(session.color)}.`
addChatMessage(game, null, message);
} else {
message = `${name} is no longer ${colorToWord(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 ${colorToWord(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 ${colorToWord(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 addActivity = (game, session, message) => {
let date = Date.now();
if (game.activities.length && game.activities[game.activities.length - 1].date === date) {
date++;
}
game.activities.push({ color: session.color, message, date });
}
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 0;
}
/* If this corner is already being walked, skip it */
if (placedCorner.walking) {
return 0;
}
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];
if (placedRoad.walking) {
return;
}
const tmp = processRoad(game, color, roadIndex, placedRoad);
longest = Math.max(tmp, longest);
/*if (tmp > longest) {
longest = tmp;
placedCorner.longestRoad = roadIndex;
placedCorner.longest
}
longest = Math.max(
*/
});
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 0;
}
/* If this road is already being walked, skip it */
if (placedRoad.walking) {
return 0;
}
placedRoad.walking = true;
/* Calculate the longest road branching from both corners */
let roadLength = 1;
layout.roads[roadIndex].corners.forEach(cornerIndex => {
const placedCorner = game.placements.corners[cornerIndex];
if (placedCorner.walking) {
return;
}
roadLength += processCorner(game, color, cornerIndex, placedCorner);
});
return roadLength;
};
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 clearRoadWalking = (game) => {
/* Clear out walk markers on roads */
layout.roads.forEach((item, itemIndex) => {
delete game.placements.roads[itemIndex].walking;
});
/* Clear out walk markers on corners */
layout.corners.forEach((item, itemIndex) => {
delete game.placements.corners[itemIndex].walking;
});
}
const calculateRoadLengths = (game, session) => {
clearRoadWalking(game);
let currentLongest = game.longestRoad,
currentLength = currentLongest
? game.players[currentLongest].longestRoad
: -1;
/* Clear out player longest road counts */
for (let key in game.players) {
game.players[key].longestRoad = 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 A:', graphs);
clearRoadWalking(game);
graphs.forEach(graph => {
graph.longestRoad = 0;
graph.set.forEach(roadIndex => {
const placedRoad = game.placements.roads[roadIndex];
clearRoadWalking(game);
const length = processRoad(game, placedRoad.color, roadIndex, placedRoad);
if (length >= graph.longestRoad) {
graph.longestStartSegment = roadIndex;
graph.longestRoad = length;
}
});
});
console.log('Graphs B:', graphs);
console.log('Pre update:', game.placements.roads.filter(road => road.color));
for (let color in game.players) {
if (game.players[color] === 'Not active') {
continue;
}
game.players[color].longestRoad = 0;
}
graphs.forEach(graph => {
graph.set.forEach(roadIndex => {
const placedRoad = game.placements.roads[roadIndex];
clearRoadWalking(game);
const longestRoad = processRoad(game, placedRoad.color, roadIndex, placedRoad);
placedRoad.longestRoad = longestRoad;
game.players[placedRoad.color].longestRoad =
Math.max(game.players[placedRoad.color].longestRoad, longestRoad);
});
});
game.placements.roads.forEach(road => delete road.walking);
console.log('Post update:', game.placements.roads.filter(road => road.color));
let checkForTies = false;
console.log(currentLongest, currentLength);
if (currentLongest && game.players[currentLongest].longestRoad < currentLength) {
addChatMessage(game, session, `${playerNameFromColor(game, currentLongest)} had their longest road split!`);
checkForTies = true;
}
let longestRoad = 4, longestPlayers = [];
for (let key in game.players) {
if (game.players[key].status === 'Not active') {
continue;
}
if (game.players[key].longestRoad > longestRoad) {
longestPlayers = [ key ];
longestRoad = game.players[key].longestRoad;
} else if (game.players[key].longestRoad === longestRoad) {
if (longestRoad >= 5) {
longestPlayers.push(key);
}
}
}
console.log({ longestPlayers });
if (longestPlayers.length > 0) {
if (longestPlayers.length === 1) {
game.longestRoadLength = longestRoad;
if (game.longestRoad !== longestPlayers[0]) {
game.longestRoad = longestPlayers[0];
addChatMessage(game, session,
`${playerNameFromColor(game, game.longestRoad)} now has the longest ` +
`road (${longestRoad})!`);
}
} else {
if (checkForTies) {
game.longestRoadLength = longestRoad;
const names = longestPlayers.map(color => playerNameFromColor(game, color));
addChatMessage(game, session, `${names.join(', ')} are tied for longest ` +
`road (${longestRoad})!`);
}
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;
}
const isCompatibleOffer = (game, player, offer) => {
const isBank = offer.name === 'The bank';
let valid = player.gets.length === offer.gives.length &&
player.gives.length === offer.gets.length;
if (!valid) {
console.log(`Gives and gets lengths do not match!`);
return false;
}
console.log({
player: getPlayerName(game, player),
gets: player.gets,
gives: player.gives
}, {
name: offer.name,
gets: offer.gets,
gives: offer.gives
});
player.gets.forEach(get => {
if (!valid) {
return;
}
valid = offer.gives.find(item =>
(item.type === get.type || item.type === '*') &&
item.count === get.count) !== undefined;
});
if (valid) player.gives.forEach(give => {
if (!valid) {
return;
}
valid = offer.gets.find(item =>
(item.type === give.type || item.type === 'bank') &&
item.count === give.count) !== undefined;
});
return valid;
};
const isSameOffer = (player, offer) => {
const isBank = offer.name === 'The bank';
if (isBank) {
return false;
}
let same = player.gets && player.gives &&
player.gets.length === offer.gets.length &&
player.gives.length === offer.gives.length;
if (!same) {
return false;
}
player.gets.forEach(get => {
if (!same) {
return;
}
same = offer.gets.find(item =>
item.type === get.type && item.count === get.count) !== undefined;
});
if (same) player.gives.forEach(give => {
if (!same) {
return;
}
same = offer.gives.find(item =>
item.type === give.type && item.count === give.count) !== undefined;
});
return same;
};
/* Verifies player can make the offer */
const checkPlayerOffer = (game, player, offer) => {
let error = undefined;
console.log({
name: getPlayerName(game, player),
gets: offer.gets,
gives: offer.gives,
sheep: player.sheep,
wheat: player.wheat,
brick: player.brick,
stone: player.stone,
wood: player.wood,
});
offer.gives.forEach(give => {
if (!error) {
return;
}
if (!(give.type in player)) {
error = `${give.type} is not a valid resource!`;
return;
}
if (player[give.type] < give.count) {
error = `You do not have ${give.count} ${give.type}!`;
return;
}
if (offer.gets.find(get => give.type === get.type)) {
error = `You can not give and get the same resource type!`;
return;
}
});
if (!error) offer.gets.forEach(get => {
if (error) {
return;
}
if (offer.gives.find(give => get.type === give.type)) {
error = `You can not give and get the same resource type!`;
};
})
return error;
};
const gameSignature = (game) => {
if (!game) {
return "";
}
const salt = 251;
const signature =
game.borderOrder.map(border => `00${(Number(border)^salt).toString(16)}`.slice(-2)).join('') + '-' +
game.pipOrder.map((pip, index) => `00${(Number(pip)^salt^(salt*index)).toString(16)}`.slice(-2)).join('') + '-' +
game.tileOrder.map((tile, index) => `00${(Number(tile)^salt^(salt*index)).toString(16)}`.slice(-2)).join('');
return signature;
};
const setGameFromSignature = (game, border, pip, tile) => {
const salt = 251;
const borders = [], pips = [], tiles = [];
for (let i = 0; i < 6; i++) {
borders[i] = parseInt(border.slice(i * 2, (i * 2) + 2), 16)^salt;
if (borders[i] > 6) {
return false;
}
}
for (let i = 0; i < 19; i++) {
pips[i] = parseInt(pip.slice(i * 2, (i * 2) + 2), 16)^salt^(salt*i) % 256;
if (pips[i] > 18) {
return false;
}
}
for (let i = 0; i < 19; i++) {
tiles[i] = parseInt(tile.slice(i * 2, (i * 2) + 2), 16)^salt^(salt*i) % 256;
if (tiles[i] > 18) {
return false;
}
}
game.borderOrder = borders;
game.pipOrder = pips;
game.tileOrder = tiles;
return true;
}
const offerToString = (offer) => {
return offer.gives.map(item => `${item.count} ${item.type}`).join(', ') +
' in exchange for ' +
offer.gets.map(item => `${item.count} ${item.type}`).join(', ');
}
const placeRoad = (game, limits) => {
game.turn.actions = [ 'place-road' ];
game.turn.limits = { roads: limits };
}
const placeCity = (game, limits) => {
game.turn.actions = [ 'place-city' ];
game.turn.limits = { corners: limits };
}
const placeSettlement = (game, limits) => {
game.turn.actions = [ 'place-settlement' ];
game.turn.limits = { corners: 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);
/* Chat messages can set game flags and fields */
const parts = chat.message.match(/^set +([^ ]*) +(.*)$/i);
if (parts && parts.length === 3) {
switch (parts[1].toLowerCase()) {
case 'game':
if (parts[2].trim().match(/^beginner('?s)?( +layout)?/i)) {
setBeginnerGame(game);
addChatMessage(game, session, `${session.name} set game board to the Beginner's Layout.`);
break;
}
const signature = parts[2].match(/^([0-9a-f]{12})-([0-9a-f]{38})-([0-9a-f]{38})/i);
if (signature) {
if (setGameFromSignature(game, signature[1], signature[2], signature[3])) {
game.signature = parts[2];
addChatMessage(game, session, `${session.name} set game board to ${parts[2]}.`);
} else {
addChatMessage(game, session, `${session.name} requested an invalid game board.`);
}
}
break;
}
}
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, card;
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 = {};
for (let key in game.players) {
game.players[key].gives = [];
game.players[key].gets = [];
delete game.players[key].offerRejected;
}
addActivity(game, session, `${name} 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 = {};
addActivity(game, session, `${name} has cancelled trading negotiations.`);
break;
}
/* Any player can make an offer */
if (value === 'offer') {
const offer = req.body;
error = checkPlayerOffer(game, session.player, offer);
if (error) {
break;
}
if (isSameOffer(session.player, offer)) {
console.log(session.player);
error = `You already have a pending offer submitted for ${offerToString(offer)}.`;
break;
}
session.player.gives = offer.gives;
session.player.gets = offer.gets;
if (game.turn.name === name) {
/* This is a new offer from the active player -- reset everyone's
* 'offerRejected' flag */
for (let key in game.players) {
delete game.players[key].offerRejected;
}
game.turn.offer = offer;
}
addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
break;
}
/* Any player can reject an offer */
if (value === 'reject') {
session.player.offerRejected = true;
addActivity(game, session, `${session.name} rejected ${game.turn.name}'s 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 an offer.`;
break;
}
const offer = req.body;
let target;
error = checkPlayerOffer(game, session.player, offer);
if (error) {
break;
}
/* 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;
}
} else {
target = offer;
}
/* Verify the requesting offer wasn't jacked --\
* make sure the target.gives === player.gets and target.gives === player.gets */
if (!isCompatibleOffer(game, player, target)) {
error = `The requested offer does not match the negotiated terms!`;
break;
}
debugChat(game, 'Before trade');
/* Transfer goods */
player.gets.forEach(item => {
if (target.name !== 'The bank') {
target[item.type] -= item.count;
}
player[item.type] += item.count;
});
player.gives.forEach(item => {
if (target.name !== 'The bank') {
target[item.type] += item.count;
}
player[item.type] -= item.count;
});
addChatMessage(game, session, `${session.name} traded ` +
` ${offerToString(session.player)} ` +
`from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
delete game.turn.offer;
if (target) {
delete target.gives;
delete target.gets;
}
delete session.player.gives;
delete session.player.gets;
debugChat(game, 'After trade');
game.turn.actions = [];
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. New board signature: ${game.signature}.`;
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.robberInAction) {
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)
};
game.turns++;
addActivity(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;
pickRobber(game);
addChatMessage(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
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 };
} else {
game.turn.actions = [];
game.turn.robberInAction = false;
delete game.turn.limits;
addChatMessage(game, null, `The dread robber ${game.robberName} was placed on a terrain with no other players, ` +
`so ${game.turn.name} does not steal resources from anyone.`);
}
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);
}
});
debugChat(game, 'Before steal');
if (cards.length === 0) {
addActivity(game, session, `${playerNameFromColor(game, value)} 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 1 ${type} from ${playerNameFromColor(game, value)}.`);
}
debugChat(game, 'After steal');
game.turn.robberInAction = false;
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 (game.turn && game.turn.robberInAction) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
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.`;
}
debugChat(game, 'Before development purchase');
addActivity(game, session, `${session.name} purchased a development card.`);
addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, 1 sheep to purchase a development card.`)
player.stone--;
player.wheat--;
player.sheep--;
debugChat(game, 'After development purchase');
card = game.developmentCards.pop();
card.turn = game.turns;
player.development.push(card);
break;
case 'play-card':
if (game.state !== 'normal') {
error = `You cannot play 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 play a card until you have rolled.`;
break;
}
if (game.turn && game.turn.robberInAction) {
error = `Robber is in action. You can not play a card until all Robber tasks are resolved.`;
break;
}
card = req.body;
card = player.development.find(item => item.type == card.type && item.card == card.card);
if (!card) {
error = `The card you want to play was not found in your hand!`;
break;
}
if (player.playedCard === game.turns && card.type !== 'vp') {
error = `You can only play one development card per turn!`;
break;
}
if (card.played) {
error = `You have already played this card.`;
break;
}
/* Check if this is a victory point */
if (card.type === 'vp') {
let points = player.points;
player.development.forEach(item => {
if (item.type === 'vp') {
points++;
}
});
if (points < 10) {
error = `You can not play victory point cards until you can reach 10!`;
break;
}
addActivity(game, session, `${session.name} played a Victory Point card.`);
}
if (card.type === 'progress') {
switch (card.card) {
case 'road-1':
case 'road-2':
const allowed = Math.min(player.roads, 2);
if (!allowed) {
addActivity(game, session, `${session.name} played a Road Building card, but has no roads to build.`);
break;
}
let roads = getValidRoads(game, session.color);
if (roads.length === 0) {
addActivity(game, session, `${session.name} played a Road Building card, but they do not have any valid locations to place them.`);
break;
}
game.turn.active = 'road-building';
game.turn.free = true;
game.turn.freeRoads = allowed;
addActivity(game, session, `${session.name} played a Road Building card. They now place ${allowed} roads for free.`);
placeRoad(game, roads);
break;
case 'monopoly':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'monopoly';
addActivity(game, session, `${session.name} played the Monopoly card, and is selecting their resource type to claim.`);
break;
case 'year-of-plenty':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'year-of-plenty';
addActivity(game, session, `${session.name} played the Year of Plenty card.`);
break;
default:
addActivity(game, session, `Oh no! ${card.card} isn't impmented yet!`);
break;
}
}
card.played = true;
player.playedCard = game.turns;
if (card.type === 'army') {
player.army++;
addActivity(game, session, `${session.name} played a Kaniget!`);
if (player.army > 2 &&
(!game.largestArmy || game.players[game.largestArmy].army < player.army)) {
if (game.largestArmy !== session.color) {
game.largestArmy = session.color;
addChatMessage(game, session, `${session.name} now has the largest army (${player.army})!`)
}
}
game.turn.robberInAction = true;
delete game.turn.placedRobber;
addChatMessage(game, null, `The robber ${game.robberName} has fled before the power of the Knight, ` +
`but a new robber has returned and ${session.name} must now place them.`);
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);
}
}
break;
case 'select-resource':
if (!game || !game.turn || !game.turn.actions ||
game.turn.actions.indexOf('select-resource') === -1) {
error = `Please, let's not cheat. Ok?`;
console.log(game);
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
const type = value.trim();
switch (type) {
case 'wheat':
case 'brick':
case 'sheep':
case 'stone':
case 'wood':
break;
default:
error = `That is not a valid resource type!`;
break;
};
if (error) {
break;
}
addActivity(game, session, `${session.name} has chosen ${type}!`);
switch (game.turn.active) {
case 'monopoly':
const gave = [];
let total = 0;
for (let color in game.players) {
const player = game.players[color];
if (player.status === 'Not active') {
continue
}
if (color === session.color) {
continue;
}
if (player[type]) {
gave.push(`${playerNameFromColor(game, color)} gave ${player[type]} ${type}`);
session.player[type] += player[type];
total += player[type];
player[type] = 0;
}
}
if (gave.length) {
addChatMessage(game, session, `Players ${gave.join(', ')}. In total, ${session.name} received ${total} ${type}.`);
} else {
addActivity(game, session, 'No players had that resource. Wa-waaaa.');
}
break;
case 'year-of-plenty':
session.player[type] += 2;
addChatMessage(game, session, `${session.name} received 2 ${type} from the bank.`);
break;
}
delete game.turn.active;
game.turn.actions = [];
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 (game.turn && game.turn.robberInAction) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
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;
}
placeSettlement(game, corners);
addActivity(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 (!game.turn.free) {
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;
}
debugChat(game, 'Before settlement purchase');
player.settlements--;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, 1 wheat to purchase a settlement.`)
player.brick--;
player.wood--;
player.wheat--;
player.sheep--;
}
delete game.turn.free;
debugChat(game, 'After settlement purchase');
corner.color = session.color;
corner.type = 'settlement';
let bankType = undefined;
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;
}
bankType = (type === 'bank')
? '3 of anything for 1 resource'
: `2 ${type} for 1 resource`;
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
game.turn.actions = [];
game.turn.limits = {};
if (bankType) {
addActivity(game, session,
`${name} placed a settlement by a maritime bank that trades ${bankType}.`);
} else {
addActivity(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';
let bankType = undefined;
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;
}
bankType = (type === 'bank')
? '3 of anything for 1 resource'
: `2 ${type} for 1 resource`;
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
player.settlements--;
if (bankType) {
addActivity(game, session,
`${name} placed a settlement by a maritime bank that trades ${bankType}. ` +
`Next, they need to place a road.`);
} else {
addActivity(game, session, `${name} placed a settlement. ` +
`Next, they need to place a road.`);
}
placeRoad(game, layout.corners[index].roads);
}
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 (game.turn && game.turn.robberInAction) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
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;
}
placeCity(game, corners);
addActivity(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 (!game.turn.free) {
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';
debugChat(game, 'Before city purchase');
player.cities--;
player.settlements++;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 2 wheat, 1 stone to upgrade to a city.`)
player.wheat -= 2;
player.stone -= 3;
}
delete game.turn.free;
debugChat(game, 'After city purchase');
game.turn.actions = [];
game.turn.limits = {};
addActivity(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 (game.turn && game.turn.robberInAction) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
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;
}
placeRoad(game, roads);
addActivity(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 (!game.turn.free) {
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;
}
debugChat(game, 'Before road purchase');
player.roads--;
if (!game.turn.free) {
addChatMessage(game, session, `${name} spent 1 brick, 1 wood to purchase a road.`)
player.brick--;
player.wood--;
}
debugChat(game, 'After road purchase');
road.color = session.color;
addActivity(game, session, `${name} placed a road.`);
calculateRoadLengths(game, session);
if (game.turn.active === 'road-building') {
game.turn.freeRoads--;
if (game.turn.freeRoads === 0) {
delete game.turn.free;
delete game.turn.active;
delete game.turn.freeRaods;
}
let roads = getValidRoads(game, session.color);
if (roads.length === 0) {
delete game.turn.active;
delete game.turn.freeRaods;
addActivity(game, session, `${name} has another road to play, but there are no more valid locations.`);
} else if (game.turn.freeRoads !== 0) {
game.turn.free = true;
placeRoad(game, roads);
break; /* do not reset actions or limits -- player has another road to place! */
}
}
delete game.turn.free;
game.turn.actions = [];
game.turn.limits = {};
} else if (game.state === 'initial-placement') {
road.color = session.color;
addActivity(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 = {
name: next,
color: getColorFromName(game, next)
};
placeSettlement(game, getValidCorners(game));
calculateRoadLengths(game, session);
addChatMessage(game, null, `It is ${next}'s turn to 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 = staticData.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}`);
}
addActivity(game, session, `${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} did not discard enough and must discard ${player.mustDiscard} more cards.`);
break;
}
let move = true;
for (let color in game.players) {
const discard = game.players[color].mustDiscard;
if (discard) {
move = false;
}
}
if (move) {
addChatMessage(game, null, `Drat! A new robber has arrived and must be placed by ${game.turn.name}!`);
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);
}
}
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;
}
addChatMessage(game, null, `${name} requested to start the game.`);
game.state = state;
break;
}
break;
}
return sendGame(req, res, game, error);
})
const ping = (session) => {
session.ping = Date.now();
console.log(`Sending ping to ${session.name}`);
session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping }));
if (session.keepAlive) {
clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
}
router.ws("/ws/:id", async (ws, req) => {
const { id } = req.params;
/* Setup WebSocket event handlers prior to performing any async calls or
* we may miss the first messages from clients */
ws.on('error', (event) => {
console.error(`WebSocket error: `, event.message);
});
ws.on('open', (event) => {
console.log(`WebSocket open: `, event.message);
});
ws.on('message', async (message) => {
/* Ensure the session is loaded prior to the first 'message'
* being processed */
const game = await loadGame(id);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
return;
}
const session = getSession(game, req.session);
try {
const data = JSON.parse(message);
switch (data.type) {
case 'pong':
console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`);
break;
case 'game-update':
console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`);
sendGame(req, undefined, game, undefined, ws);
break;
}
} catch (error) {
console.error(error);
}
});
/* This will result in the node tick moving forward; if we haven't already
* setup the event handlers, a 'message' could come through prior to this
* completing */
const game = await loadGame(id);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
return;
}
const session = getSession(game, req.session);
console.log(`WebSocket connect from game ${id}:${session.name ? session.name : "Unnamed"}`);
if (session) {
console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`);
session.ws = ws;
if (session.keepAlive) {
clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
} else {
console.log(`No session found for WebSocket with id ${id}`);
}
});
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 debugChat = (game, preamble) => {
preamble = `Degug ${preamble.trim()}`;
let playerInventory = preamble;
for (let key in game.players) {
if (game.players[key].status === 'Not active') {
continue;
}
if (playerInventory !== '') {
playerInventory += ' player';
} else {
playerInventory += ' Player'
}
playerInventory += ` ${playerNameFromColor(game, key)} has `;
const has = [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].map(resource => {
const count = game.players[key][resource] ? game.players[key][resource] : 0;
return `${count} ${resource}`;
}).filter(item => item !== '').join(', ');
if (has) {
playerInventory += `${has}, `;
} else {
playerInventory += `nothing, `;
}
}
if (game.debug) {
addChatMessage(game, null, playerInventory.replace(/, $/, '').trim());
} else {
console.log(playerInventory.replace(/, $/, '').trim());
}
}
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 sendGameToSession = (session, reducedSessions, game, reducedGame, error, res) => {
const player = session.player ? session.player : undefined;
if (player) {
player.haveResources = player.wheat > 0 ||
player.brick > 0 ||
player.sheep > 0 ||
player.stone > 0 ||
player.wood > 0;
}
/* Strip out data that should not be shared with players */
delete reducedGame.developmentCards;
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: player,
sessions: reducedSessions,
layout: layout
});
if (!res) {
if (!error) {
if (!session.ws) {
console.error(`No WebSocket connection to ${session.name}`);
} else {
console.log(`Sending update to ${session.name}`);
session.ws.send(JSON.stringify({
type: 'game-update',
update: playerGame
}));
}
}
} else {
console.log(`Returning update to ${session.name ? session.name : 'Unnamed'}`);
res.status(200).send(playerGame);
}
}
const sendGame = async (req, res, game, error, wsUpdate) => {
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."
addChatMessage(game, null, message);
resetGame(game);
}
game.active = active;
/* 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;
});
/* Calculate points and determine if there is a winner */
for (let key in game.players) {
const player = game.players[key];
if (player.status === 'Not active') {
continue;
}
player.points = 0;
if (key === game.longestRoad) {
player.points += 2;
}
if (key === game.largestArmy) {
player.points += 2;
}
player.points += MAX_SETTLEMENTS - player.settlements;
player.points += 2 * (MAX_CITIES - player.cities);
player.unplayed = 0;
player.potential = 0;
player.development.forEach(card => {
if (card.type === 'vp') {
if (card.played) {
player.points++;
} else {
player.potential++;
}
}
if (!card.played) {
player.unplayed++;
}
});
if (!game.winner && (player.points >= 10 && session.color === key)) {
addChatMessage(game, null, `${playerNameFromColor(game, key)} won the game with ${player.points} victory points!`);
game.winner = key;
game.state = 'winner';
delete game.turn.roll;
}
}
/* If the game isn't in a win state, do not share development card information
* with other players */
if (game.state !== 'winner') {
for (let key in game.players) {
const player = game.players[key];
if (player.status === 'Not active') {
continue;
}
delete player.potential;
}
}
/* 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;
}
if (reduced.ws) {
delete reduced.ws;
}
if (reduced.keepAlive) {
delete reduced.keepAlive;
}
reducedGame.sessions[id] = reduced;
/* Do not send session-id as those are secrets */
reducedSessions.push(reduced);
}
if (!wsUpdate) {
/* Save per turn while debugging... */
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
}
if (wsUpdate) {
/* This is a one-shot request from a client to send the game-update over WebSocket */
sendGameToSession(session, reducedSessions, game, reducedGame);
} else {
for (let id in game.sessions) {
const target = game.sessions[id], useWS = target !== session;
if (useWS) {
if (!error) {
sendGameToSession(target, reducedSessions, game, reducedGame);
}
} else {
sendGameToSession(target, reducedSessions, game, reducedGame, error, res);
}
}
}
}
const resetGame = (game) => {
console.log(`Reseting ${game.id}`);
Object.assign(game, {
startTime: Date.now(),
state: 'lobby',
turns: 0,
turn: {},
sheep: 19,
ore: 19,
wool: 19,
brick: 19,
wheat: 19,
placements: {
corners: [],
roads: []
},
developmentCards: [],
chat: [],
activities: [],
pipOrder: game.pipOrder,
borderOrder: game.borderOrder,
tileOrder: game.tileOrder,
signature: game.signature,
players: game.players
});
delete game.longestRoad;
delete game.largestArmy;
delete game.longestRoadLength;
delete game.winner;
delete game.longestRoad;
/* Reset all player data */
for (let color in game.players) {
clearPlayer(game.players[color]);
}
/* Populate the game corner and road placement data as cleared */
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
};
}
/* Populate the game development cards with a fresh deck */
for (let i = 1; i <= 14; i++) {
game.developmentCards.push({
type: 'army',
card: i
});
}
[ 'monopoly', 'road-1', 'road-2', 'year-of-plenty']
.forEach(card => game.developmentCards.push({
type: 'progress',
card: card
}));
[ 'market', 'library', 'palace', 'university']
.forEach(card => game.developmentCards.push({
type: 'vp',
card: card
}));
shuffle(game.developmentCards);
/* Ensure sessions are connected to player objects */
for (let key in game.sessions) {
const session = game.sessions[key];
if (session.color) {
session.player = game.players[session.color];
session.player.status = 'Active';
session.player.lastActive = Date.now();
}
}
/* Put the robber back on the Desert */
for (let i = 0; i < game.pipOrder.length; i++) {
if (game.pipOrder[i] === 18) {
console.log(`Setting robber at ${i}`);
game.robber = i;
break;
}
}
}
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) {
break;
}
}
const game = {
id: id,
developmentCards: [],
players: {
O: newPlayer(),
R: newPlayer(),
B: newPlayer(),
W: newPlayer()
}
};
[ "pips", "borders", "tiles" ].forEach((field) => {
game[field] = staticData[field]
});
setBeginnerGame(game);
resetGame(game);
addChatMessage(game, null, `New game created with Beginner's Layout: ${game.id}`);
games[game.id] = game;
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 setBeginnerGame = (game) => {
pickRobber(game);
shuffle(game.developmentCards);
game.borderOrder = [];
for (let i = 0; i < 6; i++) {
game.borderOrder.push(i);
}
game.tileOrder = [
9, 12, 1,
5, 16, 13, 17,
6, 2, 0, 3, 10,
4, 11, 7, 14,
18, 8, 15
];
game.robber = 9;
game.pipOrder = [
5, 1, 6,
7, 2, 9, 11,
12, 8, 18, 3, 4,
10, 16, 13, 0,
14, 15, 17
];
game.signature = gameSignature(game);
}
const shuffleBoard = (game) => {
pickRobber(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);
game.signature = gameSignature(game);
}
/*
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;