1
0
James Ketrenos e865133cc1 Lots of tweaks and fixes
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-02-21 15:29:53 -08:00

2814 lines
79 KiB
JavaScript
Executable File

"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');
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
let gameDB;
require("../db/games").then(function(db) {
gameDB = db;
});
const router = express.Router();
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
const assetData = {
tiles: [
{ type: "desert", card: 0 },
{ type: "wood", card: 0 },
{ type: "wood", card: 1 },
{ type: "wood", card: 2 },
{ type: "wood", card: 3 },
{ type: "wheat", card: 0 },
{ type: "wheat", card: 1 },
{ type: "wheat", card: 2 },
{ type: "wheat", card: 3 },
{ type: "stone", card: 0 },
{ type: "stone", card: 1 },
{ type: "stone", card: 2 },
{ type: "sheep", card: 0 },
{ type: "sheep", card: 1 },
{ type: "sheep", card: 2 },
{ type: "sheep", card: 3 },
{ type: "brick", card: 0 },
{ type: "brick", card: 1 },
{ type: "brick", card: 2 }
],
pips: [
{ roll: 5, pips: 4 },
{ roll: 2, pips: 1 },
{ roll: 6, pips: 5 },
{ roll: 3, pips: 2 },
{ roll: 8, pips: 5 },
{ roll: 10, pips: 3 },
{ roll: 9, pips: 4 },
{ roll: 12, pips: 1 },
{ roll: 11, pips: 2 },
{ roll: 4, pips: 3 },
{ roll: 8, pips: 5 },
{ roll: 10, pips: 3 },
{ roll: 9, pips: 4 },
{ roll: 4, pips: 3 },
{ roll: 5, pips: 4 },
{ roll: 6, pips: 6 },
{ roll: 3, pips: 2 },
{ roll: 11, pips: 2 },
{ roll: 7, pips: 0 }, /* Robber is at the end or indexing gets off */
],
borders: [
[ "bank", undefined, "sheep" ],
[ undefined, "bank", undefined ],
[ "bank", undefined, "brick" ],
[ undefined, "wood", undefined ],
[ "bank", undefined, "wheat" ],
[ undefined, "stone", undefined ]
],
developmentCards: [
]
};
for (let i = 1; i <= 14; i++) {
assetData.developmentCards.push({
type: 'army',
card: i
});
}
[ 'monopoly', 'road-1', 'road-2', 'year-of-plenty'].forEach(card => assetData.developmentCards.push({
type: 'progress',
card: card
}));
[ 'market', 'library', 'palace', 'university'].forEach(card => assetData.developmentCards.push({
type: 'vp',
card: card
}));
const games = {};
const processTies = (players) => {
players.sort((A, B) => {
if (A.order === B.order) {
return B.orderRoll - A.orderRoll;
}
return 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';
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! ${game.gender === 'female' ? 'Roberta' : 'Robert'} Robber Roberson!`);
game.turn.robberInAction = true;
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: 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: []
};
}
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) {
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);
}
if (!game.pipOrder || !game.borderOrder || !game.tileOrder) {
console.log("Shuffling old save file");
shuffleBoard(game);
}
if (!game.pips || !game.borders || !game.tiles) {
[ "pips", "borders", "tiles" ].forEach((field) => {
game[field] = assetData[field]
});
}
if (game.state === 'active') {
game.state = 'initial-placement';
}
if (typeof game.turn !== 'object') {
delete game.turn;
}
if (!game.placements) {
resetGame(game);
}
/* Reconnect session player colors to the player objects */
for (let id in game.sessions) {
const session = game.sessions[id];
if (session.color && session.color in game.players) {
session.player = game.players[session.color];
} else {
session.color = undefined;
session.player = undefined;
}
}
for (let color in game.players) {
if (!game.players[color].development) {
game.players[color].development = [];
}
if (!game.players[color].army) {
game.players[color].army = 0;
}
}
games[id] = game;
return game;
};
const clearPlayer = (player) => {
player.status = 'Not active';
player.lastActive = 0;
player.order = 0;
delete player.orderRoll;
delete player.orderStatus;
}
const adminActions = (game, action, value) => {
let color, player, parts, session;
switch (action) {
case "debug":
if (parseInt(value) === 0 || value === 'false') {
delete game.debug;
} else {
game.debug = true;
}
break;
case "state":
switch (value) {
case 'game-order':
resetGame(game);
game.state = 'game-order';
break;
}
break;
case "give":
parts = value.match(/^([^-]+)-(.*)$/);
if (!parts) {
return `Unable to parse give request.`;
}
const type = parts[1], card = parts[2];
for (let id in game.sessions) {
if (game.sessions[id].name === game.turn.name) {
session = game.sessions[id];
}
}
if (!session) {
return `Unable to determine current player turn to give resources.`;
}
if (type in session.player) {
const count = parseInt(card);
session.player[card] += count;
addChatMessage(game, null, `Admin gave ${count} ${type} to ${game.turn.name}.`);
break;
}
const index = game.developmentCards.findIndex(item =>
item.card === 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]}.`;
addChatMessage(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 ${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()) {
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
tmp.player = game.sessions[key];
delete game.sessions[key];
} else {
return `${name} is already taken and has been active in the last minute.`;
}
}
}
if (name === '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);
}
const setPlayerColor = (game, session, color) => {
if (!game) {
return `No game found`;
}
const name = session.name, player = session.player;
/* Selecting the same color is a NO-OP */
if (session.color === color) {
return;
}
const priorActive = getActiveCount(game);
let message;
if (player) {
/* Deselect currently active player for this session */
clearPlayer(player);
if (game.state !== 'lobby') {
message = `${name} has exited to the lobby and is no longer playing as ${session.color}.`
addChatMessage(game, null, message);
} else {
message = `${name} is no longer ${session.color}.`;
}
session.player = undefined;
session.color = undefined;
}
/* Verify the player has a name set */
if (!name) {
return `You may only select a player when you have set your name.`;
}
/* If the player is not selecting a color, then return */
if (!color) {
if (message) {
addChatMessage(game, null, message);
}
return;
}
/* Verify selection is valid */
if (!(color in game.players)) {
return `An invalid player selection was attempted.`;
}
/* Verify selection is not already taken */
for (let key in game.sessions) {
const tmp = game.sessions[key].player;
if (tmp && tmp.color === color) {
return `${game.sessions[key].name} already has ${color}`;
}
}
/* All good -- set this player to requested selection */
session.player = getPlayer(game, color);
session.player.status = `Active`;
session.player.lastActive = Date.now();
session.color = color;
addChatMessage(game, session, `${session.name} has chosen to play as ${color}.`);
const afterActive = getActiveCount(game);
if (afterActive !== priorActive) {
if (priorActive < 2 && afterActive >= 2) {
addChatMessage(game, null,
`There are now enough players to start the game when you are ready.`);
}
}
};
const addChatMessage = (game, session, message) => {
game.chat.push({
from: session ? session.name : undefined,
color: session ? session.color : undefined,
date: Date.now(),
message: message
});
};
const getColorFromName = (game, name) => {
for (let id in game.sessions) {
if (game.sessions[id].name === name) {
return game.sessions[id].color;
}
}
return '';
};
const getLastPlayerName = (game) => {
let index = game.playerOrder.length - 1;
for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) {
return game.sessions[id].name;
}
}
return '';
}
const getFirstPlayerName = (game) => {
let index = 0;
for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) {
return game.sessions[id].name;
}
}
return '';
}
const getNextPlayer = (game, name) => {
let color;
for (let id in game.sessions) {
if (game.sessions[id].name === name) {
color = game.sessions[id].color;
break;
}
}
if (!color) {
return name;
}
let index = game.playerOrder.indexOf(color);
index = (index + 1) % game.playerOrder.length;
for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) {
return game.sessions[id].name;
}
}
return name;
}
const getPrevPlayer = (game, name) => {
let color;
for (let id in game.sessions) {
if (game.sessions[id].name === name) {
color = game.sessions[id].color;
break;
}
}
if (!color) {
return name;
}
let index = game.playerOrder.indexOf(color);
index = (index - 1) % game.playerOrder.length;
for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) {
return game.sessions[id].name;
}
}
return name;
}
const processCorner = (game, color, cornerIndex, placedCorner) => {
/* If this corner is allocated and isn't assigned to the walking color, skip it */
if (placedCorner.color && placedCorner.color !== color) {
return 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) {
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 = (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(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;
};
const checkOffer = (player, offer) => {
let error = undefined;
console.log({
player: getPlayerName(player),
gets: player.gets,
gives: player.gives
}, {
name: offer.name,
gets: offer.gets,
gives: offer.gives
});
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(', ');
}
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;
}
addChatMessage(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 = {};
addChatMessage(game, session, `${name} has cancelled trading negotiations.`);
break;
}
/* Any player can make an offer */
if (value === 'offer') {
const offer = req.body;
error = checkOffer(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;
}
addChatMessage(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;
addChatMessage(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 = checkOffer(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(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} has accepted a trade ` +
`offer to give ${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++;
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.robberInAction = false;
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);
}
});
debugChat(game, 'Before steal');
if (cards.length === 0) {
addChatMessage(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 ${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');
addChatMessage(game, session, `${session.name} purchased 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;
}
}
if (card.type === 'progress') {
switch (card.card) {
case 'road-1':
case 'road-2':
addChatMessage(game, session, `${session.name} played a Road Building card. The server is giving them 2 brick and 2 wood to build those roads!`);
player.brick += 2;
player.wood += 2;
break;
case 'monopoly':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'monopoly';
addChatMessage(game, session, `${session.name} played the Monopoly card.`);
break;
case 'year-of-plenty':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'year-of-plenty';
addChatMessage(game, session, `${session.name} played the Year of Plenty card.`);
break;
default:
addChatMessage(game, session, `Oh no! ${card.card} isn't impmented yet!`);
break;
}
}
card.played = true;
player.playedCard = game.turns;
if (card.type === 'army') {
player.army++;
addChatMessage(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, session, `${session.name} must now move the robber.`);
}
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;
}
addChatMessage(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 {
addChatMessage(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;
}
game.turn.actions = [ 'place-settlement' ];
game.turn.limits = { corners };
addChatMessage(game, session, `${game.turn.name} is considering placing a settlement.`);
break;
case 'place-settlement':
if (game.state !== 'initial-placement' && game.state !== 'normal') {
error = `You cannot place an item unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
index = parseInt(value);
if (game.placements.corners[index] === undefined) {
error = `You have requested to place a settlement illegally!`;
break;
}
/* If this is not a valid road in the turn limits, discard it */
if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) {
error = `You tried to cheat! You should not try to break the rules.`;
break;
}
corner = game.placements.corners[index];
if (corner.color) {
error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`;
break;
}
if (!player.banks) {
player.banks = [];
}
if (game.state === 'normal') {
if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to build a settlement.`;
break;
}
if (player.settlements < 1) {
error = `You have already built all of your settlements.`;
break;
}
debugChat(game, 'Before settlement purchase');
player.settlements--;
player.brick--;
player.wood--;
player.wheat--;
player.sheep--;
debugChat(game, 'After settlement purchase');
corner.color = session.color;
corner.type = 'settlement';
if (layout.corners[index].banks.length) {
layout.corners[index].banks.forEach(bank => {
const border = game.borderOrder[Math.floor(bank / 3)],
type = game.borders[border][bank % 3];
console.log(`Bank ${bank} = ${type}`);
if (!type) {
console.log(`Bank ${bank}`)
return;
}
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} placed a settlement.`);
calculateRoadLengths(game, session);
} else if (game.state === 'initial-placement') {
if (game.direction && game.direction === 'backward') {
session.initialSettlement = index;
}
corner.color = session.color;
corner.type = 'settlement';
if (layout.corners[index].banks.length) {
layout.corners[index].banks.forEach(bank => {
console.log(game.borderOrder);
console.log(game.borders);
const border = game.borderOrder[Math.floor(bank / 3)],
type = game.borders[border][bank % 3];
console.log(`Bank ${bank} = ${type}`);
if (!type) {
return;
}
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
player.settlements--;
player.maritime = player.banks.map(bank => game.borders[Math.floor(bank / 3) + bank % 3]);
game.turn.actions = ['place-road'];
game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */
addChatMessage(game, session, `Placed a settlement. Next, they need to place a road.`);
}
break;
case 'buy-city':
if (game.state !== 'normal') {
error = `You cannot purchase a city unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
if (!game.turn.roll) {
error = `You cannot build until you have rolled.`;
break;
}
if (player.wheat < 2 || player.stone < 3) {
error = `You have insufficient resources to build a city.`;
break;
}
if (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;
}
game.turn.actions = ['place-city'];
game.turn.limits = { corners };
addChatMessage(game, session, `${game.turn.name} is considering upgrading a settlement to a city.`);
break;
case 'place-city':
if (game.state !== 'normal') {
error = `You cannot place an item unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
index = parseInt(value);
if (game.placements.corners[index] === undefined) {
error = `You have requested to place a city illegally!`;
break;
}
/* If this is not a placement the turn limits, discard it */
if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) {
error = `You tried to cheat! You should not try to break the rules.`;
break;
}
corner = game.placements.corners[index];
if (corner.color !== session.color) {
error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`;
break;
}
if (corner.type !== 'settlement') {
error = `This location already has a city!`;
break;
}
if (player.wheat < 2 || player.stone < 3) {
error = `You have insufficient resources to build a city.`;
break;
}
if (player.city < 1) {
error = `You have already built all of your cities.`;
break;
}
corner.color = session.color;
corner.type = 'city';
debugChat(game, 'Before city purchase');
player.cities--;
player.settlements++;
player.wheat -= 2;
player.stone -= 3;
debugChat(game, 'After city purchase');
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} upgraded a settlement to a city!`);
break;
case 'buy-road':
if (game.state !== 'normal') {
error = `You cannot purchase a road unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
if (!game.turn.roll) {
error = `You cannot build until you have rolled.`;
break;
}
if (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;
}
game.turn.actions = ['place-road'];
game.turn.limits = { roads };
addChatMessage(game, session, `${game.turn.name} is considering building a road.`);
break;
case 'place-road':
if (game.state !== 'initial-placement' && game.state !== 'normal') {
error = `You cannot place an item unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
index = parseInt(value);
if (game.placements.roads[index] === undefined) {
error = `You have requested to place a road illegally!`;
break;
}
/* If this is not a valid road in the turn limits, discard it */
if (game.turn && game.turn.limits && game.turn.limits.roads && game.turn.limits.roads.indexOf(index) === -1) {
error = `You tried to cheat! You should not try to break the rules.`;
break;
}
const road = game.placements.roads[index];
if (road.color) {
error = `This location already has a road belonging to ${playerNameFromColor(game, road.color)}!`;
break;
}
if (game.state === 'normal') {
if (player.brick < 1 || player.wood < 1) {
error = `You have insufficient resources to build a road.`;
break;
}
if (player.roads < 1) {
error = `You have already built all of your roads.`;
break;
}
debugChat(game, 'Before road purchase');
player.roads--;
player.brick--;
player.wood--;
debugChat(game, 'After road purchase');
road.color = session.color;
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} placed a road.`);
calculateRoadLengths(game, session);
} else if (game.state === 'initial-placement') {
road.color = session.color;
addChatMessage(game, session, `${name} placed a road.`);
calculateRoadLengths(game, session);
let next;
if (game.direction === 'forward' && getLastPlayerName(game) === name) {
game.direction = 'backward';
next = name;
} else if (game.direction === 'backward' && getFirstPlayerName(game) === name) {
/* Done! */
delete game.direction;
} else {
if (game.direction === 'forward') {
next = getNextPlayer(game, name);
} else {
next = getPrevPlayer(game, name);
}
}
if (next) {
game.turn = {
actions: ['place-settlement'],
limits: { corners: getValidCorners(game) },
name: next,
color: getColorFromName(game, next)
};
calculateRoadLengths(game, session);
addChatMessage(game, null, `It is ${next}'s turn 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 = assetData.tiles[game.tileOrder[index]].type;
if (!(resource in receives)) {
receives[resource] = 0;
}
receives[resource]++;
}
});
let message = [];
for (let type in receives) {
player[type] += receives[type];
message.push(`${receives[type]} ${type}`);
}
addChatMessage(game, null, `${session.name} receives ${message.join(', ')}.`);
}
}
addChatMessage(game, null, `It is ${name}'s turn.`);
game.state = 'normal';
}
}
break;
case 'discard':
if (game.turn.roll !== 7) {
error = `You can only discard due to the Robber!`;
break;
}
const discards = req.body;
let sum = 0;
for (let type in discards) {
if (player[type] < parseInt(discards[type])) {
error = `You have requested to discard more ${type} than you have.`
break;
}
sum += parseInt(discards[type]);
}
if (sum > player.mustDiscard) {
error = `You have requested to discard more cards than you are allowed!`;
break;
}
for (let type in discards) {
player[type] -= parseInt(discards[type]);
player.mustDiscard -= parseInt(discards[type])
}
addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`);
if (player.mustDiscard) {
addChatMessage(game, null, `${session.name} must discard ${player.mustDiscard} more cards.`);
}
break;
case "state":
const state = value;
if (!state) {
error = `Invalid state.`;
break;
}
if (state === game.state) {
break;
}
switch (state) {
case "game-order":
if (game.state !== 'lobby') {
error = `You cannot start a game from other than the lobby.`;
break;
}
resetGame(game);
message = `${name} requested to start the game.`;
addChatMessage(game, null, message);
game.state = state;
break;
}
break;
}
return sendGame(req, res, game, error);
})
router.get("/:id", async (req, res/*, next*/) => {
const { id } = req.params;
// console.log("GET games/" + id);
let game = await loadGame(id);
if (game) {
return sendGame(req, res, game)
}
game = createGame(id);
return sendGame(req, res, game);
});
const 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 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."
addChatMessage(game, null, message);
resetGame(game);
}
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.robberInAction) {
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);
}
}
}
/* 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') {
player.potential++;
if (card.played) {
player.points++;
}
}
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;
}
reducedGame.sessions[id] = reduced;
/* Do not send session-id as those are secrets */
reducedSessions.push(reduced);
}
/* 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);
});
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
});
return res.status(200).send(playerGame);
}
const resetGame = (game) => {
game.state = 'lobby';
game.turns = 0;
game.placements = {
corners: [],
roads: []
};
Object.assign(game, {
sheep: 19,
ore: 19,
wool: 19,
brick: 19,
wheat: 19,
longestRoad: null,
largestArmy: null,
developmentCards: assetData.developmentCards.slice()
});
for (let key in game.players) {
Object.assign(game.players[key], {
wheat: 0,
sheep: 0,
stone: 0,
brick: 0,
wood: 0,
roads: MAX_ROADS,
cities: MAX_CITIES,
settlements: MAX_SETTLEMENTS,
points: 0,
development: [],
banks: [],
maritime: [],
army: 0,
playedCard: 0,
haveResources: false,
unplayed: 0,
longestRoad: 0,
mustDiscard: 0,
gives: [],
gets: []
});
game.players[key].order = 0;
delete game.players[key].orderRoll;
delete game.players[key].orderStatus;
}
shuffle(game.developmentCards);
for (let i = 0; i < layout.corners.length; i++) {
game.placements.corners[i] = {
color: undefined,
type: undefined
};
}
for (let i = 0; i < layout.roads.length; i++) {
game.placements.roads[i] = {
color: undefined,
type: undefined
};
}
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
};
console.log(`New game created with Beginner's Layout: ${game.id}`);
addChatMessage(game, null,
`New game created with Beginner's Layout: ${game.id}`);
[ "pips", "borders", "tiles" ].forEach((field) => {
game[field] = assetData[field]
});
resetGame(game);
games[game.id] = game;
setBeginnerGame(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) => {
game.gender = Math.random() > 0.5 ? 'male' : 'female';
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.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, beginnersGame) => {
game.gender = Math.random() > 0.5 ? 'male' : 'female';
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;