3021 lines
85 KiB
JavaScript
Executable File
3021 lines
85 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;
|
|
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) {
|
|
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, `But 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);
|
|
}
|
|
} else {
|
|
mustDiscard.forEach(player =>
|
|
addChatMessage(game, null, `${getPlayerName(game, player)} must discard ${player.mustDiscard} resource cards the robber steals while fleeing!`)
|
|
);
|
|
}
|
|
} 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
|
|
};
|
|
}
|
|
|
|
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]}.`;
|
|
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 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 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) {
|
|
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;
|
|
}
|
|
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 = 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;
|
|
}
|
|
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 = 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} 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;
|
|
|
|
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 };
|
|
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, 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) {
|
|
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 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');
|
|
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;
|
|
}
|
|
addChatMessage(game, session, `${session.name} played a Victory Point card.`);
|
|
}
|
|
|
|
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, and is selecting their resource type to claim.`);
|
|
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, 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;
|
|
}
|
|
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;
|
|
}
|
|
placeSettlement(game, 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 (!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) {
|
|
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) {
|
|
addChatMessage(game, session,
|
|
`${name} placed a settlement by a maritime bank that trades ${bankType}.`);
|
|
} else {
|
|
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';
|
|
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) {
|
|
addChatMessage(game, session,
|
|
`${name} placed a settlement by a maritime bank that trades ${bankType}. ` +
|
|
`Next, they need to place a road.`);
|
|
} else {
|
|
addChatMessage(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);
|
|
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 (!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) {
|
|
player.wheat -= 2;
|
|
player.stone -= 3;
|
|
}
|
|
delete game.turn.free;
|
|
|
|
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;
|
|
}
|
|
placeRoad(game, 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 (!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) {
|
|
player.brick--;
|
|
player.wood--;
|
|
}
|
|
delete game.turn.free;
|
|
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 = {
|
|
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}`);
|
|
}
|
|
addChatMessage(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} 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();
|
|
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", (ws, req) => {
|
|
const { id } = req.params;
|
|
|
|
console.log(`WebSocket connect from game ${id}`);
|
|
|
|
let game;
|
|
if (!(id in games)) {
|
|
game = createGame(id);
|
|
} else {
|
|
game = games[id];
|
|
}
|
|
|
|
const session = getSession(game, req.session);
|
|
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}`);
|
|
}
|
|
|
|
ws.on('error', (event) => {
|
|
console.error(`WebSocket error: `, event.message);
|
|
});
|
|
|
|
ws.on('open', (event) => {
|
|
console.log(`WebSocket open: `, event.message);
|
|
});
|
|
|
|
ws.on('message', (message) => {
|
|
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;
|
|
}
|
|
} catch (error) {
|
|
console.error(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;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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);
|
|
});
|
|
|
|
for (let id in game.sessions) {
|
|
const target = game.sessions[id],
|
|
useWS = target !== session,
|
|
player = target.player ? target.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: target.name,
|
|
color: target.color,
|
|
order: (target.color in game.players) ? game.players[target.color].order : 0,
|
|
player: player,
|
|
sessions: reducedSessions,
|
|
layout: layout
|
|
});
|
|
|
|
if (useWS) {
|
|
if (!error) {
|
|
if (!target.ws) {
|
|
console.error(`No WebSocket connection to ${target.name}`);
|
|
} else {
|
|
console.log(`Sending update to ${target.name}`);
|
|
target.ws.send(JSON.stringify({
|
|
type: 'game-update',
|
|
update: playerGame
|
|
}));
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`);
|
|
res.status(200).send(playerGame);
|
|
}
|
|
}
|
|
}
|
|
|
|
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: [],
|
|
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) {
|
|
console.log(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;
|